🚌 플로이드-워셜(Floyd-Warshall) 알고리즘: 모든 정점 쌍 최단 경로 찾기
📚 플로이드-워셜 알고리즘이란?
플로이드-워셜(Floyd-Warshall) 알고리즘은 그래프에서 가능한 모든 정점(노드) 쌍에 대한 최단 경로를 한 번에 찾아내는 알고리즘이다.
하나의 시작점에서 다른 모든 정점까지의 최단 경로를 찾는 다익스트라 알고리즘과는 달리, 플로이드-워셜은 모든 정점에서 다른 모든 정점으로의 최단 경로를 구한다. 이 알고리즘의 가장 큰 특징 중 하나는 음수 가중치 간선이 포함된 그래프에서도 사용할 수 있다는 점이다. (단, 음수 가중치의 합이 0보다 작은 ‘음수 사이클’은 없어야 했다.)
이 알고리즘은 동적 계획법(Dynamic Programming) 에 기반을 두고 있으며, 로버트 플로이드와 스티븐 워셜에 의해 널리 알려졌다.
그 중, 플로이드 씨는 튜링상 까지 받으셧다고…
💡 주요 개념
- 모든 쌍 최단 경로 (All-Pairs Shortest Path): 그래프의 모든 정점 쌍 (u, v)에 대한 최단 경로를 찾는 문제다.
- 동적 계획법 (Dynamic Programming): 큰 문제를 작은 하위 문제로 나누어 풀고, 하위 문제의 해를 저장하여 중복 계산을 피하는 방식이다. 플로이드-워셜은 ‘k’라는 중간 정점을 거쳐가는 경우를 순차적으로 고려하며 최단 경로를 갱신해 나간다.
-
인접 행렬 (Adjacency Matrix): 그래프의 연결 관계를 2차원 배열로 나타낸 것이다.
graph[i][j]
는 정점i
에서j
로 가는 간선의 가중치를 의미한다. 직접 연결되지 않았다면 무한대(∞)로 표시한다.
🚀 플로이드-워셜 알고리즘 동작 방식 (단계별 설명)
플로이드-워셜 알고리즘은 간단한 3중 반복문 구조를 통해 모든 쌍의 최단 경로를 계산한다. 핵심 아이디어는 “i에서 j로 가는 최단 경로는, 중간에 k를 거쳐 가는 경로와 기존 경로 중 더 짧은 것이다” 이다.
-
초기화 (Initialization):
- 최단 거리를 저장할 2차원 배열
dist[i][j]
를 준비한다. -
dist[i][j]
를 두 정점i
,j
를 잇는 간선의 가중치로 초기화한다. 만약 간선이 없다면무한대(∞)
로 설정한다. - 자기 자신으로 가는 경로
dist[i][i]
는0
으로 설정한다.
- 최단 거리를 저장할 2차원 배열
-
경로 갱신 (Update Paths):
- 그래프의 모든 정점
k
에 대해,k
를 중간 경유지로 사용하는 경우를 고려한다. - 모든 정점 쌍
(i, j)
에 대해 다음을 비교하고 갱신한다.-
기존 최단 경로:
dist[i][j]
-
k
를 거쳐 가는 경로:dist[i][k] + dist[k][j]
-
기존 최단 경로:
- 만약
k
를 거쳐 가는 경로가 더 짧다면,dist[i][j]
값을dist[i][k] + dist[k][j]
로 갱신한다.dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
- 그래프의 모든 정점
-
반복 (Repeat):
- 모든 정점
k
에 대해 2번 단계를 반복하면,dist
배열에는 모든 정점 쌍 간의 최단 경로가 저장된다.
- 모든 정점
가장 중요한 점은 3중 반복문에서 중간 경유지인 k
에 대한 반복문이 가장 바깥쪽에 위치해야 한다는 점이다.
k
번 정점까지를 경유지로 고려했을 때의 최단 경로가 다음 계산에 올바르게 사용되어야 하기 때문!
⚠️ 중요한 특징 및 시간 복잡도
-
시간 복잡도:
O(V^3)
(V는 정점의 수). 3중 반복문 구조 때문에 정점의 개수가 많아지면 실행 시간이 급격히 늘어날 수 있다. - 음수 가중치 처리: 음수 가중치를 가진 간선이 있어도 잘 동작하지만, 합이 음수인 사이클(음수 사이클)이 존재하면 최단 경로를 찾을 수 없다. (계속 순환하면 비용이 무한히 작아지기 때문이다.)
- 구현의 간결함: 알고리즘의 로직이 3중 반복문으로 매우 간단하여 구현하기 쉽다.
- 동적 계획법 기반: ‘k-1’까지의 정점을 경유지로 고려한 최단 경로 정보를 바탕으로 ‘k’를 경유지로 포함하는 최단 경로를 계산해 나간다.
💻 파이썬 구현 예시
다음은 플로이드-워셜 알고리즘을 파이썬으로 구현한 예시다. 입력 graph
는 인접 행렬 형태로 주어졌다고 가정했다.
def floyd_warshall(graph):
"""
플로이드-워셜 알고리즘을 사용하여 모든 정점 쌍 간의 최단 거리를 계산한다.
Args:
graph (list of lists): 그래프를 인접 행렬 형태로 표현한 2차원 리스트.
연결되지 않은 경우 float('inf') 값을 가짐.
Returns:
list of lists: 모든 정점 쌍 간의 최단 거리가 저장된 2차원 리스트.
"""
# 정점의 개수
num_vertices = len(graph)
# 최단 거리 테이블을 그래프 자체로 초기화 (깊은 복사)
dist = [row[:] for row in graph]
# k: 경유하는 노드
for k in range(num_vertices):
# i: 출발 노드
for i in range(num_vertices):
# j: 도착 노드
for j in range(num_vertices):
# i에서 j로 가는 기존 경로보다 k를 거쳐가는 경로가 더 짧은 경우 갱신
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
return dist
# --- 예시 그래프 정의 (인접 행렬) ---
# 무한대(infinity) 값 정의
INF = float('inf')
# graph[i][j]는 i에서 j로 가는 가중치를 의미
# 자기 자신으로 가는 비용은 0
graph = [
[0, 5, INF, 10],
[INF, 0, 3, INF],
[INF, INF, 0, 1],
[INF, INF, INF, 0]
]
# --- 플로이드-워셜 알고리즘 실행 ---
shortest_paths = floyd_warshall(graph)
# --- 결과 출력 ---
print("모든 정점 쌍 간의 최단 경로:")
for row in shortest_paths:
print(row)
# 출력 결과:
# [0, 5, 8, 9]
# [inf, 0, 3, 4]
# [inf, inf, 0, 1]
# [inf, inf, inf, 0]
✅ 마무리
플로이드-워셜 알고리즘은 모든 정점 사이의 최단 경로를 구해야 할 때 매우 유용한 알고리즘이었다.
비록 시간 복잡도는 높지만, 코드가 간결하고 음수 가중치도 처리할 수 있다는 장점이 있다.
그래프의 크기가 작고 모든 쌍의 최단 경로 정보가 필요할 때 강력한 성능을 발휘한다.