|
|
|
|
## 图论-多源最短路径($Floyd$算法)
|
|
|
|
|
|
|
|
|
|
### 一、$Floyd$
|
|
|
|
|
$Floyd$算法是一次性求所有结点之间的最短距离,能处理负权边的图,程序比暴力的$DFS$更简单,但是复杂度是$O(n^3)$,只适合 $n < 200$的情况。
|
|
|
|
|
$Floyd$运用了 **动态规划** 的思想,求 $i 、 j$两点的最短距离,可分两种情况考虑,即经过图中某个点 $k$的路径和不经过点 $k$ 的路径,**取两者中的最短路径**。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 二、模板
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
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$](https://link.juejin.cn/?target=https%3A%2F%2Fvjudge.net%2Fproblem%2FPOJ-3259)
|
|
|
|
|
|
|
|
|
|
**类型**
|
|
|
|
|
判负环
|
|
|
|
|
|
|
|
|
|
**题意**
|
|
|
|
|
- 正常路是$m$条双向正权边
|
|
|
|
|
- 虫洞是$w$条单向负权边
|
|
|
|
|
- 题目让判断是否有负权回路
|
|
|
|
|
|
|
|
|
|
**办法**
|
|
|
|
|
利用$Floyd$找两点间花费的最短时间,判断从起始位置到起始位置的最短时间是否为负值(判断负权环),若为负值,说明他通过虫洞回到起始位置时比自己最初离开起始位置的时间早。
|
|
|
|
|
|
|
|
|
|
**代码实现**:
|
|
|
|
|
在第二重循环,求完第$i$个结点后判断。$i$到$i$之间的最短距离是一个负值,说明存在一个经过它的负环。
|
|
|
|
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#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$](http://acm.hdu.edu.cn/showproblem.php?pid=1385)
|
|
|
|
|
|
|
|
|
|
**类型**
|
|
|
|
|
打印路径
|
|
|
|
|
|
|
|
|
|
**题意**
|
|
|
|
|
给你所有城市到其他城市的道路成本和经过每个城市的城市税,给你很多组城市,要求你找出每组城市间的最低运输成本并且输出路径,**如果有多条路径则输出字典序最小的那条路径**。 **注意**,起点城市和终点城市不需要收城市税(中间点才收税,也就是插值的$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$,也就是第一个后继节点。
|
|
|
|
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#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$](https://acm.hdu.edu.cn/showproblem.php?pid=1599)
|
|
|
|
|
|
|
|
|
|
**类型: 最小环**
|
|
|
|
|
|
|
|
|
|
**题意**:
|
|
|
|
|
>杭州有$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 1
|
|
|
|
|
**$Sample$ $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`
|
|
|
|
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#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$](https://acm.hdu.edu.cn/showproblem.php?pid=1704)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**题意**
|
|
|
|
|
给出$M$对胜负关系,胜负关系有传递性(若$A$胜$B$,$B$胜$C$则$A$胜$C$), **求有多少对不能确定的胜负关系**
|
|
|
|
|
|
|
|
|
|
**解法**:思路很简单,$floyd$ 一遍做传递闭包,然后暴力枚举就行辣,但是竟然会$TLE$,然后上网学了一种新的优化姿势(其实这种优化用处不大,但由于本题是非常稀疏的图,所以$O(N^3)$几乎变成了$O(N^2)$)
|
|
|
|
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#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$](https://acm.hdu.edu.cn/showproblem.php?pid=3631)
|
|
|
|
|
(变形)
|
|
|
|
|
|
|
|
|
|
**题意**
|
|
|
|
|
有向图求$2$点间的最短路径,要求只能经过被标记的点
|
|
|
|
|
|
|
|
|
|
**思路**
|
|
|
|
|
由于只能用标记的点去更新,并且又要求任意两点之间的最短距离,显然$floyd$是最合适的。
|
|
|
|
|
这道题要用$floyd$过的话关键就看对于$floyd$的理解了,因为只有标记的点可以走,为了节省时间,我们可以在新标记点的时候以那点为中转点进行一次$floyd$,这就避免了$n^3$的复杂度
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
https://juejin.cn/post/6935691567696969764
|