第四章 图论(2):多源汇最短路模型

最短路问题前提:图中一定不存在负环

Floyd原理

集合
d ( k , i , j ) d(k,i,j) d(k,i,j):所有从 i i i出发,最终走到 j j j,且中间只经过节点编号不超过 k k k的所有路径长度的最小值

状态计算
根据第 k k k个结点是否在路径中

  • 所有不包含结点 k k k的路径: d ( k − 1 , i , j ) d(k-1,i,j) d(k1,i,j)
  • 所有包含结点 k k k的路径: d ( k − 1 , i , k ) + d ( k − 1 , k , j ) d(k-1,i,k)+d(k-1,k,j) d(k1,i,k)+d(k1,k,j)

故有: d ( k , i , j ) = m i n { d ( k − 1 , i , k ) + d ( k − 1 , k , j ) , d ( k − 1 , i , j ) } d(k,i,j)=min \{d(k-1,i,k)+d(k-1,k,j), d(k-1,i,j) \} d(k,i,j)=min{d(k1,i,k)+d(k1,k,j),d(k1,i,j)}

优化后有: d ( i , j ) = m i n { d ( i , k ) + d ( k , j ) , d ( i , j ) } d(i,j)=min \{d(i,k)+d(k,j), d(i,j) \} d(i,j)=min{d(i,k)+d(k,j),d(i,j)}

Floyd应用

  • 最短路
  • 传递闭包
  • 找最小环
  • 恰好经过K条边的最短路(倍增)

0、FLoyd算法基础模型

使用前提:图中一定不能包含负环!最短距离会变成-∞

时间复杂度: O(n^3)

算法思路:

d[i][j]:存储所有的边

d[k, i, j]表示从i只经过1k这些中间结点到达j的最短距离,基于动态规划思想,状态转移方程为:d[k, i, j] = d[k-1, i, k] + d[k-1, k, j],表示加上第k个结点之后,ij的距离就表示min(i到j的距离, i->k + k->j 的距离)。第一维优化后便是:d[i][j] = min(d[i][j], d[i][k] + d[k][j])

for (k = 1; k <= n; k ++ )
	for (i = 1; i <= n; i ++ )
		for (j = 1; j <= n; j ++ )
			d[i][j] = min(d[i][j], d[i][k] + d[k][j]);

循环之后,d[i][j]存储的是从ij的最短路径长度。

1、Floyd求最短路

ACWing 854

注: 由于本题存在重边和自环,处理方法是

  • 对于重边,初始化的时候保留最小边即可;
  • 对于自环直接删除即可,即d[i][i] = 0
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 210, INF = 1e9;

int n, m, Q;
int d[N][N];

void floyd() {
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

int main() {
    scanf("%d%d%d", &n, &m, &Q);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (i == j) d[i][j] = 0; // 对角线上为0(自环)
            else d[i][j] = INF;      // 其余是正无穷
    while (m--) {
        int a, b, c; scanf("%d%d%d", &a, &b, &c);
        d[a][b] = min(d[a][b], c);   // 多条边保留最小边
    }
    floyd();
    while (Q--) {
        int a, b; scanf("%d%d", &a, &b);
        if (d[a][b] > INF / 2) puts("impossible");
        else printf("%d\n", d[a][b]);
    }
    return 0;
}

2、牛的旅行 (最短路)

ACwing 1125

  • 牧区:节点
  • 牧场:节点的连通块
  • 牧场直径:连通块中最远的两个节点的距离

分析
假设牧场1的直径为 d 1 d_1 d1,牧场2的直径为 d 2 d_2 d2,将两个牧场连接起来后整个牧场的直径为 d d d

  1. d ≥ m a x ( d 1 , d 2 ) d \ge max(d_1, d_2) dmax(d1,d2),即将两个牧场连接之后对其每个牧场内部的直径没有影响,所以整个牧场的最小值一定 ≥ \ge 所有连通块直径的最大值
  2. 如何出 1 1 1中的直径:应该在牧场1、牧场2中取距离连线端点最远的点,加上这根连线所构成的直径;

求解

  1. Floyd算法求出任意两点之间的最短距离;
  2. m a x d [ i ] maxd[i] maxd[i],表示和 i i i联通的且距离 i i i最远的点的距离;
    • 情况1:连通块内部最大值 m a x d [ i ] maxd[i] maxd[i]
    • 情况2:连通块之间,枚举在与 i i i不连通的点 j j j,并求出 i 、 j i、j ij之间连边长度。 i , j i, j i,j需要满足 d [ i , j ] = I N F d[i,j]=INF d[i,j]=INF;直径 = m a x d [ i ] + d i s t [ i , j ] + m a x d [ j ] =maxd[i] + dist[i, j] + maxd[j] =maxd[i]+dist[i,j]+maxd[j]
    • 对上面两种情况取 m a x max max
  3. 对所有方案的直径取 m i n min min
#include <iostream>
#include <algorithm>
#include <cmath>

#define x first
#define y second

using namespace std;

typedef pair<double, double> PDD;

const int N = 155;
const double INF = 1e20;

int n;
PDD q[N]; // 坐标
double d[N][N]; // 距离
double maxd[N];
char g[N][N]; // 矩阵

double get_dist(PDD a, PDD b) {
    double dx = a.x - b.x;
    double dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
}

int main() {
    cin >> n;
    for (int i = 0; i < n; i++) cin >> q[i].x >> q[i].y;
    for (int i = 0; i < n; i++) cin >> g[i];

    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            if (i == j) d[i][j] = 0; // 初始化
            else if (g[i][j] == '1') d[i][j] = get_dist(q[i], q[j]); // 有边
            else d[i][j] = INF; // 无边

    for (int k = 0; k < n; k++)
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);

    double r1 = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++)
            if (d[i][j] < INF / 2) // 连通
                maxd[i] = max(maxd[i], d[i][j]);
        r1 = max(r1, maxd[i]);
    }

    double r2 = INF;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            if (d[i][j] > INF / 2) // 不连通
                r2 = min(r2, maxd[i] + maxd[j] + get_dist(q[i], q[j]));

    printf("%.6lf\n", max(r1, r2));

    return 0;
}

3、排序 (传递闭包)

ACwing 343

传递闭包
将图中所有能够间接到达的点都连上一条边,这样的图称为传递闭包。比如图中存在三个点a、b、c,且a → b、b → c,那么可以添加连线a → c

Floyd算法可以时间复杂度为 O ( n 3 ) O(n^3) O(n3)内,将一个图变成传递闭包。比如:假设存在无权图 g ( i , j ) g(i,j) g(i,j),规定图中 { g ( i , j ) = 1 , i → j 存在边 g ( i , j ) = 0 , i → j 不存在边 \begin{cases} g(i,j)=1, \quad i → j存在边\\ g(i,j)=0, \quad i → j不存在边\\ \end{cases} {g(i,j)=1,ij存在边g(i,j)=0,ij不存在边将图 g ( i , j ) g(i,j) g(i,j)变成传递闭包,计算步骤:

  1. 初始化: d ( i , j ) = g ( i , j ) d(i,j) = g(i,j) d(i,j)=g(i,j)
  2. 遍历图 d ( i , j ) d(i,j) d(i,j)
    for k
    	for i	
    		for j
     			if (d(i,k) && d(k,j))
    				d(i,j) = 1
    

本题思路
先求闭包,然后判断 t y p e type type

  • 矛盾 t y p e = 2 type = 2 type=2 d ( i , i ) = 1 d(i,i) = 1 d(i,i)=1,结束当前循环
  • 唯一确定 t y p e = 1 type = 1 type=1:当 i ≠ j i \ne j i=j时, d ( i , j ) 、 d ( j , i ) d(i,j)、d(j,i) d(i,j)d(j,i)当中必有一个为 1 1 1,结束当前循环
  • 没有矛盾,也不能唯一确定,即顺序不唯一, t y p e = 0 type = 0 type=0 继续执行

若唯一确定,如何排序?找到当前没有被标记的数(即为最小数),标记并输出。

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 26;

int n, m;
bool g[N][N], d[N][N]; // 边g,传递闭包d
bool st[N];

void floyd() {
    memcpy(d, g, sizeof d);
    for (int k = 0; k < n; k++)
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                d[i][j] |= d[i][k] && d[k][j];
}

int check() {
    for (int i = 0; i < n; i++) // 判断是否存在矛盾
        if (d[i][i]) return 2;
    for (int i = 0; i < n; i++) // 能否唯一确定
        for (int j = 0; j < i; j++)
            if (!d[i][j] && !d[j][i]) return 0;
    return 1;
}

char get_min() {
    for (int i = 0; i < n; i++)
        if (!st[i]) {
            bool flag = true;
            for (int j = 0; j < n; j++) // 是否没有任何一个元素小于它
                if (!st[j] && d[j][i]) {
                    flag = false; break;
                }
            if (flag) {
                st[i] = true; return 'A' + i;
            }
        }
}

int main() {
    while (cin >> n >> m, n || m) {
        memset(g, 0, sizeof g);
        int type = 0, t;
        for (int i = 1; i <= m; i++) {
            char str[5];
            cin >> str;
            int a = str[0] - 'A', b = str[2] - 'A';

            if (!type) { // 结果不确定
                g[a][b] = 1;
                floyd();
                type = check();
                if (type) t = i;
            }
        }
        
        if (!type) puts("Sorted sequence cannot be determined.");
        else if (type == 2) printf("Inconsistency found after %d relations.\n", t);
        else {
            memset(st, 0, sizeof st);
            printf("Sorted sequence determined after %d relations: ", t);
            for (int i = 0; i < n; i++) // 从小到大输出每个字符
                printf("%c", get_min());
            printf(".\n");
        }
    }
    return 0;
}

4、观光之旅 (最小环问题)

ACwing 344

FLoyd很容易找到包含两个点的环。

将所环按环上编号最大的点来分类,可以分成n类,我们只需要求出每一类,记为第 k k k类的最小值,最后取一个min即可。

对于每一类,我们可以枚举所有的 ( i , j ) (i,j) (i,j),每一个环可以表示成如下图。又因为 ( i , k ) 、 ( k , j ) (i,k)、(k,j) (i,k)(k,j)两条边权重是已知的,分别为 w ( i , k ) 、 w ( k , j ) w(i,k)、w(k,j) w(i,k)w(k,j)。而 ( i , j ) (i,j) (i,j)只能经过编号为 [ 1 , k − 1 ] [1,k-1] [1,k1]的结点,即为 d ( i , j ) d(i,j) d(i,j),第 k k k类的最小值为 d ( i , j ) + w ( i , k ) + w ( k , j ) d(i,j)+w(i,k)+w(k,j) d(i,j)+w(i,k)+w(k,j)
在这里插入图片描述
最后将每一类 k ∈ [ 1 , n ] k \in[1,n] k[1,n]取一个 m i n min min即可。

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110, INF = 0x3f3f3f3f;

int n, m; // 点数和边数
int d[N][N], g[N][N]; // d:floyd数组 g:每条边的长度
int pos[N][N]; // 记录每个点从哪一个状态转移过来
int path[N], cnt; // 当前最小环的方案,cnt方案数量

// 从i走到j的路径
void get_path(int i, int j) {
    int k = pos[i][j];
    if (k == 0) return; //如果是0,说明i,j之间不经过除i,j之外的其他点
    get_path(i, k); //i->newk
    path[cnt++] = k;
    get_path(k, j); //newk->j
}

int main() {
    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    for (int i = 1; i <= n; i++) g[i][i] = 0;

    while (m--) {
        int a, b, c; cin >> a >> b >> c;
        g[a][b] = g[b][a] = min(g[a][b], c);
    }

    int res = INF;
    memcpy(d, g, sizeof d);
    for (int k = 1; k <= n; k++) {
        // 求最小环
        for (int i = 1; i < k; i++) // k是环上最大的点,只需要枚举到k即可
            for (int j = i + 1; j < k; j++) // 图是无向图,i、j互换对图没影响,只需枚举一半
                if ((long long) d[i][j] + g[j][k] + g[k][i] < res) {
                    res = d[i][j] + g[j][k] + g[k][i];
                    cnt = 0;
                    path[cnt++] = k; path[cnt++] = i;
                    get_path(i, j); // k→i→j→k
                    path[cnt++] = j;
                }

        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                if (d[i][j] > d[i][k] + d[k][j]) {
                    d[i][j] = d[i][k] + d[k][j];
                    pos[i][j] = k;
                }
    }

    if (res == INF) puts("No solution.");
    else {
        for (int i = 0; i < cnt; i++) cout << path[i] << ' ';
        cout << endl;
    }

    return 0;
}

5、牛站 (恰好经过N条边的最短路)

ACwing 345

基于倍增思想

d ( k , i , j ) d(k,i,j) d(k,i,j):从 i i i j j j,恰好经过 k k k条边的最短路径

d ( a + b , i , j ) d(a+b,i,j) d(a+b,i,j):从 i i i j j j,恰好经过 a + b a+b a+b条边的最短路径,枚举中间点 k ∈ [ 1 , n ] k \in [1, n] k[1,n],表示从 i i i出发恰好经过 a a a条边之后的点k,然后在恰好经过 b b b条边到 j j j,则有 d ( a + b , i , j ) = m i n ( d ( a , i , k ) , d ( b , k , j ) ) d(a+b,i,j)=min(d(a,i,k),d(b,k,j)) d(a+b,i,j)=min(d(a,i,k),d(b,k,j))显然前面 a a a条边和后面 b b b条边无关联,所以要去取最短路径就需要在前后分别取最小值,最后对 k k k求取遍 k ∈ [ 1 , n ] k \in [1,n] k[1,n]的最小值即可。

#include <cstring>
#include <iostream>
#include <algorithm>
#include <map>

using namespace std;

const int N = 210;

int k, n, m, S, E;
int g[N][N]; // 每两个点之间的距离
int res[N][N];

// 变种FLoyd算法
void mul(int c[][N], int a[][N], int b[][N]) {
    static int temp[N][N]; // 防止c与a、b冲突
    memset(temp, 0x3f, sizeof temp);
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                temp[i][j] = min(temp[i][j], a[i][k] + b[k][j]);
    memcpy(c, temp, sizeof temp);
}

void qmi() {
    memset(res, 0x3f, sizeof res);
    for (int i = 1; i <= n; i++) res[i][i] = 0; // 初始化

    while (k) {
        if (k & 1) mul(res, res, g);    // res = res * g
        mul(g, g, g);   // g = g * g
        k >>= 1;
    }
}

int main() {
    cin >> k >> m >> S >> E;

    memset(g, 0x3f, sizeof g);
    map<int, int> ids; // 离散化:存储当前已经存在的所有点
    if (!ids.count(S)) ids[S] = ++n;
    if (!ids.count(E)) ids[E] = ++n;
    S = ids[S], E = ids[E];

    while (m--) {
        int a, b, c;
        cin >> c >> a >> b;
        if (!ids.count(a)) ids[a] = ++n; // 如果a不存在,给予一个唯一编号
        if (!ids.count(b)) ids[b] = ++n; // 如果b不存在,给予一个唯一编号
        a = ids[a], b = ids[b]; // 将a、b变成离散化之后的结果

        g[a][b] = g[b][a] = min(g[a][b], c);
    }

    // 快速幂
    qmi();

    cout << res[S][E] << endl;

    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值