다익스트라 알고리즘 (Dijkstra's Algorithm)
한눈에 보기
음수 간선이 없는 그래프에서 한 정점으로부터의 최단 거리를 구하는 다익스트라 알고리즘의 그리디 원리, 우선순위 큐 구현, 복잡도 비교와 한계를 정리한다.
최종 업데이트 2026.09.17
다익스트라(Dijkstra) 알고리즘은 간선마다 비용이 다른 그래프에서 한 정점으로부터 나머지 모든 정점까지의 최단 거리를 구한다. BFS는 모든 간선의 비용이 같을 때만 최단 거리를 주므로, 비용이 제각각이면 다익스트라가 필요하다.
핵심 아이디어는 "아직 확정되지 않은 정점 중 가장 가까운 것부터 확정한다"는 탐욕 전략이다. 이 "가장 가까운 것 고르기"를 우선순위 큐로 처리하면 O(E log V)에 끝난다. 코딩테스트에서는 이 힙 버전만 알면 충분하다.
개념과 원리
시작점의 거리를 0, 나머지를 무한대로 두고 시작한다. 그리고 이 두 가지를 반복한다.
- 아직 확정되지 않은 정점 중 현재 기록된 거리가 가장 짧은 정점을 고른다. 그 거리를 최종 확정한다.
- 그 정점에서 나가는 간선들을 따라가 보며, 더 짧은 경로가 되면 기록을 갱신한다. 이것을 완화(relaxation)라고 부른다.
1번에서 고른 정점의 거리를 왜 바로 확정해도 되는가? 그 정점보다 가까운 미확정 정점이 없으니, 다른 경로를 돌아서 가면 반드시 더 멀어지기 때문이다. 간선 비용이 음수가 아니라는 전제가 여기서 쓰인다.
진행 과정 따라가기
위 그래프에서 정점 1을 시작점으로 돌려보면 이렇게 흘러간다. 표의 값은 각 시점에 기록된 최선의 거리이고, ∞는 아직 도달하지 못했다는 뜻이다.
| 확정한 정점 | 1 | 2 | 3 | 4 | 5 | 이때 일어난 일 |
|---|---|---|---|---|---|---|
| 시작 | 0 | ∞ | ∞ | ∞ | ∞ | 시작점만 0 |
| 1 (거리 0) | 0 | 4 | 1 | ∞ | ∞ | 1→2와 1→3을 완화 |
| 3 (거리 1) | 0 | 3 | 1 | ∞ | 9 | 3을 거치니 2가 4→3으로 줄었다 |
| 2 (거리 3) | 0 | 3 | 1 | 8 | 9 | 2→4를 완화 |
| 4 (거리 8) | 0 | 3 | 1 | 8 | 9 | 8+1=9는 기존 9와 같아 갱신 없음 |
| 5 (거리 9) | 0 | 3 | 1 | 8 | 9 | 모든 정점 확정, 종료 |
매번 미확정 정점 중 가장 작은 값을 골랐다는 점에 주목하자. 1 → 3 → 2 → 4 → 5 순으로 확정됐고, 이 순서는 정점 번호와도 간선 개수와도 무관하게 오직 거리 순이다.
음수 간선이 있으면 왜 안 되는가
다익스트라는 "한 번 확정한 정점은 다시 보지 않는다"는 전제 위에서 동작한다. 음수 간선이 있으면 이 전제가 깨진다. 나중에 훨씬 싼 경로가 나타날 수 있기 때문이다.
시간 복잡도
| 구현 방식 | 복잡도 | 설명 |
|---|---|---|
| 배열로 최솟값 찾기 | O(V²) | 매번 전체를 훑어 최소를 찾는다 |
| 우선순위 큐 (힙) | O(E log V) | 코딩테스트의 표준 |
간선이 정점 수에 비해 적은 희소 그래프에서는 힙 버전이 압도적으로 빠르다. 반대로 간선이 거의 모든 쌍에 존재하는 밀집 그래프(E ≈ V²)라면 O(V²) 버전이 오히려 나을 수도 있지만, 코딩테스트에서 그럴 일은 드물다.
구현 코드 예시
import heapq
INF = float('inf')
def dijkstra(graph, start, n):
"""graph[v] = [(다음 정점, 비용), ...] 형태의 인접 리스트."""
dist = [INF] * (n + 1)
dist[start] = 0
pq = [(0, start)] # (거리, 정점) — 거리가 앞에 와야 거리 기준으로 정렬된다
while pq:
d, v = heapq.heappop(pq)
if d > dist[v]: # 이미 더 짧은 경로로 처리한 정점이면 버린다
continue
for nxt, w in graph[v]:
nd = d + w
if nd < dist[nxt]: # 더 짧아질 때만 갱신하고 큐에 넣는다
dist[nxt] = nd
heapq.heappush(pq, (nd, nxt))
return dist
if d > dist[v]: continue 두 줄이 이 구현의 핵심이다. 힙에서 원소를 빼는 기능이 없어서 갱신할 때마다 새 항목을 밀어 넣는데, 그러다 보면 같은 정점이 여러 번 들어간다. 꺼낼 때 "이 값이 지금 기록보다 크면 낡은 정보"라고 판단해 건너뛰는 것이다. 이 검사를 빼도 답은 맞지만 불필요한 순회가 늘어 느려진다.
경로 자체가 필요하면 이전 정점을 기록해두고 거꾸로 따라가면 된다.
def dijkstra_with_path(graph, start, end, n):
dist = [INF] * (n + 1)
prev = [0] * (n + 1) # 어디서 왔는지 기록
dist[start] = 0
pq = [(0, start)]
while pq:
d, v = heapq.heappop(pq)
if d > dist[v]:
continue
for nxt, w in graph[v]:
if d + w < dist[nxt]:
dist[nxt] = d + w
prev[nxt] = v
heapq.heappush(pq, (d + w, nxt))
path, cur = [], end # 끝에서부터 거슬러 올라간다
while cur != start:
path.append(cur)
cur = prev[cur]
path.append(start)
return dist[end], path[::-1]
어떤 알고리즘을 쓸 것인가
| 상황 | 알고리즘 | 복잡도 |
|---|---|---|
| 모든 간선 비용이 같음 | BFS | O(V + E) |
| 비용이 다르고 전부 0 이상 | 다익스트라 | O(E log V) |
| 음수 간선이 있음 | 벨만-포드 | O(VE) |
| 모든 쌍의 최단 거리 | 플로이드-워셜 | O(V³) |
| 비용이 0 또는 1뿐 | 0-1 BFS (덱) | O(V + E) |
자주 하는 실수
- 힙에
(정점, 거리)순으로 넣는다. 파이썬 힙은 튜플의 첫 원소로 비교하므로 거리가 앞이어야 한다. 순서를 바꾸면 정점 번호가 작은 것부터 꺼내져서 완전히 다른 알고리즘이 된다. - 방문 배열을 따로 만들어 관리한다.
visited를 두는 대신d > dist[v]로 걸러내는 편이 간결하고 실수가 적다. 굳이 둔다면 꺼낼 때 표시해야 하며, 넣을 때 표시하면 갱신 기회를 놓친다. - 거리 초기값을
0이나 작은 수로 둔다. 반드시 충분히 큰 값이어야 한다.float('inf')나 문제 제약을 넘는 큰 정수를 쓴다. - 음수 간선에 그대로 쓴다. 오류 없이 조용히 틀린 답이 나온다. 문제에 음수 비용이 있는지 먼저 확인한다.
- 무방향 그래프에서 한 방향만 넣는다. 양쪽 모두
graph[a].append((b, w)),graph[b].append((a, w))로 넣어야 한다. - 도달 불가 정점을 처리하지 않는다. 결과에
INF가 남아 있으면 "도달 불가"라는 뜻이다. 그대로 출력하면 이상한 값이 찍힌다.
면접에서 이렇게 물어본다
- 다익스트라가 탐욕 알고리즘인 이유를 설명해 보라.
- 음수 간선이 있으면 왜 동작하지 않는지 반례를 들어 설명해 보라.
- 우선순위 큐를 쓰면 복잡도가 어떻게 개선되는가.
- 힙에서 낡은 항목을 걸러내는 방법과 그것이 필요한 이유는 무엇인가.
- BFS로 최단 거리를 구할 수 있는 조건은 무엇인가.
- 벨만-포드, 플로이드-워셜과 다익스트라를 어떤 기준으로 선택하겠는가.
- 최단 거리뿐 아니라 실제 경로를 구하려면 무엇을 추가해야 하는가.
알고리즘에서 같이 읽기
- 이분 탐색 (Binary Search)정렬된 배열에서 탐색 범위를 절반씩 줄여 O(log n)에 값을 찾는 이분 탐색의 원리와 동작 과정을 그림으로 정리했다. lower bound와 upper bound, 코딩테스트 단골인 파라메트릭 서치와 회전 정렬 배열, 구현 코드와 자주 하는 실수까지 다룬다.
- 비트마스크 (Bitmask)정수의 각 비트로 집합의 포함 여부를 표현하는 비트마스크 기법을 다룬다. 기본 비트 연산 정리, 부분집합 열거, 외판원 순회 동적 계획법 구현과 주의점을 정리한다.
- DFS와 BFS (Depth First Search, Breadth First Search)그래프의 모든 정점을 방문하는 DFS와 BFS의 원리와 방문 순서 차이를 스택과 큐로 설명하고, O(V + E) 복잡도, 구현 코드, 문제 유형별 선택 기준을 정리한다.
- 동적 계획법 (Dynamic Programming)겹치는 부분 문제와 최적 부분 구조를 전제로 부분 문제의 답을 저장해 재사용하는 동적 계획법을 다룬다. 하향식과 상향식 비교, 문제 풀이 단계, 배낭 문제 구현을 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기