개념

다익스트라는 간선의 가중치가 모두 00 이상일 때, 한 시작점에서 다른 모든 정점까지의 최단 거리를 구합니다. 간선마다 이동 비용이 다르고, 한 곳에서 출발하는 최단 경로가 필요할 때 사용합니다. 모든 간선의 비용이 같다면 BFS로 이동 횟수를 구할 수 있습니다.

더 짧은 경로로 거리 갱신하기

시작점까지의 거리는 00으로 두고, 나머지 정점은 아직 도달하지 못한 상태로 둡니다. 정점 uu까지 현재 찾은 거리를 d(u)d(u), uu에서 vv로 가는 간선의 가중치를 ww라고 하면 uu를 거쳐 vv까지 가는 비용은 d(u)+wd(u)+w입니다. 이 값이 기존의 d(v)d(v)보다 작으면 거리를 바꿉니다. 더 짧은 경로를 찾을 때마다 저장된 거리를 갱신하는 방식입니다.

아래는 정점 88개와 방향 간선 1212개가 있는 그래프입니다. 시작점은 11입니다. 간선 위에는 비용을, 아래 표에는 현재 기록한 거리를 표시합니다. 진한 정점은 거리가 확정된 정점이고, 진한 간선은 현재 찾은 경로의 직전 간선입니다. ∞\infty는 아직 경로를 찾지 못한 상태입니다.

11의 간선을 확인하면 22까지 44, 33까지 77을 기록합니다.

4 7 2 5 1 6 3 8 4 2 5 3 1 2 3 4 5 6 7 8 거리 1 0 2 4 3 7 4 ∞ 5 ∞ 6 ∞ 7 ∞ 8 ∞
▲ [그림 1] 시작점 1의 이웃에 거리 기록

현재 거리가 가장 작은 22를 선택합니다. 2→32\to3을 거치면 비용은 4+2=64+2=6이므로 33의 거리를 77에서 66으로 바꿉니다. 44까지는 4+5=94+5=9를 기록합니다.

다음에는 거리 66인 33을 선택합니다. 3→43\to4를 거치면 6+1=76+1=7이므로 44의 거리를 다시 99에서 77로 줄입니다. 66까지는 6+6=126+6=12를 기록합니다.

4 7 2 5 1 6 3 8 4 2 5 3 1 2 3 4 5 6 7 8 거리 1 0 2 4 3 6 4 7 5 ∞ 6 12 7 ∞ 8 ∞
▲ [그림 2] 2와 3을 거치며 더 짧은 경로로 갱신

이어서 4,5,6,7,84,5,6,7,8 순서로 거리를 확정합니다. 44에서 66으로 가는 비용은 7+8=157+8=15이므로 이미 기록한 1212를 바꾸지 않습니다. 66을 처리할 때는 88까지 12+5=1712+5=17을 기록합니다. 이처럼 각 간선을 지나는 비용과 기존 거리를 비교하여 더 작은 값을 유지합니다.

4 7 2 5 1 6 3 8 4 2 5 3 1 2 3 4 5 6 7 8 거리 1 0 2 4 3 6 4 7 5 10 6 12 7 14 8 17
▲ [그림 3] 시작점 1에서 각 정점까지의 최단 거리

가장 가까운 정점부터 선택하는 이유

아직 거리를 확정하지 않은 정점 중 현재 거리가 가장 작은 정점을 선택합니다. 선택한 정점까지의 거리가 33이고, 나머지 후보의 거리가 모두 33 이상이라면 나머지 후보를 거쳐 돌아오는 경로도 비용이 33 이상입니다. 간선 비용이 00 이상이므로 이동을 추가한다고 비용이 줄어들지 않기 때문입니다. 따라서 선택한 정점의 거리를 확정할 수 있습니다.

음수 간선이 있으면 이 근거가 성립하지 않습니다. S→AS\to A가 22, S→BS\to B가 55, B→AB\to A가 −4-4이면 처음에는 AA가 더 가까워 보이지만, BB를 거치는 비용은 5−4=15-4=1입니다. 이런 그래프에서는 벨만–포드를 사용해 음수 간선을 처리할 수 있습니다.

우선순위 큐와 경로 복원

매번 모든 정점을 살펴보며 가장 가까운 후보를 찾는 대신, 거리가 작은 정점을 먼저 꺼내는 우선순위 큐에 거리와 정점을 함께 넣습니다. 더 짧은 경로를 찾으면 새 후보를 추가하므로 같은 정점의 후보가 여러 개 생길 수 있습니다. 현재 기록보다 큰 거리의 후보는 이미 더 짧은 경로로 대체된 것이므로 건너뜁니다.

실제 경로도 필요하다면 거리가 줄어들 때 직전 정점을 함께 기록합니다. 위 그림에서 33의 직전 정점은 11에서 22로 바뀝니다. 도착점 88에서 직전 정점을 따라가면 8,6,3,2,18,6,3,2,1을 얻고, 이를 뒤집으면 최단 경로 1→2→3→6→81\to2\to3\to6\to8이 됩니다.

BFS와 벨만–포드에서도 정점을 발견하거나 거리를 갱신할 때 직전 정점을 저장하여 경로를 복원할 수 있습니다. 플로이드–워셜에서는 정점 쌍마다 다음에 갈 정점이나 경유 정점을 저장합니다.

정점과 간선이 많아 모든 간선을 여러 차례 확인하기 어려운 문제에서도 우선순위 큐를 사용하는 다익스트라를 고려할 수 있습니다. 적용 전에 가중치가 모두 00 이상인지 확인합니다.

복잡도

정점 수를 VV, 간선 수를 EE라고 합니다. 힙에 같은 정점의 후보가 여러 번 들어가는 구현을 기준으로 합니다.

연산시간복잡도공간복잡도
최단 거리 계산O((V+E)log⁡(V+E))O((V+E)\log(V+E))그래프·힙·거리 O(V+E)O(V+E)
경로 복원O(V)O(V)경로 저장 O(V)O(V)

예제

문제

방향 그래프에서 시작점부터 도착점까지의 최단 거리와 경로를 구하여라.

입력

첫째 줄에 정점 수 NN, 간선 수 MM, 시작점 SS, 도착점 TT가 공백을 사이에 두고 주어진다. 1≤N≤1001\le N\le100; 0≤M≤3000\le M\le300; 1≤S,T≤N1\le S,T\le N이다.

다음 MM개 줄에 간선의 시작점 aa, 끝점 bb, 가중치 ww가 공백을 사이에 두고 주어진다. 1≤a,b≤N1\le a,b\le N; 1≤w≤1001\le w\le100이다. 도착점에 도달할 수 있다면 최단 경로는 하나이다.

출력

도착점에 도달할 수 있으면 첫째 줄에 최단 거리를 출력한다. 둘째 줄에 시작점부터 도착점까지 경로의 정점 번호를 순서대로 공백을 사이에 두고 출력한다. 도달할 수 없으면 첫째 줄에 -1을 출력한다.

예제 입력

4 5 1 4
1 2 2
1 3 8
2 3 1
3 4 3
2 4 9

예제 출력

6
1 2 3 4

예제 코드

C++20

#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <utility>
#include <functional>

using namespace std;

vector<vector<pair<int, int>>> g;
vector<int> d, p, ans;

int main() {
    const int INF = 1000000;
    int n, m, s, t, i, a, b, w, u, du;
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;

    cin >> n >> m >> s >> t;
    g.resize(n + 1);
    d.assign(n + 1, INF);
    p.assign(n + 1, 0);
    for (i = 0; i < m; ++i) {
        cin >> a >> b >> w;
        g[a].push_back({b, w});
    }
    d[s] = 0;
    pq.push({0, s});

    while (!pq.empty()) {
        du = pq.top().first;
        u = pq.top().second;
        pq.pop();
        // 더 짧은 경로로 이미 갱신된 정점의 오래된 큐 항목 제외
        if (du != d[u]) continue;
        for (const auto& [v, cost] : g[u]) {
            // u를 거쳐 더 짧아지면 거리와 경로 복원용 이전 정점 갱신
            if (du + cost < d[v]) {
                d[v] = du + cost;
                p[v] = u;
                pq.push({d[v], v});
            }
        }
    }

    if (d[t] == INF) cout << -1 << '\n';
    else {
        // 도착점에서 이전 정점을 따라가며 경로를 역순으로 수집
        for (u = t; u != 0; u = p[u]) {
            ans.push_back(u);
        }
        reverse(ans.begin(), ans.end());
        cout << d[t] << '\n';
        for (int v : ans) {
            cout << v << ' ';
        }
        cout << '\n';
    }
}