345. 牛站
https://www.acwing.com/problem/content/347/
给定一张由 T 条边构成的无向图,点的编号为 1∼1000之间的整数。 求从起点 S 到终点 E 恰好经过 N
条边(可以重复经过)的最短路。 注意: 数据保证一定有解。输入格式 第 1 行:包含四个整数 N,T,S,E。 第 2…T+1 行:每行包含三个整数,描述一条边的边长以及构成边的两个点的编号。
输出格式 输出一个整数,表示最短路的长度。 数据范围 2≤T≤100, 2≤N≤10^6
输入样例:
2 6 6 4
11 4 6
4 4 8
8 4 9
6 6 8
2 6 9
3 8 9
输出样例: 10
1. 问题定义
给定一个带权有向图 G=(V,E)G = (V, E)G=(V,E),求从任意点 iii 到任意点 jjj 恰好经过 k 条边的最短路径。
- 输入:图的邻接矩阵 ggg,其中 g[i][j]g[i][j]g[i][j] 表示边 i→ji \to ji→j 的权值(若无边则为 +∞+\infty+∞)。
- 输出:矩阵 resresres,其中 res[i][j]res[i][j]res[i][j] 表示从 iii到 jjj 经过恰好
k条边的最短路径长度。
2. 动态规划解法(暴力法)
(1) 状态定义
设 dp[m][i][j]dp[m][i][j]dp[m][i][j] 表示从 iii到 jjj恰好经过 m 条边的最短路径长度。
(2) 状态转移
-
初始条件:
dp[0][i][j]={0if i=j (经过 0 条边,原地不动)+∞otherwise dp[0][i][j] = \begin{cases} 0 & \text{if } i = j \ (\text{经过 0 条边,原地不动}) \\ +\infty & \text{otherwise} \end{cases} dp[0][i][j]={0+∞if i=j (经过 0 条边,原地不动)otherwise
dp[1][i][j]=g[i][j](直接走一条边)。 dp[1][i][j] = g[i][j] (直接走一条边)。dp[1][i][j]=g[i][j](直接走一条边)。 -
递推关系:
- 要计算 dp[m][i][j]dp[m][i][j]dp[m][i][j],可以枚举中转点ttt,并取最小值:
dp[m][i][j]=min1≤t≤n{dp[m−1][i][t]+g[t][j]} dp[m][i][j] = \min_{1 \leq t \leq n} \{ dp[m-1][i][t] + g[t][j] \} dp[m][i][j]=1≤t≤nmin{dp[m−1][i][t]+g[t][j]} - 即,从
i走m-1条边到t,再走 1 条边到j。
- 要计算 dp[m][i][j]dp[m][i][j]dp[m][i][j],可以枚举中转点ttt,并取最小值:
#include <iostream>
#include <cstring>
#include <map>
using namespace std;
const int N = 210;
const int INF = 0x3f3f3f3f;
int dp[N][N], g[N][N]; // dp[m][i][j] 表示经过 m 条边的最短路径
int k, n, m, S, E;
map<int, int> id;
int main() {
cin >> k >> m >> S >> E;
// 初始化邻接矩阵
memset(g, 0x3f, sizeof g);
// 离散化
if (!id.count(S)) id[S] = ++n;
if (!id.count(E)) id[E] = ++n;
S = id[S], E = id[E];
while (m--) {
int a, b, c;
cin >> c >> a >> b;
if (!id.count(a)) id[a] = ++n;
if (!id.count(b)) id[b] = ++n;
a = id[a], b = id[b];
g[a][b] = g[b][a] = min(g[a][b], c); // g存储经过一条边的最短路径长度
}
// 初始化 DP 数组
memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= n; i++) dp[i][i] = 0; // 经过 0 条边
// 暴力 DP 计算 dp[k][i][j]
for (int step = 1; step <= k; step++) {
int temp[N][N];
memset(temp, 0x3f, sizeof temp);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
for (int t = 1; t <= n; t++) {
temp[i][j] = min(temp[i][j], dp[i][t] + g[t][j]);
}
}
}
memcpy(dp, temp, sizeof temp);
}
cout << dp[S][E] << endl;
return 0;
}
为什么“恰好经过 k 条边的最短路径”具有最优子结构?
这个问题的最优子结构性质来源于路径的分解性质。具体来说,如果我们要找从 iii到 jjj 经过 恰好 kkk 条边 的最短路径,那么这条路径可以拆分成:
- 前 aaa 条边:从 iii 到某个中间点 ttt 的最短路径(经过 aaa 条边)。
- 后 bbb 条边:从 ttt 到 jjj 的最短路径(经过 bbb 条边),其中 a+b=ka + b = ka+b=k。
由于最短路径的最优性,整体最优解必然由子问题的最优解构成,这就是最优子结构的体现。
所以
dp[a+b]=dp[a]×dp[b]dp[a+b]=dp[a]×dp[b]dp[a+b]=dp[a]×dp[b]
- dp[m]dp[m]dp[m] 表示经过 m 条边的最短路径矩阵
3. 优化思路:矩阵快速幂(倍增)
(1) 状态转移的矩阵表示
-
定义矩阵乘法 C=A×BC = A \times BC=A×B:
C[i][j]=min1≤t≤n{A[i][t]+B[t][j]} C[i][j] = \min_{1 \leq t \leq n} \{ A[i][t] + B[t][j] \} C[i][j]=1≤t≤nmin{A[i][t]+B[t][j]}- 这类似于 Floyd 算法的松弛操作,但用
min和+代替了传统的+和*。
- 这类似于 Floyd 算法的松弛操作,但用
-
关键性质:
- 设 dp[m]dp[m]dp[m] 表示经过
m条边的最短路径矩阵,则:
dp[a+b]=dp[a]×dp[b] dp[a + b] = dp[a] \times dp[b] dp[a+b]=dp[a]×dp[b] - 即,走
a + b条边的最短路径 = 先走a条边,再走b条边的最短路径。
- 设 dp[m]dp[m]dp[m] 表示经过
(2) 快速幂优化
- 由于矩阵乘法满足结合律,我们可以用快速幂计算 dp[k]dp[k]dp[k]:
- 初始化 dp[1]=gdp[1] = gdp[1]=g(原图的邻接矩阵)。
- 计算 dp[k]=gkdp[k] = g^kdp[k]=gk(矩阵快速幂)。
- 例如:
- k=13k = 13k=13(二进制
1101),则:
dp[13]=dp[8]×dp[4]×dp[1] dp[13] = dp[8] \times dp[4] \times dp[1] dp[13]=dp[8]×dp[4]×dp[1] - 通过倍增(平方)计算 dp[1],dp[2],dp[4],dp[8],…dp[1], dp[2], dp[4], dp[8], \dotsdp[1],dp[2],dp[4],dp[8],…,再按需合并。
- k=13k = 13k=13(二进制
(3) 时间复杂度
- 每次矩阵乘法(
mul)需要 O(n3)O(n^3)O(n3) 时间。 - 快速幂需要 O(logk)O(\log k)O(logk) 次矩阵乘法。
- 总时间复杂度:O(n3logk)O(n^3 \log k)O(n3logk),比暴力 O(n3k)O(n^3 k)O(n3k) 快很多。
(4) 代码
链接:https://www.acwing.com/solution/content/17209/
来源:AcWing
#include<iostream>
#include<cstring>
#include<map>
using namespace std;
const int N=210;
int res[N][N],g[N][N];
int k,n,m,S,E;
map<int,int> id;
void mul(int c[][N],int a[][N],int b[][N])
{
static int temp[N][N];
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;//经过0条边
while(k)//更新的过程
{
if(k&1) mul(res,res,g);//res=res*g;根据k决定是否用当前g的结果去更新res
mul(g,g,g);//g=g*g;g的更新
k>>=1;
}
}
int main()
{
cin>>k>>m>>S>>E;//虽然点数较多,但由于边数少,所以我们实际用到的点数也很少,可以使用map来离散化来赋予
//他们唯一的编号
memset(g,0x3f,sizeof g);
//这里我们来解释一下为什么不去初始化g[i][i]=0呢?
//我们都知道在类Floyd算法中有严格的边数限制,如果出现了i->j->i的情况其实在i->i中我们是有2条边的
//要是我们初始化g[i][i]=0,那样就没边了,影响了类Floyd算法的边数限制!
if(!id.count(S)) id[S]=++n;
if(!id.count(E)) id[E]=++n;
S=id[S],E=id[E];
while(m--)
{
int a,b,c;
scanf("%d%d%d",&c,&a,&b);
if(!id.count(a)) id[a]=++n;
if(!id.count(b)) id[b]=++n;
a=id[a],b=id[b];
g[a][b]=g[b][a]=min(g[a][b],c);
}
qmi();
cout<< res[S][E] <<endl;
return 0;
}
输出这条路径的节点序列
#include <iostream>
#include <cstring>
#include <vector>
#include <map>
using namespace std;
const int N = 210;
const int INF = 0x3f3f3f3f;
int dp[N][N], g[N][N]; // dp[k][i][j] 表示恰好 k 条边的最短路径
int pre[N][N][N]; // pre[k][i][j] 表示在 k 步时,i -> j 的前驱
int k, n, m, S, E;
map<int, int> id; // 离散化节点
map<int, int> rid; // 反向映射(编号 -> 原始节点)
void recover_path(int i, int j, int steps, vector<int>& path) {
if (steps == 0) {
path.push_back(rid[i]);
return;
}
int t = pre[steps][i][j];
recover_path(i, t, steps - 1, path);
path.push_back(rid[j]);
}
int main() {
cin >> k >> m >> S >> E;
// 初始化邻接矩阵
memset(g, 0x3f, sizeof g);
memset(pre, -1, sizeof pre);
// 离散化节点
if (!id.count(S)) id[S] = ++n, rid[n] = S;
if (!id.count(E)) id[E] = ++n, rid[n] = E;
S = id[S], E = id[E];
while (m--) {
int a, b, c;
cin >> c >> a >> b;
if (!id.count(a)) id[a] = ++n, rid[n] = a;
if (!id.count(b)) id[b] = ++n, rid[n] = b;
a = id[a], b = id[b];
if (c < g[a][b]) {
g[a][b] = g[b][a] = c;
pre[1][a][b] = a; // 1 步时,a -> b 的前驱是 a
pre[1][b][a] = b; // 1 步时,b -> a 的前驱是 b
}
}
// 初始化 DP 数组
memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= n; i++) dp[i][i] = 0; // 0 条边
// 动态规划计算 dp[k][i][j]
for (int step = 1; step <= k; step++) {
int temp[N][N];
int temp_pre[N][N][N];
memset(temp, 0x3f, sizeof temp);
memset(temp_pre, -1, sizeof temp_pre);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
for (int t = 1; t <= n; t++) {
if (dp[i][t] + g[t][j] < temp[i][j]) {
temp[i][j] = dp[i][t] + g[t][j];
temp_pre[step][i][j] = t; // 记录 step 步时的前驱
}
}
}
}
memcpy(dp, temp, sizeof temp);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
pre[step][i][j] = temp_pre[step][i][j];
}
}
}
// 输出最短路径长度
cout << "最短路径长度: " << dp[S][E] << endl;
// 回溯路径
if (dp[S][E] == INF) {
cout << "无可行路径!" << endl;
} else {
vector<int> path;
recover_path(S, E, k, path);
cout << "路径: ";
for (int node : path) cout << node << " ";
cout << endl;
}
return 0;
}
1178

被折叠的 条评论
为什么被折叠?



