作者:少儿编程乔老师

每周一算法:第K短路

题目描述

给定一张 N N N个点(编号 1 , 2 … N 1,2…N 1,2N), M M M条边的有向图,求从起点 S S S到终点 T T T的第 K K K短路的长度,路径允许重复经过点或边。

注意: 每条最短路中至少要包含一条边。

输入格式

第一行包含两个整数 N N N M M M

接下来 M M M行,每行包含三个整数 A A A B B B L L L,表示点 A A A 与点 B B B之间存在有向边,且边长为 L L L

最后一行包含三个整数 S S S T T T K K K,分别表示起点 S S S,终点 T T T和第 K K K短路。

输出格式

输出占一行,包含一个整数,表示第 K K K 短路的长度,如果第 K K K 短路不存在,则输出 − 1 -1 1

数据范围

1 ≤ S , T ≤ N ≤ 1000 1≤S,T≤N≤1000 1S,TN1000
0 ≤ M ≤ 1 0 4 0≤M≤10^4 0M104
1 ≤ K ≤ 1000 1≤K≤1000 1K1000
1 ≤ L ≤ 100 1≤L≤100 1L100

输入样例

2 2
1 2 5
2 1 4
1 2 2

输出样例

14

算法思想

对于一个图来说,起点到终点可能存在很多条路径,如果求其中最短路径的长度,可以使用图的最短路算法,例如: Dijkstra \text{Dijkstra} Dijkstra SPFA \text{SPFA} SPFA…等等。

但题目要求的是第 K K K短的路径长度,朴素的思想是在搜索状态空间时,将当前节点的所有邻边全部扩展,然后在所有路径中寻找第 K K K短的路径,这样会导致要搜索的状态空间非常庞大。因此,为了提高搜索效率,可以考虑使用 A* \text{A*} A*算法,在启发函数的作用下,只需要搜索较少的状态空间就能找到答案。

A* \text{A*} A*算法的基本思想可以参考博主的另一篇文章:每周一算法:A*(A Star)算法

启发函数

A* \text{A*} A*算法中,需要一个优先队列,优先处理综合优先级较高的节点,对于本题来说,综合优先级 f ( n ) = g ( n ) + h ( n ) f(n)=g(n)+h(n) f(n)=g(n)+h(n),其中 g ( n ) g(n) g(n)表示起点到节点 n n n的距离,那么启发函数 h ( n ) h(n) h(n)表示 n n n到终点的预计代价,那么如何求 h ( n ) h(n) h(n)呢?

对于 A* \text{A*} A*算法来说,如果 h ( n ) h(n) h(n) 始终小于等于节点 n n n到终点的代价,则 A* \text{A*} A*算法算法保证一定能够找到最短路径,因此不妨将启发函数 h ( n ) h(n) h(n)设置为节点 n n n到终点的最短距离。这样可以通过对终点反向求一次最短路,就可以得到每个节点的预计代价,也就是 h ( n ) h(n) h(n)

第K短路

A* \text{A*} A*算法中,如果终点第 1 1 1次从优先队列里取出,此时可以得到到达终点的最小代价。那么要求第 K K K小代价怎么办?其实只需要记录每个节点出队的次数,当终点第 K K K次从优先队列里取出,此时到达终点的距离就是第 K K K小代价。

算法实现

  • 使用 Dijkstra \text{Dijkstra} Dijkstra求终点到每个点的最短距离,也就是预计代价 h [ n ] h[n] h[n]
  • 将起点的综合优先级,以及起点到自己的距离和起点编号加入优先队列
  • 只要队列不空:
    • 从队列中取出一个节点 u u u
    • 将其出队次数增加 1 1 1,如果终点的出队次数刚好等于 K K K,则返回第 K K K短路的长度
    • 遍历 u u u的邻边,对每个相邻节点 v v v
      • 如果 v v v的出队次数 < K \lt K <K,则将 v v v的综合优先级、以及起点到 v v v距离和节点编号加入优先队列

代码实现

#include <iostream>
#include <queue>
#include <cstring>
#include <vector>
using namespace std;
const int N = 1005;
typedef pair<int, int> PII;
typedef pair<int, PII> PIII;
struct edge
{
    int v, w;
};
int n, m, S, T, K;
vector<edge> g[N], rg[N];
int st[N], h[N]; //启发函数,保存每个点到终点的最短距离
int cnt[N]; //求每个点出队的次数
//求终点到其它点的最短距离
void dijkstra()
{
    memset(h, 0x3f, sizeof h);
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    h[T] = 0; heap.push({0, T}); //将终点加入优先队列
    while(heap.size())
    {
        auto t = heap.top(); heap.pop();
        int u = t.second;
        if(st[u]) continue;
        st[u] = 1;
        for(auto e : rg[u])
        {
            int v = e.v, w = e.w;
            if(h[v] > h[u] + w)
            {
                h[v] = h[u] + w;
                heap.push({h[v], v});
            }
        }
    }
}
int astar()
{
    //优先队列存储{综合优先级, {到起点的距离,点的编号}}
    priority_queue<PIII, vector<PIII>, greater<PIII>> heap;
    heap.push({0 + h[S], {0, S}});
    while(heap.size())
    {
        auto t = heap.top(); heap.pop();
        int u = t.second.second, d = t.second.first;
        cnt[u] ++;
        if(cnt[T] == K) return d; //终点出队K次
        for(auto e : g[u])
        {
            int v = e.v, w = e.w;
            //如果v已经出队k次了,且astar()并没有结束, 说明从v出发找不到第k短路,就没必要让v继续入队了
            if(cnt[v] < K)
                heap.push({d + w + h[v], {d + w, v}});
        }
        
    }
    return -1;
    
}
int main()
{
    scanf("%d%d", &n, &m, &K);
    for(int i = 0; i < m; i ++)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        g[u].push_back({v, w}), rg[v].push_back({u, w}); 
    }
    scanf("%d%d%d", &S, &T, &K);
    //当起点和终点重合,由于题目要求每条最短路至少要包含一条边,所以需要让K++,保证不输出0
    if(S == T) K ++;
    //求每个点到终点的最短距离作为启发函数的值
    dijkstra();

    printf("%d\n", astar());
}

相关练习

洛谷P2901:Cow Jogging G