2025. 6. 30. 18:00ㆍ알고리즘/알고리즘 지식
[실전 알고리즘] 0x1C강 - 플로이드 알고리즘
안녕하세요, 이번에는 플로이드 알고리즘을 다루겠습니다. 이제 최단경로 알고리즘인 플로이드, 다익스트라 알고리즘만 다루고 나면 나름 길었던 그래프 파트가 끝납니다. 목차는 눈으로 한 번
blog.encrypted.gg
바킹독님의 블로그를 참고하여 공부한 내용을 기록한 글입니다.

위의 그래프에서 직접 연결된 간선에 한해, 첫 단계의 최단 거리를 구해보자.

굉장히 쉽게 구할 수 있다.
1에서 1로 가는 거리는 당연하게도 0이고 1에서 2는 바로 갈 수 있기에 그 가중치인 4를 적어주면 된다. 3에서 5의 경우 4를 거쳐서 간다면 8로 갈 수 있지만 현재는 하나의 간선만 고려하기에, 즉 바로 갈 수 있는 경로만 고려하기에 15로 구해진다.
플로이드 알고리즘은 여기서 일단 정점 한 개를 더 거쳐서 갱신할 수 있는 최단 거리를 계산하는 알고리즘이다.
즉, D[s][t]에 대해 D[s][t] / D[s][u] + D[u][t]를 비교하는 방식이며, u를 1~n 까지 모든 정점에 대해 반복하면 된다.
두 번째 단계의 테이블은 1번 정점을 거쳐서 갱신할 수 있는 거리는 갱신해버리면 된다.

예를 들어 3->2를 생각해보면, 기존 무한대에서 1을 거치는 5로 업데이트 되었다.
바로 문제로 구현해보자.
간단하게 삼중 for문을 때려버려도 된다.
https://www.acmicpc.net/problem/11404
/** 최단거리 11404 플로이드 **/
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
int n, m;
int dist[105][105];
const int INF = 0x3f3f3f3f;
int main()
{
ios_base::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
fill(dist[i], dist[i] + n + 1, INF);
for (int i = 0; i < m; i++)
{
int a, b, c;
cin >> a >> b >> c;
dist[a][b] = min(dist[a][b], c);
}
for (int i = 1; i <= n; i++)
dist[i][i] = 0;
// k번째 원소가 중간에 거치는 원소
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if (dist[i][j] == INF)
cout << "0 ";
else
cout << dist[i][j] << ' ';
}
cout << '\n';
}
}
구현 시 주의사항
- INF는 0x7f7f7f7f로 int형의 최대 값으로 선언하게 되면, 두 INF를 더하는 과정에서 오버플로우가 생긴다.
- 삼중 for 문에서 당연하게도 중간에 거쳐갈 노드 k는 가장 바깥으로 빼야 한다.
- adj를 인접행렬로 선언하면 바로 최단 거리 저장용 테이블로 사용할 수 있다.
경로 복원
a에서 b까지 최단 거리로 가려면 a에서 어느 정점으로 가야하는지 저장해야 하는데, 이를 nxt 테이블이라 하자.
nxt 테이블은 최단 거리 테이블을 채우면서 같이 채울 수 있다.
먼저 아무 정점도 거치지 않는 첫 단계를 생각해보자.

최단 거리 테이블에 2->1 경로를 저장했고, 해당 경로의 중간에 거친 노드는 nxt 테이블의 [2][1]에 1로 저장되어 있다.
다음 단계부터가 중요하다. 다음 단계는 1번 노드를 거치는 최단 거리를 계산하는 단계인데, 2->3을 생각해보면 2->1->3이 최단거리이므로 nxt 테이블에는 nxt[2][3]=1이 저장되면 될 것이다. 그대로 채워보자.
여기서 1이 저장되는 이유는 1번 노드를 방문하는 단계니까 1을 넣는 것이 아니다. nxt[s][t]를 nxt[s][1]로 대체하는 것이다. 왜인지는 다음 단계에서 더 자세히 보자.

그 다음 두 번째 단계는 2번 노드도 거치는 경우이다.
이번에 신경써야 할 부분은 nxt를 2로만 채우는 것이 아니라, s->t 보다 s->2 + 2->t인 경우. 즉 이번 단계에서 갱신되는 노드에 대해 nxt[s][t]를 nxt[s][2]로 채워야 한다는 것이다.

nxt는 a에서 b로 갈 때 어느 정점으로 먼저 갈지를 저장하는 배열이다. 3->5를 생각해보자. 이번 단계에서 3->2, 2->5가 더 효율적임을 알아서 갱신했고, 이에 따라 nxt 배열 또한 3에서 5를 갈 때 어디로 먼저 가면 될까를 저장해야 한다.
저장될 값은 1일테고, 이를 구현적으로는 nxt[3][2]로 표현하면 된다. 3에서 5로 갈 때 가장 먼저 거쳐야 할 값은 3에서 2로 갈 때 거치는 값 = 1인 것이다. 바로 다음 단계도 보자.

3번 노드를 거치는 경우는 아무 일도 없어서 갱신이 없다. 다음도 보자.

이번에는 3->5가 특이하다. 이 전까지는 3->1->2->5가 최단 거리였고, nxt에는 1이 저장되어 있었다. 4번 노드를 거치게 되는 경우를 생각해봐도 3->1->4->5이므로 똑같이 nxt에도 1이 저장된다.
이제 nxt 배열은 다 구했고, 이를 바탕으로 경로를 복원하면 된다.
똑같이 3->5로 복원해보자.
nxt[3][5] = 1이다. 따라서 3 -> 1 경로는 자명하다.
그 다음은? 1에서 5까지 갈 때 그 다음에 갈 곳은 nxt[1][5]에 저장되어 있다. nxt[1][5]는 4이므로 3 -> 1 -> 4 까지는 구했고, 그 다음은 nxt[4][5] = 5로 3 -> 1 -> 4 -> 5의 경로 완성이다.
다시, 정리해보자면 nxt[i][j]는 i->j로 갈 때 가장 먼저 방문해야 할 곳이다. 따라서 k번째 노드를 경유하는게 더 최단 거리인 경우
nxt[i][j] = nxt[i][k]로 갱신하면 된다.
- 여기서 nxt[i][k]를 단순히 암기해서는 안된다. 무수한 알고리즘들을 외우는 방식은 코딩 테스트를 통과할 수 없고 원리를 이해해야 한다.
- nxt[i][j]는 i가 시작점이다. i -> ... -> k -> ... -> j 의 경로에서 i 바로 뒤에 위치한 노드는 nxt[i][k]로 구할 수 있기에 nxt[i][k]로 갱신하는 것이다.
- 케이스를 나눠서 좀 더 명확히 이해해보자.
- 만약 i -> k 사이에 노드가 없다면 nxt[i][k] = k 일 것이다.
- i -> u -> k 라면 nxt[i][k] = u 일텐데, 이 값은 언제 초기화되었을까? 가장 바깥쪽 loop, 즉 경유 노드가 u일 때 i -> k 경로를 구하면서 nxt[i][k] = u로 초기화되었다.
문제로 구현해보자.
https://www.acmicpc.net/problem/11780
구현은 크게 어려운 부분은 없다.
/** 최단거리 11404 플로이드 **/
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
int n, m;
int dist[105][105];
int nxt[105][105];
const int INF = 0x3f3f3f3f;
int main()
{
ios_base::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
fill(dist[i], dist[i] + n + 1, INF);
for (int i = 0; i < m; i++)
{
int a, b, c;
cin >> a >> b >> c;
dist[a][b] = min(dist[a][b], c);
// nxt도 초기화. 첫 단계이므로 a->b 간선이 있으면 nxt[a][b] = b
nxt[a][b] = b;
}
for (int i = 1; i <= n; i++)
dist[i][i] = 0;
// k번째 원소가 중간에 거치는 원소
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
{
// k 번째 원소를 거치는 경우가 더 최단 거리인 경우
if (dist[i][j] > dist[i][k] + dist[k][j])
{
// nxt[i][j] = i -> j 로 갈 때 가장 먼저 방문할 노드이므로 nxt[i][k] 저장
dist[i][j] = dist[i][k] + dist[k][j];
nxt[i][j] = nxt[i][k];
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if (dist[i][j] == INF)
cout << "0 ";
else
cout << dist[i][j] << ' ';
}
cout << '\n';
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if (dist[i][j] == 0 || dist[i][j] == INF)
{
cout << "0\n";
continue;
}
vector<int> path;
int st = i;
while (st != j)
{
path.push_back(st);
st = nxt[st][j];
}
path.push_back(j);
cout << path.size() << ' ';
for (auto x : path)
cout << x << ' ';
cout << '\n';
}
}
}
'알고리즘 > 알고리즘 지식' 카테고리의 다른 글
| [알고리즘] 다익스트라 알고리즘에서의 경로 복원, 백준 11779번: 최소비용 구하기 2 C++ 풀이까지 (2) | 2025.07.13 |
|---|---|
| [알고리즘] 최단 거리 알고리즘 - 다익스트라, 백준 1753 C++ 풀이까지 (2) | 2025.07.11 |
| [알고리즘] 투포인터 - 백준 2230: 수 고르기, 백준 1806: 부분합 (0) | 2025.03.25 |
| [알고리즘] 이분탐색 - 백준 1920, 2295, 1654 (0) | 2025.02.19 |
| [알고리즘] 그리디 - 백준 11047: 동전 0, 1931: 회의실 배정, 2217: 로프, 12865: 평범한 배낭 (1) | 2025.01.09 |