You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

499 lines
15 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 图论-多源最短路径($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$**
对于每个测试实例,如果能找到这样一条路线的话,输出花费的最小值。如果找不到的话,输出"Its 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
Its impossible
**分析**
求最小环,用$g[]$记录原距离,当枚举中间结点 $k$时,首先知道任意两点 $i、j$不经过 $k$的最短路径 $dis[i][j]$(原$floyd$的二三重循环后更新 $dis[i][j]$得到经过$k$的最短路),此时枚举 $i$和 $j$得到一个经过 $k$的环( $i$到 $j$ $j$到 $k$ $k$到 $i$)并记录最小答案即可,即 $dis[i][j] + g[i][k] + g[k][j]$。
注意题目 $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$的复杂度
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
const int N = 310;
int t, n, m, q;
int g[N][N];
bool flag[N]; // 记录是否标记
int a, b, c;
void floyd(int k) { // 以k为中转节点进行转移
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (g[i][j] > g[i][k] + g[k][j])
g[i][j] = g[i][k] + g[k][j];
}
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
while (cin >> n >> m >> q && n + m + q) {
if (t) printf("\n"); // 谜之格式
printf("Case %d:\n", ++t);
// 整体正无穷,对角线清零
memset(g, inf, sizeof g);
for (int i = 0; i <= n; i++) g[i][i] = 0;
memset(flag, false, sizeof flag);
while (m--) {
cin >> a >> b >> c;
g[a][b] = min(c, g[a][b]); // floyd也可以跑有向图
}
while (q--) {
cin >> c;
if (c == 0) {
cin >> a;
if (flag[a]) // 如果a已经被标记过了
printf("ERROR! At point %d\n", a);
else {
flag[a] = true; // 标记上
floyd(a); // 通过a进行其它节点转移
}
} else {
cin >> a >> b;
if (!(flag[a] && flag[b]))
printf("ERROR! At path %d to %d\n", a, b);
else if (g[a][b] == inf)
printf("No such path\n");
else
printf("%d\n", g[a][b]);
}
}
}
return 0;
}
```
### 八、其它习题
#### $SSL-1760$(商店选址)
**题目**
给出一个城市的地图(用邻接矩阵表示),商店设在一点,使各个地方到商店距离之和最短。
$Input$
第一行为$n$(共有几个城市); $N$小于$201$
第二行至第$n+1$行为城市地图(用邻接矩阵表示)
$Output$
最短路径之和
$Sample$ $Input$
```cpp {.line-numbers}
3
0 3 1
3 0 2
1 2 0
1
2
3
4
```
$Sample$ $Output$
```cpp {.line-numbers}
3
1
```
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 210;
int n, g[N][N];
const int INF = 0x3f3f3f3f;
int ans = INF;
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
cin >> g[i][j];
if (g[i][j] == 0 && i != j) g[i][j] = INF; // 建图(注意i==j要为0)
}
// 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])
g[i][j] = g[i][k] + g[k][j];
int s;
for (int i = 1; i <= n; i++) {
s = 0;
for (int j = 1; j <= n; j++) s += g[i][j];
if (s < ans) ans = s;
}
printf("%d", ans);
return 0;
}
```
#### [$P1828$ [$USACO3.2$] 香甜的黄油 $Sweet$ $Butter$](https://www.luogu.com.cn/problem/P1828)
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 814;
const int INF = 0x3f3f3f3f;
int id[N];
int a, b, c, g[N][N], res = INF;
int main() {
#ifndef ONLINE_JUDGE
freopen("P1828_11.in", "r", stdin);
// 参考答案8
#endif
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
int p, n, m; // p只奶牛n个牧场m条边
cin >> p >> n >> m;
memset(g, 0x3f, sizeof g);
for (int i = 1; i <= n; i++) g[i][i] = 0; // 初始化
for (int i = 1; i <= p; i++) cin >> id[i]; // i号奶牛在id[i]这个牧场
while (m--) {
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(c, g[a][b]);
}
// 标准的Floyd板子
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++) {
if (g[i][k] == INF) continue; // floyd小优化
for (int j = 1; j <= n; j++)
if (g[i][j] > g[i][k] + g[k][j])
g[j][i] = g[i][j] = g[i][k] + g[k][j];
}
for (int i = 1; i <= n; i++) { // 每个牧场出发
int ans = 0;
for (int j = 1; j <= p; j++) ans += g[i][id[j]];
if (ans >= 0) res = min(res, ans);
}
printf("%d", res);
return 0;
}
```
SSL-1761(城市问题)
#### [$P1364$ 医院设置](https://www.luogu.com.cn/problem/P1364)
SSL-1613=CCF1342最短路径问题