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.

639 lines
20 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];
}
```
### 三、最短路+思维
#### [$AcWing$ $1125$ 牛的旅行](https://www.cnblogs.com/littlehb/p/16025700.html)
总结:
- 一遍$floyd$算出原始各连通块内的多源最短路径
- 遍历枚举找出每个点在自己连通块中可以到达的最远距离,$PK$后获取到原始的最大直径长度
- 遍历所有可能连接上的两个不在同一连通块中的点,尝试连接上这两个点后,得到可以获得到的最小直径。
- 原始直径与遍历尝试的所有可能直径$PK$,谁大谁是答案。
### 四、判负环
眼尖的人儿可能发现邻接矩阵 $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;
}
}
```
**练习题**
#### [$AcWing$ $344$. 观光之旅](https://www.cnblogs.com/littlehb/p/16033489.html)
### 七、传递闭包
#### [$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;
}
```
练习题
#### [$AcWing$ $343$. 排序](https://www.cnblogs.com/littlehb/p/16029054.html)
这个更麻烦些,还需要输出大小的顺序。
### 八、变形
#### [$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;
}
```
### 九、限定边数量的情况下求多源最短路径
#### [$AcWing$ $345$ 牛站](https://www.cnblogs.com/littlehb/p/16043039.html)
### 十、其它习题
#### $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;
}
```
#### [$P1364$ 医院设置](https://www.luogu.com.cn/problem/P1364)
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;
const int INF = 0x3f3f3f3f;
int g[150][150];
int w[N]; // 居民人口数,点权
int main() {
int n;
cin >> n;
// 地图初始化
memset(g, 0x3f, sizeof g);
for (int i = 1; i <= n; i++) g[i][i] = 0;
for (int i = 1; i <= n; i++) {
int a, b;
cin >> w[i] >> a >> b; // w[i]:点权
g[i][a] = g[a][i] = 1; // i<->a无向边
g[i][b] = g[b][i] = 1; // i<->b无向边
}
// 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 ans = INF;
for (int i = 1; i <= n; i++) { // 如果将医院设置在i处那么计算一下它到各地的加权和
int s = 0;
for (int j = 1; j <= n; j++) s += w[j] * g[i][j];
ans = min(ans, s);
}
printf("%d", ans);
return 0;
}
```
#### [$SSL-1613$]()
> Description
平面上有$n$个点($N<=100$),每个点的坐标均在$-10000\sim 10000$之间。其中的一些点之间有连线。若有连线,则表示可从一个点到达另一个点,即两点间有通路,通路的距离为两点直线的 **距离** 。现在的任务是找出从一点到另一点之间的最短路径。
$Input$
输入文件$short.in$,共有$n+m+3$行,其中:
第一行为一个整数$n$。
第$2$行到第$n+1$行(共$n$行),每行的两个整数$x$和$y$,描述一个点的坐标(以一个空格隔开)。
第$n+2$行为一个整数$m$,表示图中的连线个数。
此后的$m$行,每行描述一条连线,由两个整数$i,j$组成,表示第$i$个点和第$j$个点之间有连线。
最后一行:两个整数$s$和$t$,分别表示源点和目标点。
$Output$
输出文件$short.out$仅一行,一个实数(保留两位小数),表示从$S$到$T$的最短路径的长度。
$Sample$ $Input$
```cpp {.line-numbers}
5
0 0
2 0
2 2
0 2
3 1
5
1 2
1 3
1 4
2 5
3 5
1 5
```
Sample Output
```cpp {.line-numbers}
3.41
```
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
int o(int t) {
return t * t;
}
const int N = 110;
int n, m, x[N], y[N];
double g[N][N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> x[i] >> y[i];
// double类型的数组初始化不能用memset!!!!
// memset(g, 0x3f, sizeof g);
// for (int i = 0; i <= n; i++) g[i][i] = 0;
// 需要用二重循环进行初始化
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] = 0x3f3f3f3f;
}
cin >> m;
int l, r;
while (m--) {
cin >> l >> r;
g[r][l] = g[l][r] = sqrt((double)o(x[l] - x[r]) + (double)o(y[l] - y[r])); // 勾股定理
}
// 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];
cin >> l >> r;
printf("%.2lf", g[l][r]);
return 0;
}
```