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.

279 lines
9.5 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
**分析**
求最小环,用$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)
(传递闭包)
#### [$HDU$-$3631$ $Shortest$ $Path$](https://acm.hdu.edu.cn/showproblem.php?pid=3631)
(变形)
**$TODO$**
据说可以使用$Dijkstra$算法解决,有空可以试试: **[链接](https://blog.csdn.net/K_R_forever/article/details/80525757)**
https://juejin.cn/post/6935691567696969764