AcWing 178 第K短路
\(AcWing\) \(178\) 第\(K\)短路
一、题目大意
给定一张 \(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;
}