개념

모든 정점 쌍 간의 최단 경로를 구하는 알고리즘

동적 프로그래밍 방식으로 중간 정점을 거쳐가며 최단 경로 갱신


시간복잡도

구현 방식시간복잡도
기본 구현O(V³)
공간복잡도O(V²)

특징

장점

  • 모든 쌍 최단 경로: 한 번에 모든 정점 간 최단 경로
  • 음수 가중치 가능: 음수 간선 처리 가능
  • 구현 간단: 3중 반복문으로 구현
  • 음수 사이클 탐지: 대각선 원소로 확인

단점

  • 느린 속도: O(V³) - 정점 많으면 매우 느림
  • 메모리 사용: O(V²) 2차원 배열 필요

적용 조건

  • 정점 수가 적음: V ≤ 500 정도
  • 모든 쌍 필요: 모든 정점 간 거리 필요
  • 음수 가중치 가능

구현 방법

기본 구현

def floyd_warshall(n, edges):
    """플로이드-워셜 알고리즘"""
    # 거리 배열 초기화
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]
 
    # 자기 자신으로의 거리는 0
    for i in range(n):
        dist[i][i] = 0
 
    # 간선 정보 입력
    for u, v, weight in edges:
        dist[u][v] = weight
        # 무방향 그래프면 양방향 추가
        # dist[v][u] = weight
 
    # 플로이드-워셜 알고리즘
    for k in range(n):  # 중간 정점
        for i in range(n):  # 출발 정점
            for j in range(n):  # 도착 정점
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
 
    return dist
 
# 테스트
edges = [
    (0, 1, 4),
    (0, 2, 3),
    (1, 2, -2),
    (1, 3, 2),
    (2, 3, 3)
]
 
dist = floyd_warshall(4, edges)
for row in dist:
    print(row)
 
# 출력:
# [0, 4, 2, 5]
# [inf, 0, -2, 1]
# [inf, inf, 0, 3]
# [inf, inf, inf, 0]
 
# 시간복잡도: O(V³)
# 공간복잡도: O(V²)

경로 복원 구현

def floyd_warshall_with_path(n, edges):
    """경로 추적"""
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]
    next_node = [[-1] * n for _ in range(n)]
 
    # 초기화
    for i in range(n):
        dist[i][i] = 0
 
    for u, v, weight in edges:
        dist[u][v] = weight
        next_node[u][v] = v
 
    # 플로이드-워셜
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
                    next_node[i][j] = next_node[i][k]
 
    return dist, next_node
 
def reconstruct_path(next_node, start, end):
    """경로 복원"""
    if next_node[start][end] == -1:
        return []
 
    path = [start]
    current = start
 
    while current != end:
        current = next_node[current][end]
        path.append(current)
 
    return path
 
# 테스트
edges = [
    (0, 1, 4),
    (0, 2, 3),
    (1, 2, -2),
    (1, 3, 2),
    (2, 3, 3)
]
 
dist, next_node = floyd_warshall_with_path(4, edges)
path = reconstruct_path(next_node, 0, 3)
print(f"0→3 최단 거리: {dist[0][3]}")  # 5
print(f"0→3 경로: {path}")              # [0, 1, 2, 3]

음수 사이클 탐지

def detect_negative_cycle_floyd(n, edges):
    """음수 사이클 탐지"""
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]
 
    for i in range(n):
        dist[i][i] = 0
 
    for u, v, weight in edges:
        dist[u][v] = weight
 
    # 플로이드-워셜
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if dist[i][k] != INF and dist[k][j] != INF:
                    if dist[i][k] + dist[k][j] < dist[i][j]:
                        dist[i][j] = dist[i][k] + dist[k][j]
 
    # 음수 사이클 확인 (대각선 원소가 음수면 사이클)
    for i in range(n):
        if dist[i][i] < 0:
            return True
 
    return False
 
# 테스트
# 음수 사이클 있는 경우
edges = [
    (0, 1, 1),
    (1, 2, -3),
    (2, 0, 1)
]
print(detect_negative_cycle_floyd(3, edges))  # True

동작 과정

그래프:
    0 --4--> 1
    |        |
    3       -2
    ↓        ↓
    2 --3--> 3

초기 거리 행렬:
    0   1   2   3
0 [ 0   4   3  INF]
1 [INF  0  -2   2 ]
2 [INF INF  0   3 ]
3 [INF INF INF  0 ]

k=0 (0번 정점 경유):
  1→0→2: INF (도달 불가)
  2→0→1: INF (도달 불가)
  ... (변화 없음)

k=1 (1번 정점 경유):
  0→1→2: 4+(-2) = 2 < 3 ✓
  0→1→3: 4+2 = 6 < INF ✓
    0   1   2   3
0 [ 0   4   2   6 ]
1 [INF  0  -2   2 ]
2 [INF INF  0   3 ]
3 [INF INF INF  0 ]

k=2 (2번 정점 경유):
  0→2→3: 2+3 = 5 < 6 ✓
  1→2→3: -2+3 = 1 < 2 ✓
    0   1   2   3
0 [ 0   4   2   5 ]
1 [INF  0  -2   1 ]
2 [INF INF  0   3 ]
3 [INF INF INF  0 ]

k=3 (3번 정점 경유):
  변화 없음

최종 거리 행렬:
    0   1   2   3
0 [ 0   4   2   5 ]
1 [INF  0  -2   1 ]
2 [INF INF  0   3 ]
3 [INF INF INF  0 ]

템플릿

def floyd_warshall_template(n, edges):
    # 1. 초기화
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]
 
    for i in range(n):
        dist[i][i] = 0
 
    for u, v, weight in edges:
        dist[u][v] = weight
 
    # 2. 3중 반복 (중간 정점 k 경유)
    for k in range(n):
        for i in range(n):
            for j in range(n):
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
 
    return dist

핵심 요약

개념설명
원리DP - 중간 정점 경유 최소화
시간복잡도O(V³)
공간복잡도O(V²)
적용 조건모든 쌍 최단 경로
특징음수 가중치 가능

다른 알고리즘과 비교

알고리즘시간복잡도용도음수 가중치
다익스트라O((V+E) log V)단일 출발점불가
벨만-포드O(VE)단일 출발점가능
플로이드-워셜O(V³)모든 쌍가능
다익스트라 V번O(V(V+E) log V)모든 쌍불가

주의사항

1. 반복문 순서

# 올바른 순서: k → i → j
for k in range(n):      # 중간 정점
    for i in range(n):  # 출발 정점
        for j in range(n):  # 도착 정점
            dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
 
# 잘못된 순서
for i in range(n):
    for j in range(n):
        for k in range(n):  # 잘못됨!

2. INF 처리

# INF + INF 오버플로우 방지
if dist[i][k] != INF and dist[k][j] != INF:
    if dist[i][k] + dist[k][j] < dist[i][j]:
        dist[i][j] = dist[i][k] + dist[k][j]

3. 정점 수 제한

# O(V³)이므로 V가 크면 시간 초과
# V ≤ 500 정도가 적당
# V > 500이면 다익스트라 여러 번 실행 고려

자주 나오는 문제 유형

1. 모든 쌍 최단 경로

문제: 모든 정점 간 최단 거리

def all_pairs_shortest_path(n, edges):
    """모든 쌍 최단 경로"""
    dist = floyd_warshall(n, edges)
 
    # 결과 출력
    for i in range(n):
        for j in range(n):
            if dist[i][j] == float('inf'):
                print("INF", end=" ")
            else:
                print(dist[i][j], end=" ")
        print()
 
    return dist
 
# 테스트
edges = [
    (0, 1, 4),
    (0, 2, 3),
    (1, 2, -2),
    (1, 3, 2),
    (2, 3, 3)
]
 
all_pairs_shortest_path(4, edges)

2. 경유지를 반드시 거치는 최단 경로

문제: A → B → C 최단 경로

def shortest_path_via_nodes(dist, start, via_nodes, end):
    """특정 노드들을 반드시 거치는 최단 경로"""
    # start → via[0] → via[1] → ... → end
    total_dist = 0
    current = start
 
    for via in via_nodes:
        if dist[current][via] == float('inf'):
            return float('inf')
        total_dist += dist[current][via]
        current = via
 
    if dist[current][end] == float('inf'):
        return float('inf')
 
    total_dist += dist[current][end]
    return total_dist
 
# 테스트
edges = [
    (0, 1, 2), (1, 2, 3), (2, 3, 1),
    (0, 2, 10), (1, 3, 5)
]
 
dist = floyd_warshall(4, edges)
# 0 → 1 → 2 → 3 경로
print(shortest_path_via_nodes(dist, 0, [1, 2], 3))  # 6

3. 사이클의 최소 길이

문제: 최소 길이 사이클 찾기

def shortest_cycle(n, edges):
    """최소 길이 사이클"""
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]
 
    for u, v, weight in edges:
        dist[u][v] = weight
 
    min_cycle = INF
 
    # k를 거치지 않는 최단 경로로 사이클 확인
    for k in range(n):
        # k를 추가하기 전에 사이클 확인
        for i in range(n):
            for j in range(n):
                if i != j and dist[i][j] != INF and dist[j][i] != INF:
                    min_cycle = min(min_cycle, dist[i][j] + dist[j][i])
 
        # k를 중간 정점으로 추가
        for i in range(n):
            for j in range(n):
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
 
    return min_cycle if min_cycle != INF else -1
 
# 테스트
edges = [
    (0, 1, 1),
    (1, 2, 2),
    (2, 0, 3),
    (1, 3, 4),
    (3, 1, 5)
]
 
print(shortest_cycle(4, edges))  # 6 (0→1→2→0)

4. 친구 관계의 거리

문제: 모든 사람 간의 친구 관계 단계

def friend_distance(n, friendships):
    """친구 관계 거리"""
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]
 
    for i in range(n):
        dist[i][i] = 0
 
    # 친구 관계는 거리 1
    for u, v in friendships:
        dist[u][v] = 1
        dist[v][u] = 1
 
    # 플로이드-워셜
    for k in range(n):
        for i in range(n):
            for j in range(n):
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
 
    return dist
 
# 테스트
friendships = [(0, 1), (1, 2), (2, 3), (0, 3)]
dist = friend_distance(4, friendships)
 
# 0과 2의 친구 단계
print(f"0과 2의 거리: {dist[0][2]}")  # 2 (0→1→2 or 0→3→2)

5. 회사 위치 선정 (케빈 베이컨)

문제: 모든 정점까지의 거리 합이 최소인 정점

def find_center_node(n, edges):
    """중심 정점 찾기 (케빈 베이컨)"""
    dist = floyd_warshall(n, edges)
 
    min_sum = float('inf')
    center = -1
 
    for i in range(n):
        total = sum(dist[i])
        if total < min_sum:
            min_sum = total
            center = i
 
    return center, min_sum
 
# 테스트
edges = [
    (0, 1, 1), (0, 2, 1),
    (1, 3, 1), (2, 3, 1),
    (3, 4, 1)
]
 
center, min_sum = find_center_node(5, edges)
print(f"중심 정점: {center}, 거리 합: {min_sum}")

6. 역방향 최단 경로

문제: 간선 방향을 모두 반대로 했을 때 최단 경로

def reverse_shortest_path(n, edges):
    """역방향 최단 경로"""
    # 정방향
    forward_edges = edges
    forward_dist = floyd_warshall(n, forward_edges)
 
    # 역방향 간선 생성
    reverse_edges = [(v, u, w) for u, v, w in edges]
    reverse_dist = floyd_warshall(n, reverse_edges)
 
    return forward_dist, reverse_dist
 
# 왕복 최단 거리
def round_trip_distance(forward_dist, reverse_dist, start, end):
    """왕복 거리"""
    go = forward_dist[start][end]
    back = reverse_dist[start][end]
    return go + back if go != float('inf') and back != float('inf') else float('inf')

추천 연습 문제

기초

중급

고급


최적화 기법

1. 메모리 절약 (경로 불필요 시)

def floyd_warshall_memory_optimized(n, edges):
    """메모리 최적화 - 경로 추적 없음"""
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]
 
    for i in range(n):
        dist[i][i] = 0
 
    for u, v, w in edges:
        dist[u][v] = min(dist[u][v], w)  # 중복 간선 처리
 
    for k in range(n):
        for i in range(n):
            for j in range(n):
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
 
    return dist

2. 비트마스크 최적화 (정점 제한)

def floyd_warshall_bitmask(n, edges, allowed_nodes):
    """특정 정점만 경유 가능"""
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]
 
    for i in range(n):
        dist[i][i] = 0
 
    for u, v, w in edges:
        dist[u][v] = w
 
    # allowed_nodes만 경유
    for k in allowed_nodes:
        for i in range(n):
            for j in range(n):
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
 
    return dist

언제 사용할까?

사용 가능

  • 모든 쌍 필요: 모든 정점 간 최단 거리
  • 정점 수 적음: V ≤ 500
  • 음수 가중치: 음수 간선 있음
  • 간단한 구현: 빠르게 구현 필요

사용 불가

  • 정점 많음: V > 500 (시간 초과)
  • 단일 출발점: 다익스트라가 빠름
  • 희소 그래프: 다익스트라 V번이 빠름

결론: 작은 그래프에서 모든 쌍 최단 경로가 필요할 때 최적!