AcWing 345 牛站
牛站
一、题目描述
给定一张由 条边构成的无向图,点的编号为 ∼ 之间的整数。
求从起点 到终点 恰好 经过 条边(可以重复经过)的 最短路。
注意: 数据保证一定有解
输入格式
第 行:包含四个整数 ,,,。
第 行:每行包含三个整数,描述一条边的边长以及构成边的两个点的编号。
输出格式
输出一个整数,表示最短路的长度
数据范围
输入样例:
2 6 6 4
11 4 6
4 4 8
8 4 9
6 6 8
2 6 9
3 8 9
输出样例:
10
二、前导知识
- 图中任意两点间路径数量
~
- 图中两点路径为的方案数
[] 迷路 这道题放这不太合适,因为还有拆点,有点难度~
三、题目解析
本题并不是让我们求路径的条数,而是求 在路径条数限定的情况下,求最短路径长度
1. 之间两条边
设是图的邻接矩阵,,两个点,如果之间有两条边,那么中间必然只有一个点,设为,的范围是:
状态表示
:从恰好经过 两条边 到达的 最短路径长度
状态计算
那么求两点之间插入第三个点的最短路径代码为:
for(int k = 1;k <= n;k++)
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
t[i][j] = min(t[i][j],g[i][k] + g[k][j]);
这被称作:广义矩阵乘法
下面的转化需要一点 矩阵乘法 的知识:
矩阵乘法栗子:
令矩阵 为一个 的矩阵:
a = [ 2, 3 ]
[ 4, 1 ]
令矩阵 为一个 的矩阵:
b = [ 5, 1, 2 ]
[ 2, 3, 4 ]
求解 :
设
矩阵 的维度为 。
t = [ (2×5 + 3×2), (2×1 + 3×3), (2×2 + 3×4) ]
[ (4×5 + 1×2), (4×1 + 1×3), (4×2 + 1×4) ]
注:运算规则就是矩阵第行的所有数据,对应,乘以矩阵第列的所的数据,累加和 放到矩阵的第行第列中去,其它以此类推。
所以,得到的结果矩阵 为:
t = [ 14, 11, 14 ]
[ 22, 7, 12 ]
矩阵乘法 模板 如下:
for (int k = 1; k <= COLS_A; ++k)
for (int i = 1; i <= ROWS_A; ++i)
for (int j = 1; j <= COLS_B; ++j)
t[i][j] += a[i][k] * b[k][j];
上面的最基本的矩阵乘法,现在,我们改一下需求,不再求叠加和,而是想求一下,然后把这个最小值放到里,其实也是一样的道理,这被我们称为 广义矩阵乘法,代码就是:
for(int k = 1;k <= n;k++)
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
t[i][j] = min(t[i][j],g[i][k] + g[k][j]);
我们 惊喜 的发现,这个代码与上面两点间插入第三个点求最短路径的代码是一模一样的,也就是说,我们可以理解为可以通过 类似于 乘以矩阵来达到求两点之间插入第三个点获取最短路径。
此时,噢,这里的都是,就是啊!
结论:
1、两边条,加一个点,乘
2、三边条,加两个点,乘
...
2.之间大于两条边
快速幂对广义矩阵乘法的优化
广义矩阵乘法可以求经过 两条边 到达的 最短路径 长度,则对做次 自乘 就可以求经过条边的 最短路径 长度了。
void mul(int a[][N],int b[][N]){
int t[N][N];
memset(t,0x3f,sizeof t); //预求最小,先设最大
for(int k = 1;k <= n;k++)
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
t[i][j] = min(t[i][j],a[i][k] + b[k][j]);
// sizeof 不能在此处使用,因为a数组其实是指针传入,无法获取大小
// 有两个办法可以解决:
// (1)采用全局的sizeof t进行替代 (简单)
// (2)在函数中增加大小这样一个参数,由调用者赋值传入 (麻烦)
memcpy(a,t,sizeof t);
}
void qmi(){
memcpy(f,g,sizeof f);
k--;
while(k){
if(k & 1) mul(f,g);
mul(g,g);
k >>= 1;
}
}
3. 离散化
值得注意的是点的编号最大是1000
,最多只有100
条边,也就是最多200
个节点,需要对节点编号做 离散化。
解释:算法复杂度,如果,妥妥的会挂啊!
#include <bits/stdc++.h>
using namespace std;
const int N = 205;
unordered_map<int, int> id;
int s, e; //起点 终点
int n, m; //离散化后的节点号 m条边
int k; //恰好k条边
int g[N][N]; //图
int f[N][N]; //最短距离
//矩阵乘法
void mul(int a[][N], int b[][N]) {
int t[N][N];
memset(t, 0x3f, sizeof t);
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
//求指定边数情况下的最短距离
//广义上的矩阵乘法
t[i][j] = min(t[i][j], a[i][k] + b[k][j]);
memcpy(a, t, sizeof t);
}
//快速幂
void qmi() {
// 写法1:使用一个空的矩阵,需要执行k次幂
// memset(f, 0x3f, sizeof f);
// for (int i = 1; i <= n; i++) f[i][i] = 0;
//写法2,使用一个拷贝矩阵,需要执行k-1次幂
memcpy(f, g, sizeof f);
k--;
while (k) {
if (k & 1) mul(f, g); //矩阵快速幂
mul(g, g);
k >>= 1;
}
}
// AC 481 ms
// 这个速度真是很牛X
int main() {
scanf("%d %d %d %d", &k, &m, &s, &e);
//起点的离散化后号为1
id[s] = ++n;
//如果e与s不同,那么e的新号为2,否则为1
if (!id.count(e)) id[e] = ++n;
//重新对s和e给定新的号码
s = id[s], e = id[e];
//初始化邻接矩阵
memset(g, 0x3f, sizeof g);
while (m--) {
int a, b, c;
scanf("%d %d %d", &c, &a, &b);
if (!id.count(a)) id[a] = ++n; //记录点的映射关系a-> id[a]
if (!id.count(b)) id[b] = ++n; //记录点的映射关系b-> id[b]
a = id[a], b = id[b]; //对a,b给定新的号码
//利用新的号码将边长c记录到邻接矩阵中
g[a][b] = g[b][a] = min(g[a][b], c);
}
//快速幂+动态规划思想
qmi();
//输出从起点到终点,恰好经过k条边的最短路径
printf("%d\n", f[s][e]);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!