가장 가까운 곳부터 확정하자냥! 다익스트라
안녕, 나 코딩냥이다냥! 10장에서 BFS 로 최단 거리를 구했었지냥? 그런데 그건 모든 길의 비용이 같을 때만 통했다냥. 도로마다 거리가 다르면 어떡하냥?
그때 쓰는 게 다익스트라라냥. 가중치가 있는 그래프의 최단 거리라냥.
오늘의 학습 목표
- 가중치가 있으면 BFS 가 안 되는 이유를 안다.
- 다익스트라를 우선순위 큐로 짠다.
- 음수 간선에는 못 쓴다는 걸 안다.
- 경유지와 다중 출발로 응용한다.
- 역방향 그래프로 돌아오는 길을 구한다.
1단계 · BFS 로는 안 된다냥 (10분)
10장의 BFS 는 한 칸씩 퍼졌다냥. 모든 간선이 "1칸"이라 가까운 순서대로 나왔다냥. 그런데 간선마다 비용이 다르면 그 순서가 깨진다냥.
비유로 이해하기 — 지하철 vs 걷기
A 에서 B 로 한 정거장 바로 가는 길이 있고, C 를 거쳐 두 정거장 돌아가는 길도 있다냥. 정거장 수만 세는 BFS 는 "한 정거장" 이 짧다고 한다냥.
그런데 그 한 정거장이 아주 먼 거리고, 돌아가는 두 정거장이 가까우면? BFS 는 틀린다냥. 거리(가중치) 를 봐야 하는데 정거장 수 만 봤기 때문이라냥.
BFS 는 간선을 전부 1로 본다냥.
그래서 가중치가 다른 그래프에서 BFS 로 거리를 세면 틀린다냥. 오늘 1번 문제를 BFS 로 풀면 바로 걸린다냥. 가중치가 있으면 다익스트라라냥.
2단계 · 다익스트라의 아이디어 (13분)
핵심은 하나라냥. 아직 확정 안 된 것 중 가장 가까운 것부터 확정한다냥. 한 번 확정하면 다시는 안 바뀐다냥.
- 시작점의 거리를
0, 나머지는 무한대로 둔다 - 가장 가까운 미확정 노드를 꺼낸다
- 그 노드를 거쳐 이웃에 가는 게 더 짧으면 거리를 줄인다(완화)
- 큐가 빌 때까지 반복한다
비유로 이해하기 — 물결이 번지는 속도가 다르다냥
BFS 는 물결이 일정한 속도로 동그랗게 퍼졌다냥. 다익스트라는 길마다 속도가 다른 물결이라냥. 그래서 "지금 가장 먼저 도착할 곳"을 매번 골라야 한다냥. 그걸 우선순위 큐가 해준다냥.
3단계 · 우선순위 큐로 짠다냥 (13분)
"가장 가까운 것"을 매번 빨리 찾으려면 우선순위 큐(최소 힙)를 쓴다냥.
파이썬은 heapq 가 있다냥.
import heapq
INF = float("inf")
dist = [INF] * (n + 1)
dist[1] = 0
pq = [(0, 1)] # (거리, 노드) 를 넣는다냥
while pq:
d, u = heapq.heappop(pq) # 가장 가까운 것부터 꺼낸다냥
if d > dist[u]:
continue # 이미 확정됐으면 건너뛴다냥
for v, w in adj[u]:
if d + w < dist[v]: # 완화: 더 짧으면 줄인다냥
dist[v] = d + w
heapq.heappush(pq, (dist[v], v))if d > dist[u]: continue 를 꼭 넣자냥.
큐에는 같은 노드가 여러 번 들어갈 수 있다냥. 예전의 더 긴 거리가 남아 있을 수 있다냥. 꺼냈을 때 이미 더 짧은 거리로 확정됐으면 버려야 한다냥.
이걸 빼도 답은 맞지만, 쓸데없는 일을 반복해 느려진다냥. 습관으로 넣자냥.
거리는 아주 커질 수 있다냥.
간선이 4000개, 각 비용이 100만이면 거리가 40억까지 간다냥. 파이썬은
괜찮지만 다른 언어면 long/int64 를 쓰자냥. int 로 두면 넘친다냥.
4단계 · 음수 간선은 안 된다냥 (10분)
다익스트라에는 중요한 전제가 있다냥. 모든 간선의 비용이 0 이상이어야 한다냥. 음수가 있으면 틀린다냥.
왜 음수면 안 되냥?
다익스트라는 "가장 가까운 걸 확정하면 다시는 안 바뀐다" 를 믿는다냥. 이건 12장 그리디의 정신이라냥 — 눈앞에서 제일 가까운 걸 덥석 확정하는 거라냥.
그런데 음수 간선이 있으면, 나중에 음수 길을 타고 와서 이미 확정한 거리가 더 짧아질 수 있다냥. 확정을 못 믿게 되는 거라냥. 그러면 다익스트라가 틀린다냥.
음수 간선이 있는 최단 경로는 벨만-포드라는 다른 방법을 써야 한다냥. 이 수업 범위를 넘으니, 오늘은 "음수가 있으면 다익스트라는 안 된다" 만 기억하자냥. 오늘 문제는 전부 양수라 걱정 없다냥.
5단계 · 응용: 경유·다중 출발·역방향 (12분)
다익스트라 하나만 잘 짜두면, 조금씩 바꿔서 여러 문제를 푼다냥.
1 → v → n 처럼 v 를 꼭 거쳐야 하면, 두 조각으로 나눈다냥.
1 에서 다익스트라 → dist(1, v), v 에서 다익스트라 → dist(v, n).
둘을 더하면 답이라냥. 오늘 3번 문제라냥.
대피소가 여러 곳이고 "가장 가까운 대피소까지 거리" 를 구한다면, 모든 대피소를 거리 0 으로 큐에 넣고 시작한다냥. 10장의 다중 시작 BFS 와 똑같은 요령이라냥. 오늘 4번 문제라냥.
for src in sources:
dist[src] = 0
heapq.heappush(pq, (0, src))방향 그래프에서 n 에서 1 로 돌아오는 최단 거리는, 모든 화살표를
뒤집은 역방향 그래프에서 1 에서 n 으로 가는 거리와 같다냥.
그래서 간선을 넣을 때 정방향과 역방향을 둘 다 만들어두면, 왕복을 다익스트라 두 번으로 푼다냥. 오늘 5번 문제라냥.
오늘 배운 내용 정리
먼저 스스로 답해보고, "정답 보기"를 눌러서 맞는지 확인해보자냥!
이제 직접 풀어볼 차례다냥
오늘 문제는 다섯 개라냥. 1번 모든 곳까지, 2번 출발지→목적지, 3번 경유지, 4번 다중 출발(대피소), 5번 왕복(역방향)이라냥.
다익스트라 뼈대를 한 번 제대로 짜두면 나머지는 어디서 시작하고 무엇을 더하느냐만 바뀐다냥. 가보자냥~
