0
点赞
收藏
分享

微信扫一扫

恰好经过K条边最短路——倍增算法的巧妙运用

青鸾惊鸿 2022-04-25 阅读 34
c++算法

题目描述

给定一张由 n n n 个点, m m m 条边构成的无向图,求从起点 s s s 到终点 e e e 恰好经过 k k k 条边(可以重复经过)的最短路。
数据保证一定有解。

输入格式

1 1 1 行:包含五个整数 n , m , k , s , e n, m, k, s, e n,m,k,s,e
2 ∼ m + 1 2 \sim m + 1 2m+1 行:每行包含三个整数,描述一条边的边长以及构成边的两个点的编号。

输出格式

输出一个整数,表示最短路的长度。

输入样例

3 3 2 1 3
1 3 3
1 2 2
2 3 2

输出样例

4

数据范围

对于全部的数据 1 ≤ n , m ≤ 100 1 \leq n, m \leq 100 1n,m100 1 ≤ k ≤ 1 0 9 1 \leq k \leq 10^9 1k109


题目解答

经过 k k k 条边,不难想到可以使用 BellMan-Ford \text{BellMan-Ford} BellMan-Ford 做,但可惜 k k k 太大,根本过不去。那么,不妨在它的递推公式上做点手脚:
d p i , j = min ⁡ 1 ≤ k ≤ n { d p i − 1 , k + d i s k , j } dp_{i, j} = \min_{1 \leq k \leq n}\{dp_{i - 1, k} + dis_{k, j}\} dpi,j=1knmin{dpi1,k+disk,j}

  • 在上式中, d p i , j dp_{i, j} dpi,j 代表的就是从起点经过 i i i 条边到达点 j j j 的最短路, d i s k , j dis_{k, j} disk,j 表示从点 k k k 到达点 j j j 的这条边的权值

可是 i i i 的值最大会到 1 0 9 10^9 109,数组根本开不下,所以我们这样考虑:

  • 刚刚的式子我们可以把它理解为是一步一步的算经过的边数,相当于是将经过 k k k 条边拆分成 k k k 个 “1” 的转移来完成:
    k = 1 + 1 + 1 + ⋯ + 1 ⏞ k k = \overbrace{1 + 1 + 1 + \cdots + 1}^{k} k=1+1+1++1 k
  • 那么我们不妨让 k k k 的拆分少一点,将它变成多个形如 2 t 2^t 2t 的数相加:
    k = 2 t 1 + 2 t 2 + ⋯ + 2 t x ⏞ ⌊ log ⁡ 2 k ⌋ + 1 k = \overbrace{2^{t_{1}} + 2^{t_{2}} + \cdots + 2^{t_{x}}}^{\lfloor \log_2k \rfloor + 1} k=2t1+2t2++2tx log2k+1
  • 欸,如果我们能做到这样的转移,那么只需要迭代 ⌊ log ⁡ 2 k ⌋ + 1 \lfloor \log_2k \rfloor + 1 log2k+1 次就可以了,将 BellManFord \text{BellManFord} BellManFord 的转移式子变一下:
    d p i , j = min ⁡ 1 ≤ k ≤ n { d p i − 2 t , k + d i s k , j , t } dp_{i, j} = \min_{1 \leq k \leq n}\{dp_{i - 2^t, k} + dis_{k, j, t}\} dpi,j=1knmin{dpi2t,k+disk,j,t}

我们暂且不管如何求 d i s dis dis 数组,可以直接得到一份更快的转移代码:

long long dp[105], tmp[105]; // 注意开long long
// 借助dp与tmp数组进行滚动数组优化
memset(tmp, 0x3f, sizeof tmp);
tmp[s] = 0; // 初始化起点
for (int t = 0; t <= __lg(k); t++) // 这里一般使用__lg函数
	if (k & (1 << t)) // 如果k能拆出一个2^i
	{
		memset(dp, 0x3f, sizeof dp);
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++)
				dp[j] = min(dp[j], tmp[i] + dis[i][j][t]); // 状态转移
		memcpy(tmp, dp, sizeof dp);
	}
  • 时间复杂度 O ( n 2 log ⁡ k ) O(n^2 \log k) O(n2logk)

不难发现,我们正常存图的邻接矩阵,恰好就是经过 1 1 1 条边的最短路,也就是 d i s i , j , 0 dis_{i, j, 0} disi,j,0。那么经过两条边的 d i s i , j , 1 dis_{i, j, 1} disi,j,1 就可以分成两个经过 1 1 1 条边的最短路和并得到:
d i s i , j , 1 = min ⁡ 1 ≤ k ≤ n { d i s i , k , 0 + d i s k , j , 0 } dis_{i, j, 1} = \min_{1 \leq k \leq n}\{dis_{i, k, 0} + dis_{k, j, 0}\} disi,j,1=1knmin{disi,k,0+disk,j,0}

  • 继续想下去, d i s i , j , t dis_{i, j, t} disi,j,t 就可以通过两段经过 2 t − 1 2^{t - 1} 2t1 条边的最短路和并得到( 2 t = 2 t − 1 + 2 t − 1 2^t = 2^{t - 1} + 2^{t - 1} 2t=2t1+2t1):
    d i s i , j , t = min ⁡ 1 ≤ k ≤ n { d i s i , k , t − 1 + d i s k , j , t − 1 } dis_{i, j, t} = \min_{1 \leq k \leq n}\{dis_{i, k, t - 1} + dis_{k, j, t - 1}\} disi,j,t=1knmin{disi,k,t1+disk,j,t1}

(是不是跟 Floyd \text{Floyd} Floyd 有着几分神似)

long long dis[105][105][30];
memset(dis, 0x3f, sizeof dis);
for (int i = 1; i <= m; i++)
{
	long long u, v, w;
	scanf("%lld %lld %lld", &u, &v, &w);
	dis[u][v][0] = dis[v][u][0] = w; // 更新dis[i][j][0]
}
for (int t = 1; t <= __lg(k); t++)
	for (int k = 1; k <= n; k++)
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++)
				dis[i][j][t] = min(dis[i][j][t], dis[i][k][t - 1] + dis[k][j][t - 1]);
  • 时间复杂度 O ( n 3 log ⁡ k ) O(n^3 \log k) O(n3logk)
    这样,一份完整的 AC 代码就新鲜出炉了->:

AC代码

#include <bits/stdc++.h>
using namespace std;
long long n, m, k, s, e, dis[105][105][30], dp[105], tmp[105];

int main()
{
    scanf("%lld %lld %lld %lld %lld", &n, &m, &k, &s, &e);
    memset(dis, 0x3f, sizeof dis);
    for (int i = 1; i <= m; i++)
    {
        long long u, v, w;
        scanf("%lld %lld %lld", &u, &v, &w);
        dis[u][v][0] = dis[v][u][0] = w;
    }
    for (int t = 1; t <= __lg(k); t++)
        for (int k = 1; k <= n; k++)
            for (int i = 1; i <= n; i++)
                for (int j = 1; j <= n; j++)
                    dis[i][j][t] = min(dis[i][j][t], dis[i][k][t - 1] + dis[k][j][t - 1]);
    memset(tmp, 0x3f, sizeof tmp);
    tmp[s] = 0;
    for (int t = 0; t <= __lg(k); t++)
        if (k & (1 << t))
        {
            memset(dp, 0x3f, sizeof dp);
            for (int i = 1; i <= n; i++)
                for (int j = 1; j <= n; j++)
                    dp[j] = min(dp[j], tmp[i] + dis[i][j][t]);
            memcpy(tmp, dp, sizeof dp);
        }
    printf("%lld", dp[e]);
    return 0;
}

将一步一步往前跳转化成一步 2 x 2^x 2x 往前跳的思想,被我们称作倍增
不仅在图论里,倍增算法还可以用来做很多事情,大名鼎鼎的 ST 表就是以倍增的思想为基础实现的。

例题:
洛谷P1613 跑路
洛谷P3509 ZAB-FROG
洛谷P6569 魔法值

本期博客就到这里,若注解有误,还请各位大佬多多指教。
另外觉得写得好的话,还可以点赞+收藏哦 ^ ⌣ ^ \hat{}\smile\hat{} ^^

举报

相关推荐

0 条评论