• TOC {:toc}

이 글은 백준 온라인 저지의 1092번 문제를 파이썬(Python)으로 풀이한 것을 모아놓은 글입니다.

일종의 연습 기록이며 제가 정답을 받은 코드와 참고할만한 다른 코드를 같이 기록합니다. 필요한 경우 코드에 대한 해설을 기록합니다만 코드는 통과했어도 해설은 틀릴 수 있기 때문에 가볍게 참고해주시길 부탁드립니다. 피드백은 편하신 방법으로 자유롭게 주시면 감사하겠습니다.

2021.07.12

메모리 시간 코드 길이
30220 KB 76 ms 599 B
단계 시작 시각 끝난 시각 걸린 시간
문제 이해 12:26:12 12:28:02
풀이 생각 1 12:28:41 12:33:00
풀이 생각 2 15:35:55 15:39:54
코딩 15:41:39 16:39:23
디버깅 1 19:38:29 20:18:08
디버깅 2 20:19:35 20:27:28
import sys

N = int(input())
crane = list(map(int, input().split()))
M = int(input())
boxes = list(map(int, input().split()))

crane.sort(reverse=True)
boxes.sort(reverse=True)

if boxes[0] > crane[0]:
    print(-1)
    sys.exit()

# 한 크레인이 박스를 옮기는 데 걸린 최대 시간
res = 0
# 각 크레인이 박스를 옮기는 데 걸린 시간
tmp_count = [0] * N
i, j = 0, 0

while i < M:
    # 박스의 무게가 무게 제한 보다 작을 때
    if boxes[i] <= crane[j]:
        # 현재 크레인이 res만큼 박스를 옮겼으면
        if tmp_count[j] == res:
            # 다음 크레인으로 넘어간다.
            j += 1
            # 마지막 크레인이면
            if j == N:
                # 처음 크레인으로 넘어가고
                j = 0
                # res를 1 증가시킨다.
                res += 1
            continue

        # 해당 크레인으로 박스를 옮기고
        tmp_count[j] += 1
        # 다음 박스로 넘어간다
        i += 1
    # 박스의 무게가 더 무거우면
    else:
        # res를 증가시키고
        res += 1
        # 앞의 모든 크레인으로 
        for k in range(j):
            # 박스를 옮긴다.
            tmp_count[k] += 1
        # j개의 박스가 옮겨진다.
        i += j

print(res)

아이디어 & 풀이

박스와 크레인 무게 제한 모두 내림차순으로 정렬한 뒤 박스의 무게와 크레인의 무게 제한을 비교한다.

  • 무게 제한이 큰 크레인부터 자신이 옮길 수 있는 박스를 옮기되
  • 최대한 모든 크레인이 균일한 수의 박스를 옮기는 것이 목표이다.
    • 이는 한 크레인이 너무 많은 박스를 옮기지 않아야 한다는 것을 의미한다.
    • 이를 위해 한 크레인이 박스를 옮기는 데 걸린 최대 시간 res를 따로 관리한다.
    • 모든 크레인이 이 값보다 적거나 같은 시간 동안 박스를 옮기도록 한다.

우선 입력받자마자 박스와 크레인의 첫 값을 비교한다.

  • 박스의 무게가 크레인의 최대 무게 제한보다 크면 ‘모든 박스’를 옮기는 게 불가능하므로
  • -1을 출력하고 sys.exit()을 이용해 실행을 종료한다.

박스를 옮기는 과정은 다음과 같다.

우선 현재 박스의 무게와 크레인 무게 제한을 비교한다. 박스의 무게가 무게 제한보다 작거나 같아서 박스를 옮길 수 있으면

  • 현재 크레인으로 박스를 옮기고 현재 크레인이 박스를 옮기는 데 걸린 시간 tmp_count[i]를 1 증가시킨 뒤
  • 다음 박스로 이동하면 된다.

다만 박스를 옮기기 전에, 현재 tmp_count[i]가 최대 시간보다 작은지 확인해야 한다.

  • 현재 크레인이 박스를 옮기는 데 걸린 시간이 최대 시간보다 같아졌으면 해당 크레인으로 박스를 더 옮기면 안 되기 때문에
  • 다음 크레인으로 넘어간다.
  • 현재 크레인이 마지막 크레인이었으면
    • 처음 크레인으로 이동하고
    • 첫 크레인부터 다시 박스를 추가로 쌓아야 하므로 res를 1 증가시킨다.

박스의 무게가 무게 제한보다 커서 박스를 옮길 수 없으면

  • 앞의 크레인 중 하나가 이를 대신 옮겨야 한다.
    • 앞의 크레인들은 이미 res 만큼 박스를 옮긴 상태이므로 이 중 하나의 tmp_count가 현재 res보다 1 늘어나게 된다.
    • res 역시 1 늘어나게 된다.
  • 이 경우, 앞의 모든 크레인이 남은 박스를 1개씩 더 옮기는 것이 무거운 박스를 미리 옮기게 되므로 효율적이다.
  • 정리하면,
    • 우선 res를 1 증가시키고
    • 첫 크레인(0)부터 현재 크레인(j)의 직전 크레인(j - 1)까지 tmp_count를 1 증가시킨 뒤
    • 그 수(j)만큼 다음 박스로 넘어간다.

모든 박스를 다 옮길 때까지 위의 과정을 반복한다.

디버그

  • 처음에 시간초과가 떴다.
    • 케이스 만들어서 테스트해 보니 무한루프가 돌았다.
    • 처음에 -1을 출력한 뒤 종료를 안 해서 다음 코드가 실행됐고, while 문에서 무한루프가 돈 것이었다.
    • -1을 출력한 이후에 실행을 끝나는 코드를 추가해서 해결.
  • tmp_count 단일 정수로 관리했는데 처음 크레인으로 돌아갈 때 tmp_count를 0으로 초기화시킨 것 때문에 틀렸다.
    • res가 늘어난 만큼 현재 각 크레인의 tmp_count도 증가한 상태로 처음 크레인으로 돌아가야 하는데 0으로 초기화시킨 것.
    • tmp_count를 크레인 별로 관리해서 해결했다.

피드백

  • 두 번째 문제점의 원인을 파악 못 해서 틀림 처리한 뒤 다른 방식으로 푼 정답을 확인해 제출했는데, 오답 정리의 느낌으로 틀린 풀이의 ‘아이디어 & 풀이’ 부분을 정리하다 보니 틀린 부분을 찾게 되었다. 앞으로도 비슷한 경우에 꼼꼼히 풀이 과정을 정리하면서 문제점을 파악해보면 좋을 것 같다.
  • 참고 답안 1처럼 초깃값을 설정하면
    • 모든 크레인을 돌아서 처음으로 돌아오는 과정을 없애고
    • 위에서 문제가 됐던 tmp_count도 리스트가 아니라 단일 정수 변수로 사용할 수 있다.

참고 답안 1

import sys
input = sys.stdin.readline

def solution(crane, boxes):
    res = 0

    if crane[0] < boxes[0]:
        return -1

    # 모든 크레인이 동시에 움직일 경우 최솟값
    min_count = M // N if M % N == 0 else M // N + 1
    res = min_count

    i, j, count = 0, 0, 0
    while i < M:
        # 해당 크레인이 할당량을 채울 경우
        if count >= res: 
            # 다음 크레인으로 이동한다.
            j += 1
            count = 0
        # 박스 무게가 크레인의 무게 제한보다 클 경우
        if boxes[i] > crane[j]:
            # 앞의 크레인 모두가 박스를 대신 옮기고
            i += j 
            # 목표(최대 시간)를 1 늘린다.
            res += 1
            continue
        # 현재 크레인의 할당량 1 증가
        count += 1

        # 다음 박스로 이동한다.
        i += 1  

    return res


N = int(input())
crane = list(map(int, input().split()))
M = int(input())
boxes = list(map(int, input().split()))

crane.sort(reverse=True)
boxes.sort(reverse=True)

print(solution(crane, boxes))

아이디어 & 풀이

위의 풀이와 전개는 동일하다.

M // N 혹은 M // N + 1의 값으로 res의 초깃값을 설정한 점이 가장 큰 차이점이다.

  • 크레인이 박스를 옮기는 데 걸린 최대 시간은 모든 크레인이 거의 동일한 수의 박스를 옮길 때 최소가 된다.
  • 모든 크레인이 동일한 수의 박스를 옮기면 각 크레인이 할당하는 박스의 수는 M // N 또는 M // N + 1이 된다.
  • 그러므로 박스를 옮기는 데 걸리는 시간은 이 값보다 작아질 수 없다.

이처럼 초깃값을 설정하고 시작하면 모든 크레인을 돌아서 처음으로 돌아오는 과정을 없애 코드를 더 간결하게 작성할 수 있다.

참고 답안 2

import sys
input = sys.stdin.readline

N = int(input())
crane = list(map(int, input().split()))
M = int(input())
box = list(map(int, input().split()))

crane.sort(reverse=True)
box.sort(reverse=True)

if box[0] > crane[0]:
    print(-1)
    sys.exit()

pos = [0] * N
moved = [False] * M
count = 0
res = 0

while count < M:
    for i, x in enumerate(crane):
        while pos[i] < M:
            if not moved[pos[i]] and x >= box[pos[i]]:
                moved[pos[i]] = True
                count += 1
                pos[i] += 1
                break
            else:
                pos[i] += 1
    res += 1

print(res)

아이디어 & 풀이

박스와 크레인 무게 제한 모두 내림차순으로 정렬한 뒤 박스의 무게와 크레인의 무게 제한을 비교하는 것은 위와 동일하다.

위와 다른 점은 박스 중점으로 순회하는 것이 아니라 ‘각 크레인’ 순회하면서 현재 남은 박스 중 옮길 수 있는 것을 옮긴다.

  • 이를 위해 각 크레인이 옮기려는 박스의 번호(box의 인덱스)를 나타내는 pos 리스트와
    • box의 인덱스 번째 원소가 해당 박스의 무게
    • box[pos[크레인 인덱스]]가 옮기려는 박스의 무게이다.
  • 각 박스가 옮겨졌는지 아닌지를 나타내는 moved 리스트를 관리한다.

박스를 옮기는 과정은 다음과 같다.

크레인을 순회하면서 각 크레인과 그 무게 제한에 대해 다음을 반복한다.

  • 박스를 처음부터 순회하면서
  • 현재 가리키는 박스가 아직 옮겨지지 않았고, 현재 크레인의 무게 제한보다 그 박스의 무게가 작거나 같은 경우 해당 박스를 옮긴다.
    • 해당 박스의 movedTrue로 바꾸고
    • 옮긴 박스의 수 count를 1 증가시킨 뒤
    • 다음 박스로 넘어간 후 break 한다.
  • 위의 조건을 만족하지 않으면 다음 박스로 넘어간다.
    • 옮기려는 박스의 번호를 1씩 증가시키는 것이므로 현재 pos 값을 1 올리면 된다.

모든 크레인을 순회하면 모든 크레인이 1분 동안 박스를 옮긴 것이므로 걸린 시간인 res를 1 증가시킨다.

옮긴 박스의 수 count가 전체 박스의 수가 될 때까지 반복한다.

참고 답안 3

import sys
input = sys.stdin.readline

N = int(input())
cranes = sorted(map(int, input().split()), reverse=True)
M = int(input())
boxes = sorted(map(int, input().split()), reverse=True)

if cranes[0] < boxes[0]:
    print(-1)
    sys.exit()


def move(count):
    # 크레인의 수에 count를 곱한 값이
    # 박스 수보다 적으면
    # 해당 크레인이 count 번 옮겨서는 박스를 모두 옮길 수 없기 때문에
    if N * count < M:
        # False를 반환
        return False
    # 첫 번째 크레인부터 M // count 번째 크레인까지
    for i in range(1, M // count):
        # i번째 크레인의 무게 제한이 i * count 번째 박스의 무게보다 작으면
        if boxes[i * count] > cranes[i]:
            # False를 반환
            return False
    # 그 외면 True를 반환
    return True


def binary_search(fnc, start, end):
    while start <= end:
        mid = (start + end) // 2
        if fnc(mid):
            ans = mid
            end = mid - 1
        else:
            start = mid + 1
    return ans


print(binary_search(move, 1, M))

아이디어 & 풀이

각 크레인이 동일한 횟수로 박스를 옮긴다고 가정하고 이진 탐색으로 모든 박스를 옮길 수 있는 횟수를 찾는다.

이진 탐색에서는 특정 함수가 반환한 결과를 기준으로 start, end를 재설정한다.

  • 함수 실행의 결과가 True이면 endmid 앞으로 가져와 더 작은 값의 범위에서 탐색하고
  • False이면 startmid 뒤로 가져가 더 큰 값의 범위에서 탐색한다.
  • 가장 일반적인 이진 탐색에서는 주어진 값과 해당 값을 단순 비교하는 것이 이에 해당한다.

이 문제에서는 move 함수를 정의해 이용한다.

  • 모든 크레인이 동일한 횟수로 박스를 옮긴다는 가정하에 각 크레인이 박스를 옮기는 횟수 count를 받아온다.
    • 이진 탐색에서 mid 값을 이 인자로 넘긴다.

move 함수가 동작하는 방식은 다음과 같다.

우선 크레인의 수 Ncount를 곱한 값과 박스의 수 M을 비교한다.

  • N * count는 모든 크레인이 옮기게 되는 박스의 총 개수로
  • 이 값이 M보다 작으면 해당 count로는 모든 박스를 옮길 수 없다.
  • 그러므로 False를 반환해 count를 더 큰 값의 범위에서 탐색해야 한다.

다음으로, 각 크레인이 분담한 박스의 무게를 감당할 수 있는지 확인해야 한다.

  • 각 크레인은 동일한 수(i.e., count)의 박스를 옮긴다.
  • 크레인과 박스 모두 내림차순으로 정리되어 있기 때문에 앞의 크레인부터 가장 무거운 count 개의 박스를 옮기는 것이 효율적이다.
    • 어차피 각자 count 개를 옮겨야 하면 무게 제한이 가장 큰 크레인이 가장 무거운 박스 count 개를 옮기는 게 제한에 걸릴 가능성이 작다.
  • 그러면 각 크레인이 옮기게 되는 가장 무거운 박스의 무게는 boxes[i * count]이다.
    • i번째 크레인이 옮기게 되는 박스의 번호는 i * count ~ (i + 1) * count - 1이다.
      • e.g., 0번 크레인이 0 ~ count - 1까지 count 개의 박스를 옮김.
    • boxes가 내림차순으로 정렬되어 있으므로 가장 첫 번째 박스가 가장 무겁다.
  • 즉, 크레인을 순회하면서 i번째 크레인의 무게 제한이 i * count번째 박스의 무게보다 작으면 해당 count로는 박스를 전부 옮길 수 없다.
  • 그러므로 역시 False를 반환한다.

그 외는 해당 count로 박스를 모두 옮길 수 있기 때문에 True를 반환한다.

참고 답안 4

# 풀이 1-1

N = int(input())
crane = list(map(int, input().split()))
M = int(input())
boxes = list(map(int, input().split()))

crane.sort(reverse=True)
boxes.sort(reverse=True)
count = [0 for _ in range(N)]

if boxes[0] > crane[0]:
    print(-1)

else:
    # 각 box에 대해서
    for box in boxes:
        idx = 0
        for i in range(N):
            # 박스 무게가 현재 크레인의 무게 제한보다 작거나 같고
            # 현재 크레인이 옮긴 박스의 수가 타깃 인덱스 번쨰 크레인에 비해 작으면
            if box <= crane[i] and count[idx] > count[i]:
                # 타깃 인덱스를 현재로 바꾼다.
                idx = i
            # 박스 무게가 현재 크레인의 무게 제한보다 크면
            elif box > crane[i]:
                # 크레인 탐색을 끝낸다.
                break
        # 타깃(현재) 인덱스의 크레인이 옮긴 박스의 수를 증가시킨다.
        count[idx] = count[idx] + 1

    print(max(count))

# 풀이 1-2

import sys
from heapq import heappush, heappop
input = sys.stdin.readline


def solve(cranes, boxes):
    if max(cranes) < max(boxes):
        return -1

    # 크레인이 옮긴 박스의 수도 같이 관리할 heap
    # 1-1의 count 역할
    crane_heap = []
    for box in boxes:
        # 현재 박스보다 무게 제한이 큰 크레인들을
        while cranes and cranes[-1] >= box:
            # count를 0으로 crane_heap에 넣는다.
            heappush(crane_heap, (0, cranes.pop()))
        # count가 최소이면서
        # 동일 count에서 무게 제한이 가장 작은 크레인을 pop 한 뒤
        count, crane = heappop(crane_heap)
        # count를 1 증가 시켜 다시 넣는다.
        heappush(crane_heap, (count + 1, crane))

    # crane_heap에서 최대 count 값을 갖는 원소의
    # 첫 번째 값(count)를 반환한다.
    return max(crane_heap)[0]


input()
cranes = list(map(int, input().split()))
input()
boxes = list(map(int, input().split()))

cranes.sort()
boxes.sort(reverse = True)

print(solve(cranes, boxes))

아이디어 & 풀이

박스를 순회하면서 해당 박스를 옮길 수 있으면서, 지금까지 가장 적은 박스를 옮긴 크레인을 찾는다.

  • 각 크레인이 옮긴 박스의 수를 담은 count를 먼저 정의한다.
  • 내림차순으로 정렬한 박스를 순회하면서 각 박스를 옮기기에 가장 적절한 크레인을 찾아 idx 값을 해당 크레인의 번호로 지정한다.
  • count[idx]의 값을 1 증가시키고
  • 다음 박스로 넘어간다.

적절한 idx 값을 찾는 과정은 다음과 같다.

  • idx를 0으로 초기화시킨다.
  • 각 크레인을 순회하면서 조건에 맞는 크레인의 번호(i)를 idx로 지정한다.
    • 박스 무게는 현재 크레인의 무게 제한보다 작거나 같고
    • 현재 크레인이 옮긴 박스의 수가 현재 idx 크레인이 옮긴 박스의 수보다 작아야 한다.
  • 크레인을 순회하면서 최대한 뒤쪽의(무게 제한이 작은) idx를 찾는다.
  • 해당 크레인의 무게 제한이 박스의 무게보다 작아지면 순회를 종료한다.

현재 idxcount 값을 1 증가시킨 뒤 다음 박스에 대해서도 위의 과정을 반복한다.

마지막으로 count 중 최댓값을 출력하면 된다.

풀이 1-2부터는 전개 자체는 비슷한데 가장 적은 박스를 옮긴 크레인을 찾고, count를 관리하는 방식이 조금씩 다르다.

  • 풀이 1-2: 힙을 이용한다.
    • 현재 박스의 무게를 감당할 수 있는 크레인만 힙에 집어넣은 뒤
      • box는 내림차순 정렬되어있기 때문에 앞의 box에서 넣은 크레인은 현재 box의 무게도 감당할 수 있다.
    • 힙 안의 원소 중에서 count 값이 가장 작은 원소를 pop 해 count 1 증가 시켜 다시 넣는다.