每周一算法:恰好经过K条边的最短路

题目描述

牛站

给定一张由 MMM 条边构成的无向图,点的编号为 1∼10001\sim 100011000 之间的整数。

求从起点SSS 到终点 EEE 恰好经过 KKK 条边(可以重复经过)的最短路。

注意: 数据保证一定有解。

输入格式

111 行:包含四个整数 K,M,S,EK,M,S,EKMSE

2..M+12..M+12..M+1 行:每行包含三个整数,描述一条边的边长以及构成边的两个点的编号。

输出格式

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

样例 #1

样例输入 #1

2 6 6 4
11 4 6
4 4 8
8 4 9
6 6 8
2 6 9
3 8 9

样例输出 #1

10

提示

【数据范围】
2≤M≤1002≤M≤1002M100,
2≤K≤1062≤K≤10^62K106

算法思想

倍增 + Floyd 求状态

根据题目描述,求从起点SSS到终点EEE恰好经过KKK条边的最短路。考虑「Floyd」算法中的状态d[K,i,j]d[K,i,j]d[K,i,j]表示经过编号1∼K1\sim K1K的点进行中转、从顶点iiijjj的最短距离。本题可以用类似的状态d[K,i,j]d[K,i,j]d[K,i,j]表示为恰好经过KKK条边,从顶点iiijjj的最短距离。

要计算状态d[K,i,j]d[K,i,j]d[K,i,j],很容易想到d[K,i,j]=min{d[K−1,i,k]+g[k,j]}d[K,i,j]=min\{d[K-1, i, k]+g[ k, j]\}d[K,i,j]=min{d[K1,i,k]+g[k,j]},枚举一个中转点kkk,从K−1K-1K1阶段的状态转移到KKK。这样做的时间复杂度为K×n3K\times n^3K×n3,从数据范围来看,2≤M≤100,2≤K≤1062≤M≤100,2≤K≤10^62M1002K106,显然会TLE。

进一步分析,不妨假设K=a+bK=a+bK=a+b,那么d[K,i,j]=min{d[a,i,k]+d[b,k,j]}d[K,i,j]=min\{d[a,i,k]+d[b,k,j]\}d[K,i,j]=min{d[a,i,k]+d[b,k,j]},其中kkk表示从iii出发经过恰好aaa条边到达的顶点,1≤k≤n1\le k\le n1kn,如下图所示:
在这里插入图片描述
可以发现从顶点iii走到kkk,和从顶点kkk走到jjj,这两个部分是完全独立的,并不相互依赖,所以先求前面、或者先求后面没有任何区别。也就是说,对于路径的组合可以是任意的,结合在一起答案不变,类似于加法结合律。

基于上述分析,可以使用快速幂“倍增”的思想,依次计算出d[1,i,j]→d[2,i,j]→d[4,i,j]→...d[1,i,j]\to d[2,i,j]\to d[4,i,j]\to...d[1,i,j]d[2,i,j]d[4,i,j]...,可以将时间复杂度优化为O(logK×n3)O(logK\times n^3)O(logK×n3)

离散化点集

除此之外,从给出的数据范围来看,边数M≤200M\le200M200200200200条边最多连接400400400个点,那么就需要对所有点进行离散化,重新编号为1∼n1\sim n1n

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 205;
int g[N][N], d[N][N];
int n, m, K, S, E;
map<int, int> idx; //离散化点集
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(d, 0x3f, sizeof d);
    for(int i = 1; i <= n; i ++) d[i][i] = 0;
    
    while(K)
    {
        if(K & 1) mul(d, d, g); // d = d * g
        mul(g, g, g); //g = g * g
        K >>= 1;
    }
}
int main()
{
    cin >> K >> m >> S >> E;
    memset(g, 0x3f, sizeof g); //初始化邻接矩阵
    //离散化起点和终点,重新分配编号
    idx[S] = ++ n; idx[E] = ++ n; 
    S = idx[S], E = idx[E];
    
    for(int i = 0; i < m; i ++)
    {
        int a, b, c;
        cin >> c >> a >> b; //注意输入顺序
        //将点离散化,重新分配编号
        if(!idx.count(a)) idx[a] = ++ n;
        if(!idx.count(b)) idx[b] = ++ n;
        a = idx[a], b = idx[b]; 
        g[a][b] = g[b][a] = min(g[a][b], c);
    }
    
    qmi(); //快速幂,倍增求状态
    
    cout << d[S][E] << endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值