题目描述
给定一张由
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
2∼m+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 1≤n,m≤100, 1 ≤ k ≤ 1 0 9 1 \leq k \leq 10^9 1≤k≤109
题目解答
经过
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=1≤k≤nmin{dpi−1,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=1≤k≤nmin{dpi−2t,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=1≤k≤nmin{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}
2t−1 条边的最短路和并得到(
2
t
=
2
t
−
1
+
2
t
−
1
2^t = 2^{t - 1} + 2^{t - 1}
2t=2t−1+2t−1):
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=1≤k≤nmin{disi,k,t−1+disk,j,t−1}
(是不是跟 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{}
^⌣^