파이썬 알고리즘 : 다리를 지나는 트럭

스택과 큐, 덱

2024년 3월 18일 알고리즘 문제풀이

문제

난이도

Lv.2

코드

1차

42.9점 / 100점

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from collections import deque

def solution(bridge_length, weight, truck_weights):
    answer = 0
    # 다리를 표현하기 위한 덱
    in_bridge = deque()
    
    # 다리의 길이와 트럭의 위치를 표현하기 위해 트럭이 없는 곳은 0인 원소로 채운다.
    for _ in range(bridge_length):
        in_bridge.append(0)
        
    
    for truck in truck_weights:
        # 현재 다리에 지금 주목하는 트럭이 올라가면 무게 제한을 초과할 경우
        # 가장 끝에 있는 원소를 빼낸다. 트럭일 수도, 빈 값(0)일 수도 있다.
        # 0번 인덱스가 빠지고 하나가 추가되기 때문에 모든 원소들의 index는 1씩 줄어든다.
        # 따라서 다리 위의 모든 트럭이 1칸 움직이는 것을 의미한다.
        while sum(in_bridge)+truck > weight:
                outgoing_truck = in_bridge.popleft()
                in_bridge.append(0)
                answer += 1
                # 만약 실제로 트럭이 빠졌다면, 이와 동시에 새로운 트럭이 올라올 수 있다.
                # 새로운 트럭이 올라오는 행위는 동시에 일어나므로 시간이 흐르지 않은것으로 치기 위해 뺸다.
                if outgoing_truck:
                    answer -= 1
        # 다리에 새로운 트럭이 올라올 수 있다면 올려준다.
        if sum(in_bridge)+truck <= weight:
            # 잘못된 부분: 위에서 동시에 올라온다고 해놓고 여기서 또 하나의 트럭을 빼주었다. 여기서 오답이 발생
            in_bridge.popleft()
            in_bridge.append(truck)   
            answer += 1
    # 모든 트럭을 다리 위에 올렸더라도, 도착까지 계산해야 한다.
    # 가장 도착지에서 먼 트럭이 도착지까지 걸리는 시간을 더해준다.
    for i in range(len(in_bridge)-1,-1,-1):
        if in_bridge[i]:
            answer += (i+1)
            break
    return answer

덱을 이용해야겠다는 생각은 보자마자 들었다. 배열에서 한 쪽으론 트럭을 빼내야 하고 한 쪽으론 집어 넣어야 해서 입구와 출구가 정해져있는 덱이 유리하겠다고 생각했다. 덱의 길이를 항상 bridge_length 만큼으로 유지하기 위해 항상 빈 곳은 0의 값을 채워 넣어 주었다. 다리의 길이가 1이 아니기 때문에, 다리 위에서 트럭이 움직일 떄도 시간이 흐르기 때문이다. 시간이 흐를 때마다 0번 index 값을 빼주고(popleft), 맨 뒤에 새로운 값을 삽입하면(append) 다리 위에 있는 모든 트럭의 index는 1씩 감소하게 된다. 따라서 1칸 씩 전진한 셈이 된다. 현재 트럭의 위치를 index로 설정하기 위해선 트럭이 있지 않은 공간을 0으로 설정하는 것은 내가 생각해도 잘한 것 같다. 특히나, 이렇게 하면 다리 길이보다 더 많은 트럭이 올라올 일은 걱정하지 않아도 된다.

문제를 풀면서 가장 까다로운 조건은 다리에 오르거나 다리에서 내려가는 트럭의 무게와 차지하는 위치는 무시되어야 한다는 것이다. 현재 다리 위에 더 이상 트럭을 올릴 수 없더라도 하나가 내려가는 순간 동시에 새로운 트럭이 올라올 수 있다. 로직 상 다리에서 트럭이 내려가는 것, 올라오는 것은 별도의 동작이지만 시간은 1번만 흘러가야 한다는 것을 구현하는게 힘들었다. 현재 로직상으론 풀지 못할 듯하여 방법을 바꿔서 생각해야 겠다.

2차

92.9/100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from collections import deque

def solution(bridge_length, weight, truck_weights):
    answer = 0
    in_bridge = deque()
    for _ in range(bridge_length):
        in_bridge.append(0)
    idx = 0
    while idx < len(truck_weights):
        answer += 1
        in_bridge.popleft()
        if sum(in_bridge) + truck_weights[idx] <= weight:
            in_bridge.append(truck_weights[idx])
            idx += 1
        else:
            in_bridge.append(0)
            
    for i in range(len(in_bridge)-1,-1,-1):
        if in_bridge[i]:
            answer += (i+1)
            break
    return answer

for문을 이용해 각 트럭을 기준으로 생각했었다. 이렇게 설정한 이유는 truck_weights 배열에서 주목할 트럭에 대한 인덱스를 관리하기 힘들 것이라고 생각했기 때문이다. 새롭게 다리에 올라갈 때만 주목할 트럭의 인덱스를 바꾸어주면 되니 생각만큼 어렵진 않을거라고 생각했다. 기존에는 동작에 따라 시간을 설정하였다. 다시 말해, 우선 어떤 트럭이 내려오고 다음 트럭이 동시에 올라갈 수 있는지 없는지를 따져서 시간을 얼마나 흐르게 할지 결정했다는 의미이다.

관점을 바꿔서, while 반복문을 통해 무조건 1초를 흐르게 하고 그 사이 발생하는 모든 동작을 로직에 포함하는 것으로 바꾸었다. 설명을 쉽게 하기 위해 다리에 무게가 없는 컨테이너가 있다고 생각하고 설명하겠다. 다리에 트럭이 올라갈 수 있는 갯수인 bridge_length 만큼 컨테이너 박스가 있다고 가정하자. 1초가 흐르는 순간 반드시 일어나야 하는 일은 다리에서 컨테이너박스 1개가 내려오는 것이다. 다음 행동은 다음 트럭이 올라올 수 있는지에 따라 나뉘어 지며 이때는 시간이 흐르지 않는다. while 문 1회 반복(1초)마다 발생하는 일은 다음과 같다.

  1. 가장 마지막에 있는 컨테이너박스를 내린다.

  2. 다음 올라올 차례인 트럭이 올라오더라도 무게 제한에 걸리지 않는지 확인한다.

    1. 올라와도 괜찮다면 다리에 트럭을 올린다. 첫번째 컨테이너 박스에 집어넣는다고 생각하면 된다. .
    2. 무게제한으로 올라올 수 없다면 빈 컨테이너박스를 올려준다.

이렇게 되면 while문이 반복된 만큼만 시간이 흐른다. while문은 다음 주목할 트럭을 나타내는 index가 트럭 배열보다 커지면 종료된다. 즉, 종료 시점에 아직 다리에 있는 트럭이 있다는 의미이다. 그 트럭의 index는 그 트럭보다 앞에 있는 컨테이너 박스들의 갯수를 의미한다. 따라서 (index+1) 초 후 모든 트럭이 도착한다.

1가지 테스트 케이스가 시간 초과되었다.

3차

100/100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from collections import deque

def solution(bridge_length, weight, truck_weights):
    answer = 0
    
    # 다리 위 상태를 표현할 배열
    in_bridge = deque()
    
    # 다리의 길이와 트럭의 위치를 표현하기 위해 트럭이 없는 곳은 0인 원소로 채운다.
    for _ in range(bridge_length):
        in_bridge.append(0)
    
    # 다음 올라올 차례의 트럭을 가리키는 index
    idx = 0
    # 다리 위의 트럭들의 무게의 합을 나타내는 변수
    total_weight = 0
    
    # 더 이상 올라올 트럭이 없을때까지 반복
    while idx < len(truck_weights):
        # 매 반복은 1초의 흐름을 의미
        answer += 1
        
        # 반복마다 가장 먼저 발생하는 것은 마지막칸이 다리에서 내려오는 것.
        # 이로서 다리위의 모든 원소들의 index가 1 줄어든다.
        # 다리 위의 트럭들이 1칸 씩 전진했음을 의미한다.
        total_weight -= in_bridge.popleft()
        
        # 새로운 트럭이 올라와도 무게 제한에 걸리지 않는다면?
        # 트럭을 추가하고, 무게도 더해준다.
        # 다음 올라올 트럭을 가리키도록 index도 1 더해준다.
        if total_weight + truck_weights[idx] <= weight:
            in_bridge.append(truck_weights[idx])
            total_weight += truck_weights[idx]
            idx += 1
        # 새로운 트럭이 올라오면 안될 경우
        # 빈 값을 더해준다.
        else:
            in_bridge.append(0)
            
    # 모든 트럭이 올라올 경우 while문은 종료되지만, 모든 트럭이 다리에서 내려오는 시간까지 더해주어야 한다.
    # 가장 멀리있는 트럭의 index를 더해준다.
    for i in range(len(in_bridge)-1,-1,-1):
        if in_bridge[i]:
            answer += (i+1)
            break
    return answer

반복문이 겹쳐져 있다보니 최악의 경우 시간이 초과되는 건가 생각했다. 예를 들어, 길이가 10,000인 다리에 트럭이 10,000대 이동할 때 모든 트럭의 무게가 10,000 이라면 1대가 끝까지 도착할 때 까지 다른 트럭은 꼼짝없이 기다려야 한다. 하지만 이런 경우를 따로 빼주는 건 무의미하다고 생각했다(예를 들어, 한대만 9999면 그것도 if문으로 빼줄 순 없는 노릇이다!). 시간 복잡도에 영향을 줄 수 있는건 sum 함수라고 생각해서 무게를 더한 누적값으로 관리하도록 바꿔주었더니 생각보다 쉽게 해결되었다.