在求解 k 短路问题时,令 h(x) 为从当前结点到达终点 t 的最短路径长度。可以通过在反向图上对结点 t 跑单源最短路预处理出每个结点的这个值。对于每个状态需要记录两个值,即当前到达的结点 x 和已经走过的距离 g(x),将这种状态记为 (x,g(x))。开始时,将初始状态 (s,0) 加入优先队列。每次取出估价函数 f(x)=g(x)+h(x) 最小的一个状态,枚举该状态所在结点 x 的所有出边,将对应的后继状态加入优先队列。当访问到一个结点第 k 次时,对应状态的 g(x) 就是从起始结点 s 到该结点的第 k 短路的长度。
这一搜索过程可以优化。由于只需要求出从初始结点到目标结点的第 k 短路,所以已经取出的状态到达一个结点的次数大于 k 次时,可以不扩展其后继状态。这一状态不会影响到最后的答案。这是因为之前 k 次取出该结点时,已经形成了到达该结点的 k 条合法路径,足以构造到达目标结点的前 k 条最短路。
若使用优先队列优化 Dijkstra 算法,由于至多会将所有边加入优先队列 k 次,所以算法的时间复杂度是 O(kmlogkm) 的,空间复杂度是 O(km) 的。相较于直接搜索,A* 算法针对目标结点 t 进行了剪枝,但这仅仅改良了常数,而非渐近复杂度。本节所述算法虽然复杂度并不优秀,但是可以在相同的复杂度内求出从起始点 s 到(以 t 为根的最短路树中)每个结点的前 k 短路。
// Submission: https://judge.yosupo.jp/submission/311622#include <iostream>#include <queue>#include <vector>constexpr long long inf = 0x3f3f3f3f3f3f3f3f;struct Edge { int u, v, c; Edge(int u, int v, int c) : u(u), v(v), c(c) {}};int n, m, s, t, k;std::vector<std::vector<int>> gr, ig; // Graph and inverse graph.std::vector<Edge> edges; // Edges.std::priority_queue<std::pair<long long, int>, std::vector<std::pair<long long, int>>, std::greater<>> pq;std::vector<long long> dist_t;// Calculate distances to the destination node t. (Dijkstra)void calc_distances_to_t() { dist_t.assign(n, inf); dist_t[t] = 0; pq.emplace(dist_t[t], t); while (!pq.empty()) { long long di = pq.top().first; int cur = pq.top().second; pq.pop(); if (di > dist_t[cur]) continue; for (auto e : ig[cur]) { int nxt = edges[e].u; auto di_nxt = di + edges[e].c; if (di_nxt < dist_t[nxt]) { dist_t[nxt] = di_nxt; pq.emplace(di_nxt, nxt); } } }}std::vector<long long> ans;// Find the k shortest walk. (A* search)// Complexity: O(k * n * log(k * n)).void find_k_shortest_walks() { ans.assign(k, -1); std::vector<int> cnt(n); pq.emplace(dist_t[s], s); while (!pq.empty()) { long long cost = pq.top().first; int cur = pq.top().second; pq.pop(); // Skip unreachable nodes. if (cost >= inf) continue; if (cur == t) ans[cnt[t]] = cost; ++cnt[cur]; // Terminate when the destination has been visited k times. if (cnt[t] >= k) break; // Expand the same node at most k times. if (cnt[cur] > k) continue; for (auto e : gr[cur]) { int nxt = edges[e].v; auto cost_nxt = cost - dist_t[cur] + edges[e].c + dist_t[nxt]; pq.emplace(cost_nxt, nxt); } }}int main() { // Input. std::cin >> n >> m >> s >> t >> k; gr.resize(n); ig.resize(n); edges.reserve(m); for (int i = 0; i < m; ++i) { int u, v, c; std::cin >> u >> v >> c; edges.emplace_back(u, v, c); gr[u].push_back(i); ig[v].push_back(i); } // Calculate. calc_distances_to_t(); find_k_shortest_walks(); // Output. for (auto x : ans) std::cout << x << '\n'; return 0;}
可持久化可并堆做法
前述算法实际上求出了到达所有结点的 k 短路。如果仅仅是想要求得到达给定目标结点 t 的 k 短路,实际上可以做得更快。本节提供了一种基于可持久化可并堆的 O(mlogm+klogk) 的做法。
最短路树与偏离边
前述算法的瓶颈在于只有到达目标结点 t 时才会更新答案。但是,不同路径之间可能相差并不大。例如,次短路区别于最短路,可能仅仅是在一条边处多绕了一个结点,而路径的其他部分都是相同的;前述算法却可能需要重复搜索一遍这些相同的边才能找到次短路。由于只有绕路部分才是关键的,所以,要得到前 k 条最短的路径,只需要考虑代价最小的 k 种绕路方式即可。这就引出了最短路树的概念。
在反向图上从目标结点 t 开始跑单源最短路,记录每个结点 x 到 t 的最短路长度 h(x),并记录从结点 x 开始的最短路经过的第一条边 fx;如果有多个最优的选择,选择任意一条即可。所有这些边 fx 及其端点就构成一棵树,且从树上的每个结点 x 到根节点 t 的简单路径都是 x 到 t 的一条最短路径。这就是 最短路树T。
求得最短路树 T 后,就可以计算每条不在 T 上的边会多绕多少路。对于边 e=(u,v)∈/T,边权为 w,可以定义一条新的边,仍然从 u 指向 v,且代价为 Δ(e)=w+h(v)−h(u)。本文形象地称这些权值为 Δ(e) 的边为 偏离边(sidetrack),权值 Δ(e) 则称为偏离成本。如果一条边的端点并非全部在最短路树 T 里,它就不会影响到达结点 t 的 k 短路的计算,可以直接将它们删掉。
下图左侧是有向图 G,右侧是它对应的最短路树 T(粗边)和相应的偏离边(细边):
设一条从 s 到 t 的路径经过的边集为 P,去掉 P 中与 T 的交集得到 P′。那么,将 P′ 中的边顺次排列,它相邻的两条边 e1=(u1,v1) 和 e2=(u2,v2) 一定满足
条件 (∗):后者的起点 u2 是前者的终点 v1 在最短路树 T 的祖先(包括其自身)。
这是因为对应的原始路径 P 中,v1 和 u2 之间连接了若干条 T 中的树边。反过来,对于一个满足条件 (∗) 的边集 P′,一定存在唯一一条图 G 中的路径 P 与之对应。这是因为 v1 和 u2 在最短路树 T 上的简单路径是唯一的。这样就说明,原图中的任意路径 P 与满足条件 (∗) 的偏离边序列 P′ 一一对应。而且,路径 P 的长度就等于最短路长度 h(s) 与这些偏离成本的和:
h(s)+e∈P′∑Δ(e).
这些讨论说明,寻找 k 短路的任务转化为寻找成本第 k 小且满足条件 (∗) 的偏离边序列 P′ 的任务。
// Submission: https://judge.yosupo.jp/submission/311623#include <algorithm>#include <iostream>#include <queue>#include <random>#include <vector>std::mt19937_64 rng(static_cast<std::mt19937_64::result_type>(time(nullptr)));constexpr long long inf = 0x3f3f3f3f3f3f3f3f;// Persistent Randomized Heap.struct PersistentRandomizedHeap { static constexpr int N = 1e7; int id; std::vector<int> rt, lc, rc, to; std::vector<long long> va; int new_node(long long cost, int _to) { ++id; va[id] = cost; to[id] = _to; return id; } int copy_node(int x) { ++id; lc[id] = lc[x]; rc[id] = rc[x]; va[id] = va[x]; to[id] = to[x]; return id; } int meld(int x, int y, std::mt19937_64::result_type rand) { if (!x || !y) return x | y; if (va[x] > va[y]) std::swap(x, y); x = copy_node(x); if (rand & 1) std::swap(lc[x], rc[x]); rc[x] = meld(rc[x], y, rand >> 1); return x; } PersistentRandomizedHeap() : id(0), rt(N), lc(N), rc(N), to(N), va(N) {} void insert(int i, long long cost, int _to) { rt[i] = meld(rt[i], new_node(cost, _to), rng()); } void merge(int i, int j) { rt[i] = meld(rt[i], rt[j], rng()); }} heaps;struct Edge { int u, v, c; Edge(int u, int v, int c) : u(u), v(v), c(c) {}};int n, m, s, t, k;std::vector<std::vector<int>> gr, ig;std::vector<Edge> edges;std::priority_queue<std::pair<long long, int>, std::vector<std::pair<long long, int>>, std::greater<>> pq;std::vector<long long> dist_t;std::vector<int> out;// Calculate distances to the destination node t. (Dijkstra)// Record the optimal outgoing edges which form the shortest path tree T.void calc_distances_to_t() { dist_t.assign(n, inf); dist_t[t] = 0; out.assign(n, -1); pq.emplace(dist_t[t], t); while (!pq.empty()) { long long di = pq.top().first; int cur = pq.top().second; pq.pop(); if (di > dist_t[cur]) continue; for (auto e : ig[cur]) { int nxt = edges[e].u; auto di_nxt = di + edges[e].c; if (di_nxt < dist_t[nxt]) { dist_t[nxt] = di_nxt; out[nxt] = e; pq.emplace(di_nxt, nxt); } } }}// Construct sidetracks and propagate them through tree T.void build_sidetracks() { // Insert all valid sidetracks into heaps. for (int i = 0; i < m; ++i) { auto edge = edges[i]; if (out[edge.u] != i && dist_t[edge.u] < inf && dist_t[edge.v] < inf) { heaps.insert(edge.u, edge.c + dist_t[edge.v] - dist_t[edge.u], edge.v); } } // Propagate sidetracks down the shortest path tree. std::queue<int> q; q.push(t); while (!q.empty()) { auto cur = q.front(); q.pop(); for (auto e : ig[cur]) { auto nxt = edges[e].u; if (out[nxt] == e) { heaps.merge(nxt, cur); q.push(nxt); } } }}std::vector<long long> ans;// Insert a non-empty heap into the priority queue.// Total cost is the heap top value adjusted by accumulated cost.void insert(int x, long long cost) { if (x) pq.emplace(cost + heaps.va[x], x);}// Find the k shortest paths in the sidetrack graph. (Dijkstra)// These correspond to the k shortest walks in the original graph.void find_k_shortest_walks() { int cnt = 0; ans.assign(k, -1); if (dist_t[s] >= inf) return; ans[cnt++] = dist_t[s]; insert(heaps.rt[s], dist_t[s]); while (!pq.empty() && cnt < k) { auto cost = pq.top().first; int cur = pq.top().second; pq.pop(); ans[cnt++] = cost; insert(heaps.lc[cur], cost - heaps.va[cur]); insert(heaps.rc[cur], cost - heaps.va[cur]); insert(heaps.rt[heaps.to[cur]], cost); }}int main() { // Input. std::cin >> n >> m >> s >> t >> k; gr.resize(n); ig.resize(n); edges.reserve(m); for (int i = 0; i < m; ++i) { int u, v, c; std::cin >> u >> v >> c; edges.emplace_back(u, v, c); gr[u].push_back(i); ig[v].push_back(i); } // Calculate. calc_distances_to_t(); build_sidetracks(); find_k_shortest_walks(); // Output. for (auto x : ans) std::cout << x << '\n'; return 0;}