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;
}
posted @ 2022-03-04 13:57  糖豆爸爸  阅读(163)  评论(0编辑  收藏  举报
Live2D