0Pricing
DSA Interview Prep · 강의

플로이드-워셜: 모든 쌍의 최단 경로

세 겹 반복문으로 구성된 플로이드-워셜 알고리즘을 사용해 모든 쌍의 거리 행렬을 채우고, 모든 노드 쌍 사이의 최소 이동 횟수를 구합니다.

플로이드-워셜: 모든 쌍의 최단 경로은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

모든 쌍 최단 경로

플로이드-워셜은 가중치가 있는 그래프에서 모든 노드 쌍 사이의 최단 경로를 계산합니다. 음수 간선 가중치가 있는 그래프도 처리할 수 있지만 음수 사이클은 처리하지 못합니다. 각 시작점에서 다익스트라를 실행하면 O(V × (V+E) log V)이지만, 플로이드-워셜은 간선 밀도와 관계없이 O(V³)에 실행됩니다. V ≤ 500인 밀집 그래프에서는 플로이드-워셜이 더 단순하면서도 속도는 비슷한 경우가 많습니다.

핵심 아이디어: 중간 노드

플로이드-워셜의 핵심 발상은 다음과 같습니다. dp[i][j][k] = 중간 노드로 {0, 1, ..., k}만 using하는 i에서 j로 가는 최단 경로입니다. 최단 경로가 노드 k를 중간 노드로 사용하는 경우와 그렇지 않은 경우로 나뉩니다. 사용하는 경우에는 dp[i][j][k] = dp[i][k][k-1] + dp[k][j][k-1]입니다. 사용하지 않는 경우에는 dp[i][j][k] = dp[i][j][k-1]입니다. 세 번째 차원은 앞으로만 진행되므로 제거할 수 있으며, 값을 제자리에서 갱신합니다.

거리 행렬 초기화

먼저 V×V 행렬을 준비합니다. dist[i][i] = 0은 자기 자신까지의 거리가 0임을 뜻하고, 직접 연결된 간선에는 dist[i][j] = weight를 사용하며, 간선이 없는 경우에는 dist[i][j] = inf로 설정합니다. 그런 다음 모든 중간 노드 k를 순회하면서 쌍 (i, j)를 갱신합니다. 허용되는 중간 노드 집합을 점진적으로 늘리면서 경로를 올바르게 구성하려면 k에 대한 바깥쪽 반복이 먼저 와야 합니다.

def floyd_warshall(V, edges):
    INF = float('inf')
    dist = [[INF]*V for _ in range(V)]
    for i in range(V):
        dist[i][i] = 0
    for u, v, w in edges:
        dist[u][v] = w  # directed graph
    
    for k in range(V):       # intermediate node
        for i in range(V):
            for j in range(V):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
    
    return dist

예제를 포함한 전체 구현

4개 노드 그래프에서 플로이드-워셜의 동작을 살펴보겠습니다. 각 중간 노드 k를 처리한 뒤 행렬에는 해당 노드를 거치는 더 짧은 경로가 채워집니다. 이 알고리즘은 최단 경로를 점진적으로 구성하므로 여러 간선을 거치는 경로도 자연스럽게 처리합니다.

def floyd_warshall(V, edges):
    INF = float('inf')
    dist = [[INF]*V for _ in range(V)]
    for i in range(V):
        dist[i][i] = 0
    for u, v, w in edges:
        dist[u][v] = w
    for k in range(V):
        for i in range(V):
            for j in range(V):
                if dist[i][k] != INF and dist[k][j] != INF:
                    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
    return dist

V = 4
edges = [(0,1,3),(0,2,7),(1,2,1),(1,3,5),(2,3,2)]
dist = floyd_warshall(V, edges)
for row in dist:
    print([x if x != float('inf') else 'INF' for x in row])

음수 사이클 감지

플로이드-워셜을 실행한 뒤 주대각선을 확인합니다. 어떤 dist[i][i] < 0이든 성립하면 노드 i를 지나는 음수 사이클이 존재합니다. 음수 사이클이 있으면 음의 비용으로 i에서 출발해 다시 i에 도달할 수 있기 때문입니다. 음수 사이클이 없다면 모든 대각선 항목은 0으로 유지됩니다.

def has_negative_cycle_fw(V, edges):
    dist = floyd_warshall(V, edges)
    for i in range(V):
        if dist[i][i] < 0:
            return True  # negative cycle through node i
    return False

# Negative cycle: 0->1->2->0 with weights 1,-3,1 (sum=-1)
edges_neg = [(0,1,1),(1,2,-3),(2,0,1)]
print(has_negative_cycle_fw(3, edges_neg))  # True

경로 복원

i에서 j까지의 실제 경로를 복원하려면 next[i][j] 행렬을 유지합니다. 처음에는 직접 연결된 간선에 대해 next[i][j] = j로 설정합니다. 중간 노드 k를 거쳐 갱신할 때는 next[i][j] = next[i][k]로 설정합니다. 경로를 복원하려면 i에서 시작해 j에 도달할 때까지 next 포인터를 따라갑니다. 이 방식은 O(V²)의 공간과 경로 하나를 복원할 때마다 O(V)의 시간을 추가로 사용합니다.

def fw_with_path(V, edges):
    INF = float('inf')
    dist = [[INF]*V for _ in range(V)]
    nxt = [[None]*V for _ in range(V)]
    for i in range(V): dist[i][i] = 0
    for u, v, w in edges:
        dist[u][v] = w; nxt[u][v] = v
    for k in range(V):
        for i in range(V):
            for j in range(V):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
                    nxt[i][j] = nxt[i][k]
    return dist, nxt

def get_path(nxt, i, j):
    if nxt[i][j] is None: return []
    path = [i]
    while i != j:
        i = nxt[i][j]; path.append(i)
    return path

전이 폐쇄

더 단순한 변형인 전이 폐쇄는 모든 노드 쌍에 대해 ‘노드 j가 노드 i에서 도달 가능한가?’를 판단합니다. 거리를 불리언 값으로 바꾸고 reach[i][j] = reach[i][j] or (reach[i][k] and reach[k][j])를 사용합니다. 이는 덧셈과 최솟값 대신 불리언 OR을 사용하는 플로이드-워셜입니다. reach[i][i] = True로 초기화하고 직접 연결된 간선에는 reach[i][j] = True로 설정합니다.

def transitive_closure(V, edges):
    reach = [[False]*V for _ in range(V)]
    for i in range(V):
        reach[i][i] = True
    for u, v, _ in edges:
        reach[u][v] = True
    for k in range(V):
        for i in range(V):
            for j in range(V):
                reach[i][j] = reach[i][j] or (reach[i][k] and reach[k][j])
    return reach

edges = [(0,1,1),(1,2,1)]
R = transitive_closure(3, edges)
print(R[0][2])  # True (0 can reach 2 via 0->1->2)

복잡도와 사용 시점

플로이드-워셜의 시간 복잡도는 O(V³), 공간 복잡도는 O(V²)입니다. V ≤ 300인 밀집 그래프(E ≈ V²)에서는 다익스트라를 V번 실행하는 것보다 빠릅니다. 이 경우 다익스트라를 V번 실행하는 시간 복잡도도 O(V³)이기 때문입니다. V=1000이고 E=3000인 희소 그래프에서는 V번의 다익스트라 실행에 O(V×E×log V) ≈ 33M이 걸리는 반면, 플로이드-워셜에는 O(V³) = 10⁹이 걸리므로 다익스트라가 더 효율적입니다. 각 알고리즘을 언제 사용해야 하는지 알아 두십시오.

모든 정점 쌍 사이의 최소 홉 수

모든 간선의 가중치를 1로 설정합니다(또는 최솟값 대신 덧셈을 사용하는 플로이드-워셜과 불리언 인접 행렬을 사용합니다): dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]). 이를 통해 모든 정점 쌍 사이의 최소 홉 수를 계산합니다. 즉, 모든 쌍에 대한 BFS 결과를 단 한 번의 O(V³) 플로이드-워셜 순회로 계산하는 것입니다.

def min_hops_all_pairs(V, adj_list):
    INF = float('inf')
    dist = [[INF]*V for _ in range(V)]
    for i in range(V):
        dist[i][i] = 0
        for j in adj_list[i]:
            dist[i][j] = 1
    for k in range(V):
        for i in range(V):
            for j in range(V):
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
    return dist

adj = [[1,2],[2],[3],[],[]]
print(min_hops_all_pairs(5, adj)[0])  # [0, 1, 1, 2, INF]

면접 맥락: 면접관이 플로이드-워셜에 관해 질문할 때

플로이드-워셜은 다음과 관련된 면접 질문에 등장합니다. (1) 작은 그래프에서 모든 정점 쌍 사이의 거리, (2) 총 가중치가 음수인 순환이 존재하는지 확인하기, (3) 제약 전파 문제에서 최단 경로 계산하기, (4) V ≤ 200인 경우 O(V³) 해법을 명시적으로 요구하는 문제입니다. 정확성을 위해 세 개의 반복문 구조와 음의 순환이 없어야 한다는 요구 사항을 항상 언급하십시오.

무방향 그래프에서의 플로이드-워셜

무방향 그래프에서는 각 간선에 대해 양방향을 모두 추가합니다: dist[u][v] = dist[v][u] = weight. 알고리즘의 나머지 부분은 동일합니다. 결과 행렬은 모든 정점 쌍에 대해 대칭입니다: dist[i][j] == dist[j][i]. 초기화할 때 방향성 간선을 실수로 할당하지 않도록 주의하십시오. 세 개의 반복문을 실행하기 전에 무방향 간선을 초기 행렬에 양방향으로 추가해야 합니다.

def fw_undirected(V, edges):
    INF = float('inf')
    dist = [[INF]*V for _ in range(V)]
    for i in range(V): dist[i][i] = 0
    for u, v, w in edges:
        dist[u][v] = w
        dist[v][u] = w  # both directions for undirected
    for k in range(V):
        for i in range(V):
            for j in range(V):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
    return dist

빠른 확인

이 수업에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 준비 개념에 대한 이해도를 확인해 보십시오.

수업 요약

이 수업에서는 다음을 배웠습니다. 플로이드-워셜은 세 개의 중첩된 반복문과 점화식 dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])을 사용하여 모든 정점 쌍 사이의 최단 경로를 계산합니다. 또한 계산이 끝난 후 dist[i][i] < 0인 항목이 있는지 확인하면 음의 순환을 탐지할 수 있습니다. 그리고 알고리즘은 O(V³) 시간과 O(V²) 공간을 사용합니다. 다음에는 네트워크 지연 시간과 경로 복원 기법을 사용한 최단 경로 응용을 다시 살펴보겠습니다.

자주 묻는 질문

“플로이드-워셜: 모든 쌍의 최단 경로” 강의는 무료인가요?

네 — “플로이드-워셜: 모든 쌍의 최단 경로” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“플로이드-워셜: 모든 쌍의 최단 경로”에서 뭘 배우나요?

세 겹 반복문으로 구성된 플로이드-워셜 알고리즘을 사용해 모든 쌍의 거리 행렬을 채우고, 모든 노드 쌍 사이의 최소 이동 횟수를 구합니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“플로이드-워셜: 모든 쌍의 최단 경로” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 우선순위 큐를 사용하는 다익스트라 알고리즘
  2. 벨만-포드와 음수 사이클
  3. 플로이드-워셜: 모든 쌍의 최단 경로
  4. 네트워크 지연 시간과 경로 복원
← DSA Interview Prep(으)로 돌아가기