• AcWing 178 第K短路


    \(AcWing\) \(178\)\(K\)短路

    题目传送门

    A星算法详解(个人认为最详细,最通俗易懂的一个版本)

    一、题目大意

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

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

    输入格式
    第一行包含两个整数 \(N\)\(M\)

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

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

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

    二、解题思路

    \(Dijkstra\)队列中,如果我们每次找到 距离最短的一个点更新其它点 ,当这个点 出队第\(K\) 的时候,当前的距离就是到该点的 \(K\)短路。我们将这个点类推为 终点 ,则得到: 当终点出现第\(K\)次的时候,我们求得了从起点到终点的第\(K\)短路。

    我们同时设置了这么一个 估价函数

    \(F = G + H\)
    \(G\) :该点到 起点 的距离
    \(H\) :该点到 终点 的距离

    每一次进行更新距离时,同时更新 估价函数\(F\) ,使用 优先队列(堆) 维护。事实上,除了上述这种估价函数的描述以外,还有很多其他的写法,他们的本质上都是一样的。

    \(Q\):估计函数作用是什么?为什么估价函数是取每个点到终点的最短距离?

    答:相当于在搜索的过程在做 贪心 的选择,这样我们可以 优先 走那些可以 尽快搜到终点 的路。比如有\(1w\)条路,让找第\(10\)短的路,那么,我们就把最短,次短,次短的...优先整完,这样,第\(10\)短的就快找到了。

    原始版本的\(Dijkstra\)算法中,第一次出队的都是最短的路,并且,加上了\(st\)标识,这样就很快。到了第\(K\)短的路,就不敢加\(st\)限制,因为人家可以走多次。那每次都是取最短的行不行呢?其实是不行的。以上面的 链接 为例,明知道目标在墙的后面,但我们还是在南辕北辙的向左侧去扩展,虽然现在看起来 已完成距离短,但不是真的短,本质上应该是 离出发点距离 + 到目标点的最短距离 最短,才是真的最短。 

    三、暴力+\(Dijkstra\)

    #include <bits/stdc++.h>
    using namespace std;
    const int N = 1010;
    const int M = 1e4 + 10;
    
    //本题由于是有向图,并且,允许走回头路(比如有一个环存在,就可以重复走,直到第K次到达即可,这与传统的最短路不同),所以,没有st数组存在判重
    
    int n, m; // n个顶点,m条边
    int x;    //现在是第几次到达T点
    int S, T; //起点与终点
    int K;    //第K短的路线
    int cnt[N]; //记录某个点出队列的次数
    
    int h[N], w[M], e[M], ne[M], idx; // 邻接表
    int dist[N];                      //到每个点的最短距离
    void add(int a, int b, int c) {
        e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
    }
    
    struct N1 {
        int u, d;
        bool operator<(const N1 &x) const {
            return d > x.d; //谁的距离短谁靠前
        }
    };
    
    //堆优化版本Dijkstra
    void dijkstra() {
        //标准姿势小顶堆
        priority_queue<N1> q;
    
        if (S == T) K++; //如果S=T,那么一次检查到相遇就不能算数,也就是要找第K+1短路
        q.push({S, 0});
    
        while (q.size()) {          // bfs搜索
            int d = q.top().d;      //当前点和出发点的距离
            int u = q.top().u;      //当前点u
            q.pop();
            cnt[u]++; //记录u节点出队列次数
    
            if (u == T) {             //如果到达了目标点
                if (++x == K) {       //第K次到达
                    printf("%d", d);  //输出距离长度
                    return;
                }
            }
            for (int i = h[u]; ~i; i = ne[i]) {
                int j = e[i];
                /*
                如果走到一个中间点都cnt[j]>=K,则说明j已经出队>=k次了
                说明从j出发找不到第k短路(让终点出队k次),即继续让j入队的话依然无解,没必要让j继续入队
                */
                if (cnt[j] < K)
                    q.push({j, d + w[i]}); //不管长的短的,全部怼进小顶堆,不是最短路径才是正解,是所有路径都有可能成为正解!所以,这里与传统的Dijkstra明显不一样!
            }
        }
        puts("-1");
    }
    
    //通过了 6/7个数据
    //有一个点TLE,看来暴力+Dijkstra不是正解
    
    int main() {
        //加快读入
        cin.tie(0), ios::sync_with_stdio(false);
    
        //初始化邻接表
        memset(h, -1, sizeof h);
    
        //寻找第K短路,n个顶点,m条边
        cin >> n >> m;
    
        while (m--) {
            int a, b, c;
            cin >> a >> b >> c; // a->b有一条长度为c的有向边
            add(a, b, c);
        }
    
        cin >> S >> T >> K; //开始点,结束点,第K短
    
        //迪杰斯特拉
        dijkstra();
    
        return 0;
    }
    

    四、结构体+\(Dijkstra\)+\(A*\)寻路 (推荐)

    #include <bits/stdc++.h>
    
    using namespace std;
    const int INF = 0x3f3f3f3f;
    
    const int N = 1010;
    const int M = 200010;
    int n, m;
    int S, T, K;
    int h[N], rh[N];
    int e[M], w[M], ne[M], idx;
    int dist[N];
    bool st[N];
    int cnt[N];
    
    void add(int h[], int a, int b, int c) {
        e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
    }
    
    //只有点号和距离时,按距离由小到大排序
    struct N1 {
        int u, d;
        bool operator<(const N1 &x) const {
            return d > x.d; //谁的距离短谁靠前
        }
    };
    
    //当有点号+距离+估值函数时,按估值函数值由小到大排序
    struct N2 {
        int u, d, f;
        bool operator<(const N2 &x) const {
            return f > x.f;
        }
    };
    
    void dijkstra() {
        priority_queue<N1> q;
        q.push({T, 0});
    
        memset(dist, 0x3f, sizeof dist);
        dist[T] = 0;
    
        while (q.size()) {
            N1 t = q.top();
            q.pop();
            int u = t.u;
            if (st[u]) continue;
            st[u] = true;
            for (int i = rh[u]; ~i; i = ne[i]) {
                int j = e[i];
                if (dist[j] > dist[u] + w[i]) {
                    dist[j] = dist[u] + w[i];
                    q.push({j, dist[j]});
                }
            }
        }
    }
    
    int astar() {
        priority_queue<N2> q;
        q.push({S, 0, dist[S]});
        while (q.size()) {
            auto t = q.top();
            q.pop();
    
            int u = t.u;
            int d = t.d;
            cnt[u]++;
    
            if (u == T && cnt[u] == K) return d;
    
            for (int i = h[u]; ~i; i = ne[i]) {
                int j = e[i];
                if (cnt[j] < K)
                    q.push({j, d + w[i], d + w[i] + dist[j]});
            }
        }
        return -1;
    }
    
    int main() {
        cin.tie(0), ios::sync_with_stdio(false);
        cin >> n >> m;
    
        memset(h, -1, sizeof h);
        memset(rh, -1, sizeof rh);
    
        while (m--) {
            int a, b, c;
            cin >> a >> b >> c;
            add(h, a, b, c);
            add(rh, b, a, c);
        }
    
        cin >> S >> T >> K;
        if (S == T) K++;
        dijkstra();
        printf("%d\n", astar());
        return 0;
    }
    
    

    五、\(PII\)+\(Dijkstra\)+\(A*\)寻路 (不推荐)

    #include <bits/stdc++.h>
    
    using namespace std;
    const int INF = 0x3f3f3f3f;
    
    //代码太过繁琐,准备放弃PII教学,全面采用Struct,就是最开始费劲点,一旦明白后,自定义排序、多参数值等比PII灵活太多
    
    typedef pair<int, int> PII;
    typedef pair<int, PII> PIII;
    
    const int N = 1010;
    const int M = 200010;       //因为要建反向边,所以边数是2倍,20W+10
    int n, m;                   //点数和边数
    int S, T, K;                //起点,终点,第K短
    int h[N], rh[N];            //正向边的邻接表表头和反向边的邻接表表头
    int e[M], w[M], ne[M], idx; //数组模拟邻接表
    int dist[N];                //到每个点的最短距离
    bool st[N];                 //每个点用没用过
    int cnt[N];
    
    //邻接表模板
    void add(int h[], int a, int b, int c) {
        e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
    }
    
    // 计算出点T到每个其它点的最短距离,把这个最短距离dist[i]做为估价函数
    void dijkstra() {
        priority_queue<PII, vector<PII>, greater<PII>> q;
        q.push({0, T});
    
        //初始化距离
        memset(dist, 0x3f, sizeof dist);
        dist[T] = 0;
    
        while (q.size()) {
            PII t = q.top();
            q.pop();
            int u = t.second;                    //哪个点
            if (st[u]) continue;                 //如果这个点已经出过队列了,根据Dijkstra的理论,这个最小路径已经产生,不需要再讨论
            st[u] = true;                        //标识已搜索
            for (int i = rh[u]; ~i; i = ne[i]) { //在反向图上遍历
                int j = e[i];
                if (dist[j] > dist[u] + w[i]) {
                    dist[j] = dist[u] + w[i];
                    q.push({dist[j], j});
                }
            }
        }
    }
    
    // A*算法,不用判重
    int astar() {
        //小顶堆,对F值进行从小到大排序
        priority_queue<PIII, vector<PIII>, greater<PIII>> q;
        // 小顶堆内的元素:
        // PII的first: F = G + H
        // G = 起点到当前点的距离
        // H = 当前点到终点的估算成本
        // 按实际距离累加和+估算值,按这个值来确定处理优先级
        q.push({dist[S], {0, S}}); //在添加S节点时,由于S距离自己的距离是0,所以就只有一个H值,即dist[S]
        //后面数对PII的含义是:当前节点距离出发点的距离,当前节点号
    
        while (q.size()) {
            PIII t = q.top();
            q.pop();
            // t.x在下面的代码中未使用,原因很简单,这个x本来也不是啥准确值,只是一个估值
    
            int u = t.second.second; // 要扩展的点u
            int d = t.second.first;  // u 距离出发点的距离
            cnt[u]++;                // u 出队的次数+1
    
            //如果此时的u就是终点T,前且已经出队k次 返回答案
            if (u == T && cnt[u] == K) return d;
    
            for (int i = h[u]; ~i; i = ne[i]) {
                int j = e[i];
                /*
                A*寻路的一个优化:
                如果走到一个中间点都cnt[j]>=K,则说明j已经出队>=k次了,且astar()并没有return distance,
                说明从j出发找不到第k短路(让终点出队k次),即继续让j入队的话依然无解,没必要让j继续入队
    
                distance + w[i] 累加长度
                真实值 + 估计值 由小到大 排序
    
                如果当前点出队超过k次的话 用它当前权值更新的其他点必然也是大于第k次的点 所以不需要更新
                */
                if (cnt[j] < K)
                    q.push({d + w[i] + dist[j], {d + w[i], j}});
            }
        }
        // 终点没有被访问k次
        return -1;
    }
    
    int main() {
        //加快读入
        cin.tie(0), ios::sync_with_stdio(false);
        cin >> n >> m;
    
        memset(h, -1, sizeof h);   //正向边表头
        memset(rh, -1, sizeof rh); //反向边表头
    
        while (m--) {
            int a, b, c;
            cin >> a >> b >> c;
            add(h, a, b, c);  //正向建图
            add(rh, b, a, c); //反向建图
        }
    
        cin >> S >> T >> K;
        if (S == T) K++; //最短路中至少要包含一条边,如果S和T相等,那么我们不能直接从S得到T,所以起码需要走一条边,求第K+1短就可以了
    
        //先执行一遍dijkstra算法,求出终点到每个点的最短距离,作为估值
        dijkstra();
    
        // A*寻路求第K短长度
        printf("%d\n", astar());
    
        return 0;
    }
    
  • 相关阅读:
    对 Sea.js 进行配置 seajs.config
    jquery 设置style:display
    Js获取当前日期时间及其它操作
    2.4 js数组与字符串的转换 > 我的程序猿之路:第十四章
    2.3 js刷新页面所有 > 我的程序猿之路:第十三章
    2.2 HTML/JSP中控制按钮的显示和隐藏与单页面多列表 > 我的程序猿之路:第十二章
    2.1 字符串替换字符或字符设置为空 > 我的程序猿之路:第十一章
    1.9 23种设计模式之单例模式详情 > 我的程序猿之路:第九章
    1.8 Oracle 登陆时报错信息:无监听程序 > 我的程序猿之路:第八章
    1.7 Oracle 11g )impdp(数据泵)--导入dmp文件(全过程) > 我的程序猿之路:第七章
  • 原文地址:https://www.cnblogs.com/littlehb/p/15964022.html
Copyright © 2020-2023  润新知