모든 정점 간의 최단 경로를 계산해야 하는 문제를 풀 때 자주 사용되는 방법이 무엇일까요?
그건 바로 플로이드 워샬, 플로이드 워셜, 플로이드 와셜 등 여러 이름으로 불리는 플로이드-와샬 알고리즘
입니다!
이번 포스팅에서 플로이드 와샬 알고리즘에 대해 학습하고 문제까지 풀어봤습니다 😊
그래프에서 모든 정점 간의 최단 경로
를 계산하는 알고리즘
-> 방향 그래프, 무방향 그래프 모두 사용 가능
-> 음수 가중치는 상관 없음, 하지만 음수 사이클에서는 사용 불가
모든 노드 쌍의 최단 경로를 한 번에 계산 가능 (배열에 저장하니까)
동적 계획법(DP) 기반 알고리즘
-> 노드 간의 거리 중 가장 작은 것들을 조합해 최종적으로 최단 거리 반환하기 때문
음수 사이클
그래프의 특정 순환 경로 가중치 합이
음수
가 되는 사이클
⠀
ex)A -> B -> C -> A
라는 순환 경로의 가중치 합이-5
라면 음수 사이클임
=> 이 사이클을 반복할 수록 최단거리 갱신됨 (-Infinity로 향해가니까)
=> 이런 경우 최단 경로를 정의할 수 없음
구현 방식
그래프의 각 가중치를 저장하는 2차원 배열 생성
-> dist[i][j]
는 i -> j
의 거리(가중치)
-> 연결되지 않은 노드들은 Infinity, 자기 자신은 0 할당
모든 노드들이 중간 노드가 될 수 있도록 k로 for문을 돌고, 이때 i -> j
와 i -> k -> j
의 거리를 비교해 최단거리 갱신
-> dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
dist는 모든 가능한 최단 거리 경로를 담은 배열이 됨
=> for문을 3번 돌기 때문에 시간복잡도는 O(n^3)
-> 노드 수(n)가 많으면 계산량이 많아져서 비효율적
문제들을 풀다보니 두 유형이 헷갈릴 수 있겠다고 느껴져 차이점을 정리했습니다.
제가 느끼기에는 모든 노드의 연결이 필수냐 아니냐에 따라 문제 유형이 달라지는 것 같습니다 !
플로이드-와샬 | MST | |
---|---|---|
목적 | 모든 정점 간의 최단 경로 계산 | 모든 정점을 최소 비용으로 연결하는 트리 |
결과 | 노드 쌍 간의 최단 거리 테이블 | 최소 비용으로 구성된 간선들 집합 |
키워드 | 최단 거리, 모든 노드 쌍 | 최소 비용 연결, 스패닝 트리 |
그래프 | 방향 / 무방향 둘 다 가능 | 일반적으로 무방향 연결 그래프 |
기반 알고리즘 | 동적 계획법 | 그리디 |
⠀
문제 리스트
- 백준: 11404, 2458, 1389
- 프로그래머스: 순위, 등산 코스 정하기
import sys
n = int(sys.stdin.readline().strip())
m = int(sys.stdin.readline().strip())
graph = [[float("inf")] * n for _ in range(n)]
for _ in range(m):
a, b, cost = list(map(int, sys.stdin.readline().strip().split()))
graph[a - 1][b - 1] = min(graph[a - 1][b - 1], cost)
for k in range(n):
for i in range(n):
for j in range(n):
if i == j: continue
graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j])
for i in graph:
curStr = " ".join(map(str, i))
print(curStr.replace("inf", "0"))
📢 풀이 설명
이 문제는 버스의 구간이 같아도 출발, 도착 방향이 반대면 다른 노선이다. 또, 같은 노선이라도 값이 여러 개 있을 수 있다.
즉, 방향그래프이고, 같은 구간이라도 주어지는 입력의 최솟값으로 갱신해야한다.
이 점을 유의해서 풀면 플로이드-와샬 알고리즘의 정석 문제이다.
마지막으로 연결되지 않은 구간끼리의 값을 inf
가 아닌 0
으로 표기해야한다 !!
(문제를 꼼꼼히 읽지 않아 틀린 이유 찾느라 오래걸린 사람의 마지막 말입니다 ㅜㅜ)
# BFS 풀이
import sys
from collections import deque
n, m = list(map(int, sys.stdin.readline().strip().split()))
bigGraph = [[] for _ in range(n)]
smallGraph = [[] for _ in range(n)]
for _ in range(m):
a, b = list(map(int, sys.stdin.readline().strip().split()))
bigGraph[a - 1].append(b - 1)
smallGraph[b - 1].append(a - 1)
def bfs(i, graph):
queue = deque([i])
visited = [False] * n
visited[i] = True
count = 0
while queue:
num = queue.popleft()
for k in graph[num]:
if not visited[k]:
visited[k] = True
queue.append(k)
count += 1
return count
result = 0
for i in range(n):
big = bfs(i, bigGraph)
small = bfs(i, smallGraph)
if big + small == n - 1: result += 1
print(result)
# 플로이드-와샬 풀이
import sys
n, m = list(map(int, sys.stdin.readline().strip().split()))
edges = [[0] * n for _ in range(n)]
for _ in range(m):
a, b = list(map(int, sys.stdin.readline().strip().split()))
edges[a - 1][b - 1] = 1
for k in range(n):
for i in range(n - 1):
for j in range(i + 1, n):
if edges[i][k] + edges[k][j] == 2: edges[i][j] = 1
count = 0
for i in range(n):
tmpCount = 0
for j in range(n):
tmpCount += edges[i][j] + edges[j][i]
if tmpCount == (n - 1): count += 1
print(count)
📢 풀이 설명
이 문제는 Python3
에서 플로이드-와샬로 풀면 시간초과
가 난다. n이 최대 500
이기 때문에 500^3 = 125,000,000
이 되기 때문이다.
플로이드-와샬로 통과하고싶다면 PyPy를 써야한다고 한다.
파이썬3로 풀려면 dfs / bfs
를 써야한다.
import sys
n, m = list(map(int, sys.stdin.readline().strip().split()))
graph = [[float("inf")] * (n + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
graph[i][i] = 0
for _ in range(m):
a, b = list(map(int, sys.stdin.readline().strip().split()))
graph[a][b] = 1
graph[b][a] = 1
for k in range(1, n + 1):
for i in range(1, n + 1):
for j in range(i + 1, n + 1):
graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j])
graph[j][i] = graph[i][j]
result = [0, float("inf")]
for i in range(1, n + 1):
curSum = sum(graph[i][1:])
if curSum < result[1]: result = [i, curSum]
print(result[0])
📢 풀이 설명
직접적인 간선 간의 가중치를 그래프로 만들고, 3중 포문을 돌며 간접적 간선들의 가중치를 최솟값으로 갱신한다.
번호가 1번부터 시작되기 때문에 0번 인덱스는 inf 그대로 두었다.
이 문제는 전에 bfs로 풀었었는데 이번에는 플로이드-와샬 알고리즘으로 풀어봤다.
개인적으로는 bfs로 푸는 게 더 효율적인 것 같다.
function solution(n, results) {
let result = 0;
const graph = Array.from({length: n + 1}, () => new Array(n + 1).fill(0));
⠀
for (const [win, lose] of results) {
graph[win][lose] = 1;
}
⠀
for (let k = 1; k <= n; k++) {
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= n; j++) {
if (graph[i][k] && graph[k][j]) graph[i][j] = 1;
}
}
}
⠀
for (let i = 1; i <= n; i++) {
let count = 0;
⠀
for (let j = 1; j <= n; j++) {
count += graph[i][j] + graph[j][i];
}
⠀
if (count == n - 1) result += 1;
}
⠀
return result;
}
📢 풀이 설명
이긴 선수
, 인덱스 속의 인덱스 = 진 선수
k = 중간에 거치는 선수, i = 이긴 선수, j = 진 선수
로 for문 3번 돌기i가 k를 이기고 && k가 j를 이기면
i가 j를 이긴 것
과 같으므오 graph[i][j]
도 1로 변경i가 j를 이겼
거나 j가 i를 이겼다
면 이긴 관계는 값이 1임 (둘 중 1개가 1이라면 승패가 났다는 뜻)
📢 풀이 설명