【图论】总结 2:Floyd 算法
Floyd 算法求全源最短路
全源最短路问题是指对于一张图,要求图上任意两点之间的最短路径长度的问题。我们当然可以以每个节点为起点跑 \(n\) 遍单源最短路,但 Floyd 算法给出了一种更为简洁的求法。
我们按照 DP 的思想理解 Floyd 算法,设计 \(f(i,j)\) 表示从 \(i\) 到 \(j\) 的最短路径长度。考虑转移,钦定点 \(k\),那么有 \(f(i,j)=\min\{f(i,j),f(i,k)+f(k,j)\}\),其正确性显然。其中我们枚举 \(k\) 时将其看作 DP 的阶段,因此 Floyd 算法实现时必须将 \(k\) 置于最外层循环。
因为要枚举 \(k,i,j\),因此 Floyd 算法的时间复杂度是 \(O(n^3)\),且 Floyd 算法能在无负环的情况下求解含有负权边的全源最短路问题。Floyd 算法参考代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 3e2 + 10, INF = 1e9;
int n, m;
int f[N][N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
f[i][j] = INF;
for(int i = 1; i <= m; i ++)
{
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
f[u][v] = min(w, f[u][v]);
}
for(int i = 1; i <= n; i ++) f[i][i] = 0;
for(int k = 1; k <= n; k ++)
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
for(int i = 1; i <= n; i ++)
{
for(int j = 1; j <= n; j ++)
printf("%d ", f[i][j]);
puts("");
}
return 0;
}
Floyd 算法求最小环
问题:给定一个正权无向图,求图中所有环中边权和最小的(环至少包含 \(3\) 个点)。
我们采用 Floyd 算法求解这个问题。考虑 Floyd 的 DP 过程,在最外层循环刚枚举到 \(k\) 时,\(f(i,j)\) 实际上存储了“不经过编号大于 \(k-1\) 的节点”的从 \(i\) 到 \(j\) 的最短路径的长度。那么所有满足由编号不大于 \(k\) 的节点组成且经过 \(k\) 的环中边权和最小的长度为:
我们枚举 \(k\),并取最小值,同时记录方案即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 3e2 + 10, INF = 1e9;
int n, m, ans = INF;
int w[N][N], f[N][N], pos[N][N];
vector<int> circle;
void work(int u, int v)//记录方案
{
if(pos[u][v] == 0) return;
work(u, pos[u][v]);
circle.push_back(pos[u][v]);
work(pos[u][v], v);
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
w[i][j] = INF;
for(int i = 1; i <= n; i ++) w[i][i] = 0;
for(int i = 1; i <= m; i ++)
{
int u, v, w_;
scanf("%d%d%d", &u, &v, &w_);
w[v][u] = w[u][v] = min(w[u][v], w_);
}
memcpy(f, w, sizeof w);
for(int k = 1; k <= n; k ++)
{
for(int i = 1; i < k; i ++)
for(int j = i + 1; j < k; j ++)
{
if(f[i][j] + w[j][k] + w[k][i] < ans)
{
ans = f[i][j] + w[j][k] + w[k][i];
circle.clear();
circle.push_back(i);
work(i, j);
circle.push_back(j);
circle.push_back(k);
}
}
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
if(f[i][j] > f[i][k] + f[k][j])
{
f[i][j] = f[i][k] + f[k][j];
pos[i][j] = k;
}
}
if(ans == INF)//无解
{
puts("No solution.");
return 0;
}
for(auto i : circle) printf("%d ", i);
return 0;
}
Floyd 算法求传递闭包
问题:给定 \(n\) 个元素,并给定若干形如 \(a_i\odot a_j\) 的二元关系(\(\odot\) 具有传递性,例如 \(<\)、\(>\) 等),求尽可能多的元素之间的关系。
考虑每个二元关系 \(a_i\odot a_j\),将其刻画为在 \(a_i\) 和 \(a_j\) 之间连接一条边,那么所有的二元关系可构成一张图。此时将 Floyd 算法中的 \(f(i,j)\) 改为 bool
类型,并用 \(1/0\) 刻画节点 \(i\) 与 \(j\) 之间有无关系(令 \(f(i,i)=1\)),再进行位运算即可。
for(int k = 1; k <= n; k ++)
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
f[i][j] |= f[i][k] & f[k][j];
类似于传递闭包问题,Floyd 算法也可判断图中任意两点是否连通,方法与上类似。