[Daily morning study] 플로이드-워셜(Floyd-Warshall) 알고리즘 — 모든 쌍 최단 경로

#daily morning study

Image


개요

Floyd-Warshall은 그래프에서 모든 쌍(all-pairs)의 최단 경로를 한 번에 구하는 알고리즘이다.

Dijkstra나 Bellman-Ford가 단일 출발점(single-source)에서 각 정점까지의 최단 거리를 구하는 것과 달리, Floyd-Warshall은 임의의 두 정점 (i, j) 사이의 최단 거리를 모두 계산한다.

핵심 아이디어

“k번 정점을 중간 경유지로 허용했을 때, i에서 j까지의 최단 경로는 얼마인가?”

경유지 k를 1번부터 N번까지 하나씩 늘려가면서 점화식을 반복 적용한다.

점화식:

dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j])
  • dp[i][j]: 현재까지 허용된 경유지를 사용했을 때 i → j의 최단 거리
  • k: 이번에 추가로 허용된 경유지 번호

k를 1부터 N까지 순서대로 고려하면, 최종적으로 모든 경유지를 허용한 결과(즉, 진짜 최단 경로)가 dp에 저장된다.

구현

INF = float('inf')

def floyd_warshall(N, edges):
    # 초기화
    dp = [[INF] * (N + 1) for _ in range(N + 1)]
    for i in range(1, N + 1):
        dp[i][i] = 0
    for u, v, w in edges:
        dp[u][v] = w  # 방향 그래프

    # 삼중 루프: 경유지 k를 하나씩 추가
    for k in range(1, N + 1):
        for i in range(1, N + 1):
            for j in range(1, N + 1):
                if dp[i][k] + dp[k][j] < dp[i][j]:
                    dp[i][j] = dp[i][k] + dp[k][j]

    return dp

루프 순서가 중요하다. 가장 바깥 루프가 k (경유지) 여야 한다. i, j 루프를 바깥으로 빼면 올바른 결과를 얻을 수 없다.

시간/공간 복잡도

항목복잡도
시간O(V³)
공간O(V²)

V는 정점 수. 삼중 루프이므로 자연스럽게 O(V³). V가 500 이하일 때 실용적이다 (V=1000이면 약 10억 연산).

다른 최단 경로 알고리즘과 비교

항목DijkstraBellman-FordFloyd-Warshall
목적단일 출발점단일 출발점모든 쌍
시간복잡도O((V+E) log V)O(VE)O(V³)
음수 간선불가가능가능
음수 사이클 탐지불가가능가능
구현 난이도중하하

Floyd-Warshall은 음수 가중치 간선이 있어도 동작한다. 단, 음수 사이클이 존재하면 최단 경로가 정의되지 않으므로 탐지가 필요하다.

음수 사이클 탐지

Floyd-Warshall 수행 후 dp[i][i] < 0인 정점이 있으면 음수 사이클이 그래프에 존재한다는 뜻이다.

for i in range(1, N + 1):
    if dp[i][i] < 0:
        print(f"음수 사이클 존재: 정점 {i} 포함")

원래 dp[i][i] = 0으로 초기화했는데, 음수 사이클이 있으면 i → … → i 경로가 음수 합이 되어 dp[i][i]가 0보다 작아진다.

경로 복원 (Path Reconstruction)

최단 거리뿐만 아니라 실제 경로도 추적하려면 next[i][j] 배열을 관리한다.

def floyd_warshall_with_path(N, edges):
    INF = float('inf')
    dp = [[INF] * (N + 1) for _ in range(N + 1)]
    nxt = [[None] * (N + 1) for _ in range(N + 1)]

    for i in range(1, N + 1):
        dp[i][i] = 0
        nxt[i][i] = i
    for u, v, w in edges:
        if w < dp[u][v]:
            dp[u][v] = w
            nxt[u][v] = v

    for k in range(1, N + 1):
        for i in range(1, N + 1):
            for j in range(1, N + 1):
                if dp[i][k] + dp[k][j] < dp[i][j]:
                    dp[i][j] = dp[i][k] + dp[k][j]
                    nxt[i][j] = nxt[i][k]  # i→j는 i→k 경로를 먼저 따라감

    return dp, nxt

def get_path(nxt, i, j, dp):
    INF = float('inf')
    if dp[i][j] == INF:
        return []
    path = [i]
    while i != j:
        i = nxt[i][j]
        path.append(i)
    return path

경유지 k를 거쳐 i→j를 지나갈 때, nxt[i][j] = nxt[i][k]로 업데이트한다. i에서 k까지 가는 첫 번째 다음 정점이 i에서 j까지 가는 첫 번째 다음 정점이 되기 때문이다.

이행적 폐쇄 (Transitive Closure)

최단 거리가 필요 없고 도달 가능 여부만 파악할 때는 boolean 배열로 간소화할 수 있다.

def transitive_closure(N, edges):
    reach = [[False] * (N + 1) for _ in range(N + 1)]

    for i in range(1, N + 1):
        reach[i][i] = True
    for u, v in edges:
        reach[u][v] = True

    for k in range(1, N + 1):
        for i in range(1, N + 1):
            for j in range(1, N + 1):
                reach[i][j] = reach[i][j] or (reach[i][k] and reach[k][j])

    return reach

활용 사례

  1. 도시 간 최단 거리 테이블 — V개 도시 간의 모든 최단 경로를 미리 계산해 쿼리 응답
  2. 네트워크 라우팅 — 라우터 간 최소 비용 경로를 한 번에 계산
  3. 최대 용량 경로 (Widest Path) — min 대신 max를 사용해 병목 대역폭 최대화
  4. Minimax Path — i → j 경로 중 최대 간선 가중치를 최소화하는 경로
  5. 소셜 네트워크 도달성 — 이행적 폐쇄를 통해 영향력 범위 계산

구현 시 주의사항

INF 오버플로 문제 (C++ 등에서)

Python은 임의 정밀도 정수라 float('inf')를 쓰면 안전하지만, C++에서는 INF = 1e9처럼 큰 값을 쓸 때 INF + INF가 오버플로될 수 있다.

// C++ 예시: 오버플로 방지
const long long INF = 1e18;
if (dp[i][k] != INF && dp[k][j] != INF)
    dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);

루프 순서 고정

경유지 k가 가장 바깥 루프여야 한다. i, j 순서는 바꿔도 무방하다.

자기 자신 초기화

dp[i][i] = 0 초기화를 빠뜨리면 음수 사이클 탐지뿐만 아니라 경로 계산 전체가 틀어진다.

정리

Floyd-Warshall 핵심 요약:
  목적  : 모든 쌍 최단 경로
  복잡도: O(V³) 시간, O(V²) 공간
  점화식: dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j])
  음수  : 간선 허용, 사이클 불가 (dp[i][i] < 0 → 사이클 존재)
  경유지 루프(k)가 반드시 가장 바깥에 위치해야 함