Files
python/TangDou/Topic/【Floyd专题】.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

20 KiB
Raw Blame History

图论-多源最短路径($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];
}

三、最短路+思维

AcWing 1125 牛的旅行

总结:

  • 一遍$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$

类型 判负环

题意

  • 正常路是$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 1 Sample $Output$ 3 It’s 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

#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. 观光之旅

七、传递闭包

$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;
}

练习题

AcWing 343. 排序

这个更麻烦些,还需要输出大小的顺序。

八、变形

$HDU$-3631 Shortest $Path$

题意 有向图求$2$点间的最短路径,要求只能经过被标记的点

思路 由于只能用标记的点去更新,并且又要求任意两点之间的最短距离,显然$floyd$是最合适的。 这道题要用$floyd$过的话关键就看对于$floyd$的理解了,因为只有标记的点可以走,为了节省时间,我们可以在新标记点的时候以那点为中转点进行一次$floyd$,这就避免了$n^3$的复杂度

#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 牛站

十、其它习题

$SSL-1760$(商店选址)

题目 给出一个城市的地图(用邻接矩阵表示),商店设在一点,使各个地方到商店距离之和最短。

Input 第一行为$n$(共有几个城市); $N$小于201 第二行至第$n+1$行为城市地图(用邻接矩阵表示)

Output 最短路径之和

Sample Input

3
0 3 1
3 0 2
1 2 0
1
2
3
4

Sample Output

3
1
#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$

#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 医院设置

#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

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

3.41

Code

#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;
}