K부터 출발해 모든 노드가 신호를 받을 수 있는 시간을 계산하라.
입력값 (u, v, w)는 각각 출발지, 도착지, 소요 시간으로 구성되며, 전체 노드의 개수는 N으로 입력받는다.
class Solution:
def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
graph = collections.defaultdict(list)
#그래프 인접 리스트 구성
for u, v, w in times:
graph[u].append((v, w))
#큐 변수: [(소요시간, 정점)]
Q = [(0, k)]
dist = collections.defaultdict(int)
#우선순위 큐 최솟값 기준으로 정점까지 최단 경로 삽입
while Q:
time, node = heapq.heappop(Q)
if node not in dist:
dist[node] = time
for v, w in graph[node]:
alt = time + w
heapq.heappush(Q, (alt, v))
#모든 노드의 최단 경로 존재 여부 판별
if len(dist) == n:
return max(dist.values())
return -1
이 문제에서는 다음과 같은 2가지 사항을 반별해야 한다.
1) 모든 노드가 신호를 받는 데 걸리는 시간
가장 오래 걸리는 노드까지의 시간
이는 다익스트라 알고리즘으로 추출할 수 있다.
2) 모든 노드에 도달할 수 있는지 여부
이는 모든 노드의 다익스트라 알고리즘 계산 값이 존재하는지 유무로 판별할 수 있다.
만약 노드가 8개인데 다익스트라 알고리즘 계산은 7개 밖에 할 수 없다면, 나머지 한 노드에는 도달할 수 없다는 의미이다.
heapq
를 사용하는 형태로 구현해본다.<우선순위 큐를 이용한 다익스트라 알고리즘 수도코드>
function Dijkstra(Graph, source):
dist[source] <- 0
create vertex priority queue Q
for each vertex v in Graph:
if v ≠ source
dist[v] <- INFINITY
prev[v] <- UNDEFINED
Q.add_with_priority(v, dist[v])
while Q is not empty:
u <- Q.extract_min()
for each neighbor v of u:
alt <- dist[u] + length(u, v)
if alt < dist[v]
dist[v] <- alt
prev[v] <- u
Q.decrease_priority(v, alt)
return dist, prev
Q.add_with_priority(v, dist[v])
, 그리고 우선순위 큐에서 최소 값 추출 u <- Q.extract_min()
을 통해서 이웃을 살펴보는 for each neighbor v of u:
수도코드를 확인할 수 있다.실행 가능한 파이썬 코드로 구현해보자.
그래프부터 구성한다.
위키피디아 수도코드는 이미 그래프가 구성된 상태라 가정하고 입력값을 받았지만, 이 문제에서 입력값은 (u, v, w) 아이템 목록으로 구성된 리스트 형태며, 이를 키/값 구조로 조회할 수 있는 그래프 구조 (파이썬에서는 딕셔너리로 구현하는 인접 리스트)는 아니다.
따라서 먼저 그래프를 인접 리스트로 표현하는 딕셔너리부터 만들어준다.
딕셔너리 생성을 쉽게하기 위해 collections.defaultdict
를 활용한다.
다음은 우선순위 큐를 위한 큐 변수를 정의한다.
큐 변수 Q
는 '(소요시간, 정점)` 구조로 구성한다.
초깃값은 시작점 K부터이므로, 소요 시간은 0이고, 따라서 (0, K)가 된다.
거리를 의미하는 dist
변수는 아직 아무것도 셋팅하지 않는다.
수도코드에는 큐에 add_with_priority()
, decrease_priority()
, extract_min()
3번의 연산을 내리도록 구현되어 있다.
여기서 decrease_priority()
가 문제다.
이 연산은 수도코드에서 우선순위를 조정하라는 의미로 쓰였는데, 우리가 사용하는 heapq
모듈은 우선순위 업데이트를 지원하지 않기 때문이다.
heapq
모듈은 이 기능을 지원하지 않는다.여기서는 decrease_priority()
가 필요 없도록 알고리즘을 살짝 변경해 구현해본다.
큐 순회를 시작하자마자 최솟값을 추출하는 건 수도코드와 동일하다. 그러나 바로 for each 구문으로 이웃 노드를 순회했던 수도코드와 달리, 그 작업은 잠시 뒤에 진행하고, 먼저 dist
에 node의 포함 여부부터 체크한다.
수도코드는 dist
를 이미 무한대 값으로 채우고 시작했지만, 우리는 dist
에 아무 값도 셋팅하지 않았기 때문에 처음에는 이 값이 비어있을 것이고, 이 경우에만 힙에 푸시(Push)하는 형태로 조금 다르게 구현할 것이다.
dist
에는 항상 최솟값이 셋팅될 것이다. <최단 경로를 찾아 나가는 과정 도식화>
입력값 [[3,1,5], [3,2,2], [2,1,2], [3,4,1], [4,5,1], [5,6,1], [6,7,1], [7,8,1], [8,1,1]]
여기서 N = 8이고 K = 3이다.
1)은 입력값을 그래프로 표현한 것이다.
2)의 Q
와 dist
는 위에서부터 차례로, 변수에 삽입되는 순서대로 값을 나열했다.
여기서 Q
는 우선순위 큐이므로 값이 계속 쌓이다가 낮은 값부터 하나씩 추출되면서(최소 힙이다) 제거된다.
dist
에 존재하지 않는다면 바로 dist
의 값으로 time
과 node
의 순서가 바뀌면서 입력된다.
이미 dist
에 키가 존재한다면 그 값은 버리게 된다.
dist
에는 항상 최솟값부터 셋팅되기 때문에 이미 값이 존재한다면 그 값은 더 오래 걸리는 경로이기 때문이다. 따라서 이 값은 버리게 된다그림의 2)에서는 [5,1], [6,1] 두 값이 버려지게 된다.
실제로 노드 1은 3->2->1 순서로 방문하면 소요 시간 4에 도달할 수 있으며, 이후에 삽입되려고 하는 5와 6은 모두 이보다 더 오래 걸리므로 모두 버린다.
마지막으로 모든 노드에 도달할 수 있는지 여부를 판별한다.
dist
딕셔너리의 키 개수가 N
과 동일한지 체크한다.
전체 노드 개수만큼이 모두 dist
에 있다면 모든 노드의 최단 경로를 구했다는 의미이고, 이는 모두 시작점에 도달이 가능하다는 의미이기도 하다.
만약 노드 개수가 하나라도 모자란다면 그 노드는 시작점에서 도달할 수 없다는 뜻이며, 앞서 2가지 판별 조건 중 두 번째 조건을 만족할 수 없기 때문에 -1을 리턴한다.