图论 —— k 短路
【概述】
所谓 k 短路问题,是指给定一个具有 n 个点 m 条边的带正权有向图,再给定起点 S 与终点 T,询问从 S 到 T 的所有权和中,第 k 短的路径的长度。
k 短路问题的解决方法有两种,一种是利用A*算法求解,另一种是利用最短路算法与可持久化堆结合求解。
【A*算法】
对于 Dijkstra 算法,有一个结论是:当一个点第 k 次出队的时候,此时路径长度就是 s 到它的第 k 短路
但直接来写会造成 MLE,因此要利用 A* 算法来优化状态数。
首先建立反图,跑一次最短路算法,得到每个点到 t 的最短路的距离,然后用当前走的距离加上终点的最短路长度作为优化级来进行 A*
因此,当一个点第 k 次出队时,答案为这个点的优先级,当终点第 k 次出队时,答案为这个点已走的路径
struct Edge {
int to, next;
int w;
Edge() {}
Edge(int to, int next, int w) : to(to), next(next), w(w) {}
};
struct Map {
int tot;
int head[N];
Edge edge[N * N];
Map() {
tot = 0;
memset(head, -1, sizeof(head));
}
void addEdge(int x, int y, int w) {
edge[++tot].to = y;
edge[tot].next = head[x];
edge[tot].w = w;
head[x] = tot;
}
Edge &operator[](int pos) { return edge[pos]; }
};
int n, m;
Map G, GT;
int dis[N];
bool vis[N];
struct Status{
int node;//点编号
int diss;//距离
int priority;//优先级
Status() : node(0), diss(0), priority(dis[0]) {}
Status(int node, int diss) : node(node), diss(diss), priority(diss + dis[node]) {}
bool operator<(Status b) const { return priority > b.priority; }
} status;
bool SPFA(int S, int T, int k) { //对反图求最短路
memset(dis, INF, sizeof(dis));
memset(vis, false, sizeof(vis));
dis[S] = 0;
queue<int> Q;
Q.push(S);
while (!Q.empty()) {
int x = Q.front();
Q.pop();
vis[x] = false;
for (int i = GT.head[x]; i != -1; i = GT[i].next) {
int y = GT[i].to;
if (dis[x] + GT[i].w < dis[y]) {
dis[y] = dis[x] + GT[i].w;
if (!vis[y]) {
Q.push(y);
vis[y] = true;
}
}
}
}
return dis[T] != INF;
}
int label[N];
int AStar(int S, int T, int k) {
memset(label, 0, sizeof(label));
if (S == T)
k++;
priority_queue<Status> Q;
Q.push(Status(S, 0));
while (!Q.empty()) {
Status temp = Q.top();
Q.pop();
label[temp.node]++;
if (temp.node == T && label[temp.node] == k)
return temp.diss;
for (int i = G.head[temp.node]; i != -1; i = G[i].next)
if (label[G[i].to] < k)
Q.push(Status(G[i].to, temp.diss + G[i].w));
}
return -1;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y, w;
scanf("%d%d%d", &x, &y, &w);
G.addEdge(x, y, w);
GT.addEdge(y, x, w);
}
int s, t, k;
scanf("%d%d%d", &s, &t, &k);
if (!SPFA(t, s, k))
printf("-1\n");
else
printf("%d\n", AStar(s, t, k));
return 0;
}
【可持久化堆】
考虑建立反图,然后跑最短路算法得到以 T 为根的最短路径生成树,当走一条非树边 (u,v) 时,最终的路径长度就会因此增加 dis[v]-dis[u]+w
对于一条路径,我们依次将它经过的非树边记下来,约定得到的序列是这条路径的非树边序列。
考虑对于一个合法的非树边序列,我们可以找到唯一的一条 S 到 T 的路径与之对应,因此,k 短路的长度就等于第 k 小的代价和加上 S 到 T 的最短路长度。
考虑如何来得到一个合法的非树边序列:
- 找到一条起点在当前点 p 到根 t 的路径上的非树边
- 令 p 等于这条边的终点
我们可以通过这样的方法来得到所有的非树边序列,但是我们并不需要所有的非树边序列,因此当找到第 x 短路后再拓展状态,然后用优先队列来维护,但这样每次拓展时间复杂度可达 O(m),总时间复杂度可以达到 O(mklog(mk))。
这样时间复杂度太高,令人无法接受,因为其中被用到的状态十分少,由于当一个非树边序列出队时,代价和比它大的才可能有用,因此,考虑一个非树边序列出队时通过下面的方法来进行得到新的序列:
- 追加操作:假如最后一条非树边的终点为 v,找到一条起点在 v 到 t 的路径上代价最小的非树边追加在当前非树边序列后
- 替换操作:将最后一条非树边更换为代价比它大的 1 条非树边
如下图,橙色虚线是被替换掉的非树边,紫色是新加入的非树边
考虑用一些可持久化数据结构来维护起点在点 u 到根的路径上的非树边的代价,对于替换操作,当使用可持久化堆时,把最后一条非树边替换为它所在的堆中它的左右子节点代表的边
struct Node{
int val,to;
Node *left,*right;
Node(){}
Node(int val, int to, Node *left, Node *right) : val(val), to(to), left(left), right(right) {}
};
#define Limit 1000000
Node pool[Limit];
Node *top = pool;
Node *newNode(int val, int to) {
if (top >= pool + Limit)
return new Node(val, to, NULL, NULL);
top->val = val;
top->to = to;
top->left = NULL;
top->right = NULL;
return top++;
}
Node *meGTe(Node *a, Node *b) {
if (!a)
return b;
if (!b)
return a;
if (a->val > b->val)
swap(a, b);
Node *p = newNode(a->val, a->to);
p->left = a->left;
p->right = a->right;
p->right = meGTe(p->right, b);
swap(p->left, p->right);
return p;
}
struct Status {
int dist;
Node *p;
Status(){}
Status(int dist, Node *p) : dist(dist), p(p) {}
bool operator<(Status b) const { return dist > b.dist; }
};
struct Edge {
int to, next;
int w;
Edge() {}
Edge(int to, int next, int w) : to(to), next(next), w(w) {}
};
struct Map {
int tot;
int *head;
Edge *edge;
Map() {}
Map(int n, int m) : tot(0) {
head = new int[(n + 1)];
edge = new Edge[(m + 5)];
memset(head, 0, sizeof(int) * (n + 1));
}
void addEdge(int x, int y, int w) {
edge[++tot].to = y;
edge[tot].next = head[x];
edge[tot].w = w;
head[x] = tot;
}
Edge &operator[](int pos) { return edge[pos]; }
};
int n, m;
int s, t, k;
Map G, GT;
bool *vis;
int *dis, *pre;
queue<int> SPFA(int S) {
vis = new bool[(n + 1)];
dis = new int[(n + 1)];
pre = new int[(n + 1)];
memset(dis, INF, sizeof(int) * (n + 1));
memset(vis, false, sizeof(bool) * (n + 1));
queue<int> Q;
Q.push(S);
dis[S] = 0;
pre[S] = 0;
while (!Q.empty()) {
int x = Q.front();
Q.pop();
vis[x] = false;
for (int i = GT.head[x]; i; i = GT[i].next) {
int y = GT[i].to;
int w = GT[i].w;
if (dis[x] + w < dis[y]) {
dis[y] = dis[x] + w;
pre[y] = i;
if (!vis[y]) {
vis[y] = true;
Q.push(y);
}
}
}
}
return Q;
}
Node **Hash;
void rebuild(queue<int> Q) { //建堆
for (int i = 1; i <= n; i++) {
for (int j = G.head[i]; j; j = G[j].next) {
int to = G[j].to;
if (pre[i] != j)
G[j].w += dis[to] - dis[i];
}
}
Hash = new Node *[(n + 1)];
Q.push(t);
Hash[t] = NULL;
while (!Q.empty()) {
int x = Q.front();
Q.pop();
if (pre[x])
Hash[x] = Hash[G[pre[x]].to];
for (int i = G.head[x]; i; i = G[i].next)
if (pre[x] != i && dis[G[i].to] != INF)
Hash[x] = meGTe(Hash[x], new Node(G[i].w, G[i].to, NULL, NULL));
for (int i = GT.head[x]; i; i = GT[i].next) {
int y = GT[i].to;
if (pre[y] == i)
Q.push(y);
}
}
}
int kthPath(int k) {
if (s == t)
k++;
if (dis[s] == INF)
return -1;
if (k == 1)
return dis[s];
priority_queue<Status> Q;
if (!Hash[s])
return -1;
Q.push(Status(Hash[s]->val, Hash[s]));
while (--k && !Q.empty()) {
Status x = Q.top();
Q.pop();
if (k == 1)
return x.dist + dis[s];
int y = x.p->to;
if (Hash[y])
Q.push(Status(x.dist + Hash[y]->val, Hash[y]));
if (x.p->left)
Q.push(Status(x.dist - x.p->val + x.p->left->val, x.p->left));
if (x.p->right)
Q.push(Status(x.dist - x.p->val + x.p->right->val, x.p->right));
}
return -1;
}
int main() {
scanf("%d%d", &n, &m);
G = Map(n, m);
GT = Map(n, m);
for (int i = 1, u, v, w; i <= m; i++) {
scanf("%d%d%d", &u, &v, &w);
G.addEdge(u, v, w);
GT.addEdge(v, u, w);
}
scanf("%d%d%d", &s, &t, &k);
queue<int> Q = SPFA(t);
rebuild(Q);
printf("%d\n", kthPath(k));
return 0;
}
【例题】
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)