Shortest Path Algorithms (최단 경로 알고리즘)

"가중치 그래프에서 출발점이 하나인지 모든 정점 쌍인지, 음수 간선이나 음수 사이클이 있는지에 따라 Dijkstra, Bellman-Ford, Floyd-Warshall 중 무엇을 고르고, 각각은 왜 그 조건에서만 맞는가?" 이 페이지는 그 질문에 답한다. 세 알고리즘은 MST 노트가 그래프의 최소 비용 문제로 드는 "두 정점 사이의 최소 비용 경로 찾기"에 속한다고 볼 수 있다. 이 연결은 이 Wiki의 해석이다. 원문은 이 문제를 모든 정점을 최소 비용으로 잇는 트리 문제, 즉 Graph Algorithms의 MST와 나란히 따로 든다.[1] 원문 노트 세 개는 각자의 논리적 전제를 적고, Bellman-Ford 노트와 Floyd-Warshall 노트는 다른 알고리즘과의 비교표를 갖고 있으며, 이 페이지는 그것을 하나의 선택 기준으로 묶는다.

선택 표

알고리즘 출발점 음수 간선 음수 사이클 탐지 시간 복잡도 방식
Dijkstra 단일 불가 불가 O(E log V) 그리디 + 우선순위 큐
Bellman-Ford 단일 가능 가능 O(VE) DP 방식 반복
Floyd-Warshall 모든 쌍 가능 가능 (dist[i][i] < 0) O(V³) DP
flowchart TD
    Q1{모든 정점 쌍의 거리가 필요한가} -->|예| FW[Floyd-Warshall]
    Q1 -->|아니오: 출발점 하나| Q2{음수 간선이 있거나 음수 사이클을 확인해야 하는가}
    Q2 -->|예| BF[Bellman-Ford]
    Q2 -->|아니오| DJ[Dijkstra]

공통 원리: 거리 갱신 (Relaxation)

Dijkstra

int[] dist = new int[V + 1];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;

// {거리, 노드} 순서로 최소 거리 기준 정렬
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
pq.offer(new int[]{0, start});

while (!pq.isEmpty()) {
    int[] cur = pq.poll();
    int cost = cur[0], node = cur[1];

    // 이미 더 짧은 경로로 처리된 노드 → 스킵 (중복 삽입 처리)
    if (cost > dist[node]) continue;

    for (int[] next : graph[node]) {
        int nNode = next[0], nCost = next[1];
        if (dist[node] + nCost < dist[nNode]) {
            dist[nNode] = dist[node] + nCost;
            pq.offer(new int[]{dist[nNode], nNode});
        }
    }
}
구현 방식 시간 복잡도
우선순위 큐 (Min-Heap) O(E log V)
선형 탐색 O(V²)

Bellman-Ford

int[] dist = new int[V + 1];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;

// V-1번 반복: 모든 간선 완화
for (int i = 1; i < V; i++) {
    for (int[] edge : edges) { // edge = {from, to, weight}
        int from = edge[0], to = edge[1], w = edge[2];
        if (dist[from] != Integer.MAX_VALUE && dist[from] + w < dist[to]) {
            dist[to] = dist[from] + w;
        }
    }
}

// V번째 반복: 음수 사이클 탐지
boolean hasNegativeCycle = false;
for (int[] edge : edges) {
    int from = edge[0], to = edge[1], w = edge[2];
    if (dist[from] != Integer.MAX_VALUE && dist[from] + w < dist[to]) {
        hasNegativeCycle = true;
        break;
    }
}

Floyd-Warshall

final int INF = Integer.MAX_VALUE / 2; // 오버플로 방지
int[][] dist = new int[V + 1][V + 1];

// 초기화
for (int i = 1; i <= V; i++)
    Arrays.fill(dist[i], INF);
for (int i = 1; i <= V; i++)
    dist[i][i] = 0;

// 간선 입력
for (int[] edge : edges)
    dist[edge[0]][edge[1]] = edge[2]; // 단방향

// 플로이드-워셜
for (int k = 1; k <= V; k++)
    for (int i = 1; i <= V; i++)
        for (int j = 1; j <= V; j++)
            if (dist[i][k] != INF && dist[k][j] != INF)
                dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);

경로 복원

int[][] next = new int[V + 1][V + 1]; // next[i][j]: i→j 경로의 i 다음 노드

// 초기화: 간선이 있으면 next[i][j] = j
for (int k = 1; k <= V; k++)
    for (int i = 1; i <= V; i++)
        for (int j = 1; j <= V; j++)
            if (dist[i][k] + dist[k][j] < dist[i][j]) {
                dist[i][j] = dist[i][k] + dist[k][j];
                next[i][j] = next[i][k]; // k를 경유하므로 i의 다음은 i→k의 다음
            }

// 경로 출력
List<Integer> getPath(int s, int t) {
    List<Integer> path = new ArrayList<>();
    path.add(s);
    while (s != t) {
        s = next[s][t];
        path.add(s);
    }
    return path;
}

음수 사이클과 복잡도

항목 값
시간 복잡도 O(V³)
공간 복잡도 O(V²)
적합 정점 수 V ≤ 500 수준

관련

테스트 질문

출처


  1. MST.md 8-10행 — "그래프에서 최소 비용 문제", "모든 정점을 연결하는 간선들의 가중치의 합이 최소가 되는 트리", "두 정점 사이의 최소 비용의 경로 찾기" ↩︎

  2. Bellman-Ford.md 59-66행 다익스트라 비교표 — "| 음수 간선 | 불가 | 가능 |", "| 음수 사이클 탐지 | 불가 | 가능 |", "| 구현 방식 | 그리디 + 우선순위 큐 | DP 방식 반복 |". 65행 시간 복잡도 칸은 | 시간 복잡도 | (E \log V)$ | (VE)$ |로 $O가 사라져 있다. ↩︎ ↩︎

  3. Floyd-Warshall.md 101-107행 비교표 — "| 알고리즘 | 출발점 | 음수 간선 | 시간 복잡도 |"와 Dijkstra(단일, 불가), Bellman-Ford(단일, 가능), Floyd-Warshall("전체 쌍", 가능) 행. 시간 복잡도 칸은 105-107행에서 (E \log V)$, (VE)$, (V^3)$로 손상되어 있다. ↩︎

  4. Dijkstra.md 64-67행 — "음수 가중치가 존재하면 그리디 전제가 깨짐", "음수 사이클 탐지 불가" ↩︎ ↩︎ ↩︎

  5. Floyd-Warshall.md 88-91행 — "음수 가중치 간선은 허용", "음수 사이클 판별: 알고리즘 수행 후", "인 정점이 존재하면 음수 사이클 있음". 91행 조건식은 [i][i] < 0$로 $dist가 사라져 있어, 이 페이지는 같은 노트의 dist 배열 이름으로 dist[i][i] < 0이라고 복원했다. ↩︎ ↩︎

  6. _Algorithm.md 20-22행 — "단일 출발, 양수 가중치, O(Elog⁡V)", "단일 출발, 음수 간선·사이클 탐지, O(VE)", "모든 쌍, 양수/음수 간선, O(V3)". hub 노트는 같은 vault의 같은 폴더에 있고 수식이 깨지지 않았다. ↩︎

  7. Bellman-Ford.md 70-72행 — "다익스트라보다 느리므로, 음수 간선이 없을 때는 다익스트라를 선택." 70행의 복잡도는 1917O(VE)1917로 남아 있는데, 수식 블록 기호 $$가 숫자로 바뀐 손상으로 보이며 식 O(VE) 자체는 남아 있다. ↩︎ ↩︎

  8. Dijkstra.md 11행 — "출발점으로부터 다른 모든 정점까지의 최단 경로를 구하는", "그리디(Greedy)" ↩︎ ↩︎

  9. Floyd-Warshall.md 11행 — "사이의 최단 경로를 구하는 DP 알고리즘." 같은 줄 앞부분은 "모든 정점 쌍 $ 사이"로 (i, j) 표기가 사라져 있다. _Algorithm.md 22행 — "모든 쌍, 양수/음수 간선". ↩︎ ↩︎

  10. Dijkstra.md 57-62행 — "인접 노드로의 새 경로 비용이 기존 최단 거리보다 짧을 때만 갱신.", "이를 반복함으로써 출발점에서 각 정점까지의 최단 거리가 수렴." 59행 식은 > [u] + w(u, v) < dist[v]$ 이면 [v]$를 갱신으로 앞의 $dist가 사라져 있다. 같은 노트 46행 코드 "if (dist[node] + nCost < dist[nNode]) {"와 47행 갱신 코드로 dist[u] + w(u, v) < dist[v]이면 dist[v]를 갱신한다고 복원했다. ↩︎ ↩︎

  11. Bellman-Ford.md 28-52행 Java 코드 — "if (dist[from] != Integer.MAX_VALUE && dist[from] + w < dist[to]) {", "dist[to] = dist[from] + w;" ↩︎ ↩︎

  12. Floyd-Warshall.md 27-58행 — "2차원 배열 초기화", "모든 노드 k를 순서대로 경유 노드로 설정", 코드 "final int INF = Integer.MAX_VALUE / 2; // 오버플로 방지", "dist[i][i] = 0;", "dist[edge[0]][edge[1]] = edge[2]; // 단방향", "if (dist[i][k] != INF && dist[k][j] != INF)", "dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);". 30-32·34행 초기화 설명은 [i][i] = 0$, [i][j] = weight$, [i][j] = \infty$, "모든 $ 쌍에 대해 점화식 적용"으로 손상되어 있어 코드 기준으로 복원했다. ↩︎ ↩︎ ↩︎ ↩︎

  13. Dijkstra.md 15-17행 — "지금까지 확보한 최단 경로 중 가장 짧은 경로를 가진 노드는 더 이상 거리가 짧아질 수 없다.", "가중치가 모두 양수이기 때문에 한 번 확정된 최단 거리는 이후 갱신되지 않음 → 그리디 선택이 유효." ↩︎ ↩︎

  14. Dijkstra.md 21-24행 — "출발 노드의 거리 = 0", "최단 거리가 가장 짧은 노드 선택", "(Min-Heap / PriorityQueue)", "기존 거리보다 짧으면", "모든 노드를 방문하거나 큐가 빌 때까지 반복" ↩︎

  15. Dijkstra.md 28-55행 — 코드 "// {거리, 노드} 순서로 최소 거리 기준 정렬", "// 이미 더 짧은 경로로 처리된 노드 → 스킵 (중복 삽입 처리)", "if (cost > dist[node]) continue;", "int nNode = next[0], nCost = next[1];"와 설명 "같은 노드가 더 짧은 경로를 찾을 때마다 큐에 재삽입됨.", "조건으로 이미 처리된 항목을 조기에 skip." 55행은 조건 이름이 빠진 채 남아 있어, 42행 코드의 cost > dist[node]를 그 조건으로 보았다. graph가 선언되지 않았다는 점과 그 원소 구조는 이 Wiki가 코드를 읽고 덧붙였다. ↩︎ ↩︎ ↩︎

  16. Dijkstra.md 69-76행 — 표의 "우선순위 큐 (Min-Heap)", "선형 탐색"과 "실전에서는 우선순위 큐 방식 사용." 73-74행 복잡도 칸은 (E \log V)$, (V^2)$로 $O가 사라져 있다. 우선순위 큐의 O(E log V)는 _Algorithm.md 20행 "단일 출발, 양수 가중치, O(Elog⁡V)"로 확인했다. 선형 탐색의 O(V²)는 남은 (V^2)$에 사라진 $O를 붙여 복원한 값이며, 이를 확인할 다른 Raw는 없다. ↩︎ ↩︎

  17. Bellman-Ford.md 11행 — "음수 가중치를 포함하는 그래프에서 단일 출발점 최단 경로를 구하고", "음수 사이클을 탐지" ↩︎

  18. Bellman-Ford.md 15-18행 — "정점이 V개일 때, 최단 경로는 최대 V-1개의 간선으로 이루어진다.", "사이클이 없는 최단 경로에서 같은 정점을 두 번 방문할 이유가 없으므로, 간선 수는 최대 V-1.", "따라서 모든 간선을 V-1번 반복 확인하면 최단 거리가 수렴됨." ↩︎

  19. Bellman-Ford.md 20-57행 — "출발 노드 거리 = 0", "모든 간선을 확인하며 거리 갱신", "V-1번 반복", "V번째에도 갱신이 발생하면 음수 사이클 존재", "V-1번 반복 후 최단 경로가 이미 수렴되어야 함.", "경로가 수렴하지 않고 계속 줄어듦 = 음수 사이클 존재." ↩︎ ↩︎

  20. Floyd-Warshall.md 15-18행 — "중간 경유 노드 k를 거쳐갈 때 더 짧아지는가?", "만 경유 가능할 때의 최단 경로를 점진적으로 확장.", "모든 노드 k에 대해 갱신을 마치면 전체 최단 경로 완성." ↩︎

  21. Floyd-Warshall.md 22-25행 — 식 "dist[i][j] = \min(dist[i][j],\ dist[i][k] + dist[k][j])"과 "k: 현재 경유 가능한 중간 노드". 22행은 식을 감싼 $$가 2017로 바뀌어 있지만 식은 그대로이고, 57행 코드와 같다. ↩︎

  22. Floyd-Warshall.md 60-86행 — "경로 자체가 필요할 경우", "next[i][j]: i→j 경로의 i 다음 노드", "// 초기화: 간선이 있으면 next[i][j] = j", "if (dist[i][k] + dist[k][j] < dist[i][j]) {", "next[i][j] = next[i][k]; // k를 경유하므로 i의 다음은 i→k의 다음", "while (s != t) {". 본 루프의 INF 검사는 56행 "if (dist[i][k] != INF && dist[k][j] != INF)"이다. ↩︎ ↩︎

  23. Floyd-Warshall.md 93-99행 — 표의 "시간 복잡도", "공간 복잡도", "적합 정점 수" 행. 값 칸은 (V^3)$, (V^2)$, \leq 500$ 수준으로 $O와 $V가 사라져 있다. O(V³)는 _Algorithm.md 22행 "모든 쌍, 양수/음수 간선, O(V3)"로 확인했다. O(V²)와 'V ≤ 500'은 남은 표기에 사라진 기호를 붙여 문맥으로 복원한 값이며, 확인할 다른 Raw는 없다. ↩︎

  24. 구현 유형 접근법.md 19-23행 — "시간복잡도 기준 (1억 연산 ≈ 1초)", "| N ≤ 100 | O(N³) |" ↩︎