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.

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 的路径,取两者中的最短路径

二、模板

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

类型 判负环

题意

  • 正常路是m条双向正权边
  • 虫洞是w条单向负权边
  • 题目让判断是否有负权回路

办法 利用Floyd找两点间花费的最短时间,判断从起始位置到起始位置的最短时间是否为负值(判断负权环),若为负值,说明他通过虫洞回到起始位置时比自己最初离开起始位置的时间早。

代码实现: 在第二重循环,求完第i个结点后判断。ii之间的最短距离是一个负值,说明存在一个经过它的负环。

#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个整数NMN <= 100, M <= 1000),代表景区的个数和道路的条数。 接下来的M行里,每行包括3个整数a,b,c.代表ab之间有一条通路,并且需要花费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的最短路),此时枚举 ij得到一个经过 k的环( ij jk ki)并记录最小答案即可,即 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对胜负关系,胜负关系有传递性(若ABBCAC, 求有多少对不能确定的胜负关系

解法:思路很简单,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行),每行的两个整数xy,描述一个点的坐标(以一个空格隔开)。

n+2行为一个整数m,表示图中的连线个数。 此后的m行,每行描述一条连线,由两个整数i,j组成,表示第i个点和第j个点之间有连线。 最后一行:两个整数st,分别表示源点和目标点。

Output 输出文件short.out仅一行,一个实数(保留两位小数),表示从ST的最短路径的长度。

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