컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
s algo · 05

다익스트라

알고리즘최단 경로그래프

다익스트라(Dijkstra) 알고리즘은 가중치가 있는 그래프에서 시작 노드로부터 모든 노드까지의 최단 거리를 구합니다. 핵심은 "아직 확정되지 않은 노드 중 현재까지 알려진 거리가 가장 짧은 노드를 하나씩 확정해나간다"는 것입니다.

핵심 개념

거리 추정값 (dist)

시작 노드는 거리 0, 나머지는 모두 ∞(무한대)로 시작합니다. 알고리즘이 진행되며 더 짧은 경로를 찾을 때마다 이 추정값이 갱신됩니다.

핵심 개념

확정(finalize)

아직 확정 안 된 노드 중 거리 추정값이 가장 작은 노드를 골라 "이 거리가 최종 확정"이라고 못박습니다. 음수 가중치가 없다면 한 번 확정된 노드의 거리는 이후 바뀌지 않습니다.

핵심 개념

완화(relaxation)

노드를 확정할 때마다 그 노드와 연결된 인접 노드들의 거리를 "확정된 노드까지 거리 + 간선 가중치"와 비교해, 더 짧으면 갱신합니다.

핵심 개념

한 노드가 여러 번 갱신될 수 있다

아직 확정되지 않은 노드의 거리 추정값은 더 짧은 경로가 발견될 때마다 계속 갱신됩니다. 이 시각화의 예제에서는 한 노드가 세 번이나 갱신됩니다.

핵심 개념

왜 음수 가중치는 안 되는가

다익스트라는 "한 번 확정하면 그 거리가 최종"이라고 가정합니다. 간선에 음수 가중치가 있으면 나중에 더 짧은 경로가 나타나 이 가정이 깨질 수 있어, 다익스트라로는 올바른 결과를 보장할 수 없습니다.

핵심 개념

최단 경로 트리

각 노드를 확정할 때 "어느 노드를 거쳐 왔는지(prev)"를 함께 기록해두면, 알고리즘이 끝난 뒤 시작 노드에서 임의의 노드까지의 최단 경로를 역추적할 수 있습니다.

BFS와의 관계 — algo-04(BFS/DFS)에서 본 BFS는 모든 간선의 가중치가 1이라고 가정한 다익스트라의 특수한 경우로 볼 수 있습니다. 가중치가 모두 같다면 "가장 적게 거친 노드"와 "가장 짧은 거리"가 같은 의미이기 때문입니다.
왜 미확정 노드 중 최솟값을 고르는가 — 만약 더 큰 거리 추정값을 가진 노드를 먼저 확정하면, 나중에 더 작은 추정값을 가진 노드를 거쳐 그 노드로 가는 더 짧은 경로가 나타났을 때 이미 확정한 거리를 뒤집어야 합니다. 항상 최솟값부터 확정해야 이런 모순이 생기지 않습니다.
실제 구현에서는 우선순위 큐 — 이 시각화는 매 단계 전체 노드를 훑어 최솟값을 찾지만(O(V²)), 실제 구현에서는 최소 힙 기반 우선순위 큐를 사용하면 이진 힙 기준 O((V+E) log V)로 더 빠르게 동작합니다.
이 시각화에서 확인할 것 — "진행 과정" 탭에서 거리 추정값이 갱신되는 순간을, "최단 경로 트리" 탭에서 각 노드까지의 최종 경로가 어떻게 확정되는지 확인하세요.

다익스트라가 풀려던 문제

다익스트라 알고리즘은 에츠허르 다익스트라(Edsger W. Dijkstra)가 1959년 Numerische Mathematik에 실은 짧은 논문 "A note on two problems in connexion with graphs"에서 소개한 것으로 알려져 있습니다. 가중치가 있는 그래프에서 한 시작점으로부터 다른 모든 노드까지의 최단 거리를 구합니다. 오늘날에는 네트워크 라우팅 프로토콜의 경로 계산에도 쓰입니다.

숫자로 확인하기

이 시각화의 그래프 — 노드 6개, 간선 9개로, 노드 0에서 시작하면 최종 거리는 노드 0~5 순서대로 0, 3, 1, 6, 3, 8입니다. 노드 3까지의 최단 경로는 0 → 2 → 4 → 3(1 + 2 + 3 = 6)입니다.
갱신 횟수 — 노드 3의 거리 추정값은 ∞ → 9 → 8 → 6으로 세 번 갱신되고, 마지막 값 6이 확정됩니다. 먼저 찾은 경로(0 → 2 → 3, 합 9)가 최단이 아닐 수 있다는 뜻입니다.
V = 100,000일 때 — 매번 전체 노드를 훑어 최솟값을 찾는 방식은 V² = 10¹⁰번에 가까운 확인이 필요합니다. 간선이 E = 500,000개 정도인 희소한 그래프에서 이진 힙을 쓰면 (V + E) log₂ V ≈ 600,000 × 17 ≈ 10⁷ 정도로 줄어듭니다.

자주 하는 오해

음수 간선이 하나만 있어도 다익스트라로 조금 돌려 쓰면 된다 — 확정한 노드의 거리가 나중에 줄어들 수 있어 알고리즘의 전제가 깨집니다. 음수 간선이 있으면 벨만-포드 알고리즘처럼 다른 알고리즘을 씁니다.
가중치가 0인 간선도 안 된다 — 음수가 아니면 되므로 0은 허용됩니다. 조건은 '음수가 없다'입니다.
다익스트라는 두 노드 사이의 최단 경로만 구한다 — 시작 노드 하나에서 모든 노드까지의 거리를 한 번에 구합니다. 특정 목적지에서 멈추게 할 수도 있지만 기본 형태는 모든 노드를 확정합니다.
우선순위 큐만 쓰면 항상 O((V + E) log V)다 — 이진 힙 기준의 값입니다. 우선순위 큐의 구현에 따라 달라지고, 피보나치 힙을 쓰면 O(E + V log V)가 되는 것으로 알려져 있습니다.

면접에서 이렇게 물어요

면접 질문

음수 가중치가 있으면 왜 다익스트라가 안 되나요?

다익스트라는 확정한 노드의 거리가 이후에 바뀌지 않는다고 가정합니다. 음수 간선이 있으면 이미 확정한 노드로 더 짧게 가는 길이 나중에 생길 수 있어서 이 가정이 깨집니다.

면접 질문

우선순위 큐를 쓰면 왜 빨라지나요?

매 단계에서 확정 안 된 노드 중 거리가 가장 작은 것을 전체 탐색(O(V))으로 찾는 대신 힙에서 O(log V)에 꺼냅니다. 이진 힙을 쓰면 전체가 O((V + E) log V)입니다.

면접 질문

BFS와 다익스트라는 어떻게 다른가요?

모든 간선의 가중치가 같다면 BFS가 최단 거리를 구합니다. 가중치가 제각각이면 간선 수가 적은 경로가 짧은 경로가 아닐 수 있어서 다익스트라가 필요합니다.

면접 질문

최단 경로 자체는 어떻게 복원하나요?

노드를 갱신할 때 어느 노드를 거쳐 왔는지(이전 노드)를 같이 저장합니다. 끝난 뒤 목적지에서 이전 노드를 따라 시작점까지 거슬러 올라간 뒤 뒤집습니다.

실제 시스템에서는

OSPF 라우팅 — RFC 2328은 OSPF의 경로 계산에 다익스트라 알고리즘을 쓴다고 설명합니다. 라우터가 링크 상태 데이터베이스로 그래프를 만들고 자신을 시작점으로 최단 경로 트리를 계산합니다. 자세한 내용은 라우팅 토픽에서 봅니다.
Java — PriorityQueue는 힙 기반 우선순위 큐로 넣기와 꺼내기가 O(log n)입니다. 이미 들어 있는 항목의 우선순위를 낮추는 연산은 제공하지 않아서, 더 짧은 거리가 나오면 새 항목을 추가하고 뒤늦게 나오는 오래된 항목은 건너뛰는 방식으로 구현하는 것이 일반적입니다.
Python — 표준 라이브러리 heapq가 최소 힙을 제공합니다.

함께 보면 좋은 토픽

BFS와 DFS — 가중치가 없는 그래프의 최단 거리는 BFS로 구합니다.
그래프 — 가중치 그래프와 인접 리스트 표현을 봅니다.
힙 — 우선순위 큐의 바탕이 되는 힙을 봅니다.
라우팅 — 다익스트라가 라우팅에서 쓰이는 방식을 봅니다.
최소 신장 트리 — 다익스트라와 닮은 그리디 구조의 다른 그래프 알고리즘을 봅니다.
플로이드-워셜 — 한 출발점이 아니라 모든 정점 쌍의 최단 거리를 한 번에 구하는 방법을 봅니다.

참고 자료

Java PriorityQueue — PriorityQueue — Java SE 21 API
원 논문 — Dijkstra, "A note on two problems in connexion with graphs", Numerische Mathematik 1, 1959
원 논문 — Fredman, Tarjan, "Fibonacci heaps and their uses in improved network optimization algorithms", Journal of the ACM 34(3), 1987
교재 — Cormen, Leiserson, Rivest, Stein, "Introduction to Algorithms" 4판, "Single-Source Shortest Paths" 장

Java — 다익스트라 (배열 기반, O(V²))

/* ── 매 단계 미확정 노드 중 거리가 가장 짧은 노드를 선택 ───── */
public int[] dijkstra(int src, int[][] adjMatrix) {
    int n = adjMatrix.length;
    int[] dist = new int[n];
    boolean[] visited = new boolean[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[src] = 0;

    for (int count = 0; count < n; count++) {
        int u = -1;
        for (int i = 0; i < n; i++) {          // 미확정 노드 중 최솟값 탐색 — O(V)
            if (!visited[i] && (u == -1 || dist[i] < dist[u])) u = i;
        }
        if (dist[u] == Integer.MAX_VALUE) break;  // 남은 노드가 모두 도달 불가능
        visited[u] = true;

        for (int v = 0; v < n; v++) {              // u의 모든 인접 노드 완화(relax)
            if (adjMatrix[u][v] > 0 && !visited[v] && dist[u] + adjMatrix[u][v] < dist[v]) {
                dist[v] = dist[u] + adjMatrix[u][v];
            }
        }
    }
    return dist;
}

바깥 반복문이 n번, 매번 최솟값 탐색에 O(V) + 완화에 O(V)가 걸려 전체 O(V²)입니다. 노드 수가 적을 때는 단순하고 충분히 빠릅니다.
dist[u] + adjMatrix[u][v] < dist[v] 비교가 바로 "완화(relaxation)" — 확정된 노드를 거쳐 가는 것이 더 짧다면 갱신합니다.

Java — 다익스트라 (우선순위 큐, O((V+E) log V))

/* ── 최소 힙으로 "다음 확정 후보"를 빠르게 꺼냄 ───────────── */
public int[] dijkstraPQ(int src, List<List<int[]>> adj, int n) {
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[src] = 0;

    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);  // [노드, 거리] 오름차순
    pq.offer(new int[]{src, 0});

    while (!pq.isEmpty()) {
        int[] cur = pq.poll();
        int u = cur[0], d = cur[1];
        if (d > dist[u]) continue;         // 이미 더 짧은 경로로 확정된 노드 — 오래된 항목 무시

        for (int[] edge : adj.get(u)) {
            int v = edge[0], w = edge[1];
            if (d + w < dist[v]) {
                dist[v] = d + w;
                pq.offer(new int[]{v, dist[v]});   // 갱신된 거리로 큐에 다시 추가
            }
        }
    }
    return dist;
}

배열 버전과 원리는 같지만, "미확정 노드 중 최솟값 찾기"를 매번 O(V)로 훑는 대신 우선순위 큐가 O(log V)에 꺼내주므로 훨씬 빠릅니다.
같은 노드가 큐에 중복으로 들어갈 수 있어 if (d > dist[u]) continue;로 이미 쓸모없어진(더 나은 경로로 갱신된) 오래된 항목을 걸러냅니다.

Java — 최단 경로 역추적 (prev 배열)

/* ── 완화 성공 시 prev[v]=u를 기록해두면 경로 복원 가능 ────── */
if (dist[u] + w < dist[v]) {
    dist[v] = dist[u] + w;
    prev[v] = u;                       // "v는 u를 거쳐서 왔다"를 기록
}

/* ── 목표 노드에서 시작 노드 방향으로 거슬러 올라가며 경로 복원 ── */
public List<Integer> reconstructPath(int target, int[] prev) {
    List<Integer> path = new LinkedList<>();
    for (int at = target; at != -1; at = prev[at]) {
        path.add(0, at);              // 맨 앞에 삽입 → 최종적으로 시작→목표 순서가 됨
    }
    return path;
}

거리(dist)만 구하면 "얼마나 가까운지"는 알 수 있지만 "어떤 경로로 가는지"는 알 수 없습니다. prev 배열이 그 경로 정보를 담습니다.
목표 노드에서 시작 노드까지 prev를 따라 거슬러 올라간 뒤 순서를 뒤집으면(또는 맨 앞에 삽입하면), 시작 노드부터 목표 노드까지의 최단 경로가 만들어집니다.

Q1

다익스트라 알고리즘이 다루는 문제는?

음수 간선이 있으면 다익스트라는 정확하지 않을 수 있습니다.
다익스트라 알고리즘은 음수 가중치가 없는 그래프에서 하나의 출발점으로부터 모든 정점까지의 최단 경로를 구하는 알고리즘입니다.

Q2

다익스트라 알고리즘에서 '확정'된 노드의 의미는?

한 번 확정되면 그 값이 최종 최단 거리입니다.
다익스트라는 매 단계 가장 거리가 짧은 미확정 노드를 확정 처리하는데, 이후 이 노드의 거리는 더 짧아질 수 없으므로 최종 확정됩니다.

Q3

완화(Relaxation) 연산이 하는 일은?

'더 짧은 경로를 찾으면 업데이트한다'는 개념입니다.
완화는 현재 확정된 노드를 거쳐 이웃 노드로 가는 거리가 기존에 기록된 거리보다 짧다면 그 값으로 갱신하는 연산입니다.