11 KiB
图论-多源最短路径(Floyd
算法)
一、Floyd
Floyd
算法是一次性求所有结点之间的最短距离,能处理负权边的图,程序比暴力的DFS
更简单,但是复杂度是O(n^3)
,只适合 n < 200
的情况。
Floyd
运用了 动态规划 的思想,求 i 、 j
两点的最短距离,可分两种情况考虑,即经过图中某个点 k
的路径和不经过点 k
的路径,取两者中的最短路径。
二、模板
void floyd() {
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
if (g[i][k] != inf) //优化
for (int j = 1; j <= n; j++)
if (g[i][j] > g[i][k] + g[k][j])
g[i][j] = g[i][k] + g[k][j];
}
三、判负环
眼尖的人儿可能发现邻接矩阵 g
中, g[i][i]
并没有赋初值0
,而是 inf
。并且计算后 g[i][i]
的值也不是 0
,而是 g[i][i]=g[i][u]+……+g[v][i]
,即从外面绕一圈回来的最短路径,而这正 用于判断负圈,即 g[i][i]<0
。
相关变形结合题目讲,如:负圈、打印路径、最小环、传递闭包
记录坑点:重复边,保留最小的那个。
POJ-3259
Wormholes
类型 判负环
题意
- 正常路是
m
条双向正权边 - 虫洞是
w
条单向负权边 - 题目让判断是否有负权回路
办法
利用Floyd
找两点间花费的最短时间,判断从起始位置到起始位置的最短时间是否为负值(判断负权环),若为负值,说明他通过虫洞回到起始位置时比自己最初离开起始位置的时间早。
代码实现:
在第二重循环,求完第i
个结点后判断。i
到i
之间的最短距离是一个负值,说明存在一个经过它的负环。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 502;
int n, m, w;
int g[N][N];
// floyd判断是否存在负圈
bool floyd() {
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
if (g[i][k] != INF) { // 优化
for (int j = 1; j <= n; j++)
if (g[i][j] > g[i][k] + g[k][j])
g[i][j] = g[i][k] + g[k][j];
if (g[i][i] < 0) return true; // 发现负圈
}
return false;
}
int main() {
int T;
cin >> T;
while (T--) {
cin >> n >> m >> w;
memset(g, INF, sizeof g); // 初始化邻接矩阵
// 双向正值边
while (m--) {
int a, b, c;
cin >> a >> b >> c;
// 注意坑:重边
g[a][b] = g[b][a] = min(c, g[a][b]);
}
// 单向负值边
while (w--) {
int a, b, c;
cin >> a >> b >> c;
g[a][b] = -c; // 负值边
}
if (floyd())
puts("YES");
else
puts("NO");
}
return 0;
}
四、打印路径
HDU-1385
Minimum
Transport
Cost
类型 打印路径
题意
给你所有城市到其他城市的道路成本和经过每个城市的城市税,给你很多组城市,要求你找出每组城市间的最低运输成本并且输出路径,如果有多条路径则输出字典序最小的那条路径。 注意,起点城市和终点城市不需要收城市税(中间点才收税,也就是插值的k
收税)。
分析
输出路径,多个答案则输出字典序最小的,无法到达输出-1
。
读入邻接表, w[]
记录每个城市额外费用, path[][]
记录路径,floyd()
里维护即可。然后处理下输出(比较恶心)。
解释:
int path[N][N];
i \rightarrow j
可能存在多条路线,我要找最短的。如果有多条最短的,我要字典序最小的。现在路线唯一了吧!比如这条路线最终是i \rightarrow a \rightarrow b \rightarrow c \rightarrow d \rightarrow j
,则path[i][j]=a
,也就是第一个后继节点。
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int INF = 0x3f3f3f3f;
// Floyd+记录起点后继
int n;
int g[N][N], w[N];
int path[N][N]; // 记录i到j最短路径中i的后继
void floyd() {
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
if (g[i][j] > g[i][k] + g[k][j] + w[k]) {
g[i][j] = g[i][k] + g[k][j] + w[k];
path[i][j] = path[i][k]; // i->j这条最短路径上,i后面第一个节点,是i->k路径上第一个节点
}
// 相同路径下选择后继更小的(为了字典序)
if (g[i][j] == g[i][k] + g[k][j] + w[k])
if (path[i][j] > path[i][k])
path[i][j] = path[i][k];
}
}
// 递归输出路径
void print(int s, int e) {
printf("-->%d", path[s][e]); // 输出s的后继
if (path[s][e] != e) // 如果不是直连
print(path[s][e], e); // 递归输出后继
}
/*
From 1 to 3 :
Path: 1-->5-->4-->3
Total cost : 21
From 3 to 5 :
Path: 3-->4-->5
Total cost : 16
From 2 to 4 :
Path: 2-->1-->5-->4
Total cost : 17
*/
int main() {
#ifndef ONLINE_JUDGE
freopen("HDU1385.in", "r", stdin);
#endif
while (cin >> n, n) {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
cin >> g[i][j];
if (g[i][j] == -1) g[i][j] = INF;
path[i][j] = j;
}
for (int i = 1; i <= n; i++) cin >> w[i];
floyd();
int s, e;
while (cin >> s >> e, ~s && ~e) {
printf("From %d to %d :\n", s, e);
printf("Path: %d", s);
if (s != e) print(s, e); // 起点与终点不同开始递归
printf("\nTotal cost : %d\n\n", g[s][e]);
}
}
return 0;
}
五、最小环
HDU
-1599
find
the
mincost
route
类型: 最小环
题意:
杭州有
N
个景区,景区之间有一些双向的路来连接,现在8600
想找一条旅游路线,这个路线从A
点出发并且最后回到A
点,假设经过的路线为V_1,V_2,…V_K
,V_1
,那么必须满足K>2
,就是说至除了出发点以外至少要经过2
个其他不同的景区,而且不能重复经过同一个景区。现在8600
需要你帮他找一条这样的路线,并且花费越少越好。Input
第一行是2
个整数N
和M
(N <= 100, M <= 1000
),代表景区的个数和道路的条数。 接下来的M
行里,每行包括3
个整数a,b,c
.代表a
和b
之间有一条通路,并且需要花费c
元(c <= 100
)。Output
对于每个测试实例,如果能找到这样一条路线的话,输出花费的最小值。如果找不到的话,输出"It’s impossible.".Sample
Input
cpp 3 3 1 2 1 2 3 1 1 3 1 3 3 1 2 1 1 2 3 2 3 1Sample
Output
3 It’s impossible
分析:
求最小环,用dis[]
记录原距离,当枚举中间结点 k
时,首先知道任意两点 i、j
不经过 k
的最短路径 mp[i][j]
(原floyd
的二三重循环后更新 mp[i][j]
得到经过 k
的最短路),此时枚举 i
和 j
得到一个经过 k
的环( i
到 j
, j
到 k
, k
到 i
)并记录最小答案即可,即 mp[i][j] + dis[j][k] + dis[k][i]
。
注意题目 i, j, k
不能相同,还有坑点:long long
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int INF = 0x3f3f3f3f;
const int N = 110;
int dis[N][N], g[N][N];
int n, m, ans;
void floyd() {
memcpy(dis, g, sizeof g);
for (int k = 1; k <= n; k++) {
// 最小环的DP操作
for (int i = 1; i < k; i++) // 枚举i,j
for (int j = i + 1; j < k; j++) // 注意i,j,k不能相同
if (ans > dis[i][j] + g[i][k] + g[k][j])
ans = dis[i][j] + g[i][k] + g[k][j];
for (int i = 1; i <= n; i++) // 原floyd
for (int j = 1; j <= n; j++)
if (dis[i][j] > dis[i][k] + dis[k][j])
dis[i][j] = dis[i][k] + dis[k][j];
}
}
signed main() {
while (cin >> n >> m && (~n && ~m)) {
// 邻接矩阵初始化
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (i == j)
g[i][j] = 0;
else
g[i][j] = INF;
while (m--) {
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(c, g[a][b]); // 防重边
}
ans = INF;
floyd();
if (ans == INF)
puts("It's impossible.");
else
cout << ans << endl;
}
}
六、传递闭包
HDU
-1704
Rank
题意
给出M
对胜负关系,胜负关系有传递性(若A
胜B
,B
胜C
则A
胜C
), 求有多少对不能确定的胜负关系
解法:思路很简单,floyd
一遍做传递闭包,然后暴力枚举就行辣,但是竟然会TLE
,然后上网学了一种新的优化姿势(其实这种优化用处不大,但由于本题是非常稀疏的图,所以O(N^3)
几乎变成了O(N^2)
)
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
const int N = 510;
int n, m, x, y, ans;
int g[N][N];
void floyd() {
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++) {
if (!g[i][k]) continue; // floyd优化
for (int j = 1; j <= n; j++)
g[i][j] |= g[i][k] & g[k][j]; // 通过k传递,或运算
}
}
int main() {
int T;
cin >> T;
while (T--) {
cin >> n >> m;
memset(g, 0, sizeof g);
while (m--) {
cin >> x >> y;
g[x][y] = 1; // x<y
}
// 计算传递闭包
floyd();
ans = 0;
for (int i = 1; i <= n; i++) // 统计答案
for (int j = i + 1; j <= n; j++)
if (!g[i][j] && !g[j][i]) ans++; // 无法确定大小关系
cout << ans << endl;
}
return 0;
}
七、变形
HDU
-3631
Shortest
Path
(变形)
题意
有向图求2
点间的最短路径,要求只能经过被标记的点
思路
由于只能用标记的点去更新,并且又要求任意两点之间的最短距离,显然floyd
是最合适的。
这道题要用floyd
过的话关键就看对于floyd
的理解了,因为只有标记的点可以走,为了节省时间,我们可以在新标记点的时候以那点为中转点进行一次floyd
,这就避免了n^3
的复杂度