反图 Dijkstra 求到终点的最短路作为 A* 启发函数,按 f=g+h 扩展,第 K 次弹出终点即第 K 短路。
OJ: roj
题目 ID: 3071
难度:提高
标签:搜索A*dijkstra最短路
创建: 2026-10-01 14:01
更新: 2026-10-07 13:50
形式化题目
给定一张
把所有从 -1。
样例:
digraph sample {
rankdir=LR;
node [shape=circle, fontsize=14];
1 -> 2 [label="5", fontsize=12];
2 -> 1 [label="4", fontsize=12];
}
样例只有两条边
正解
思路
朴素想法为什么行不通。 把"从
瓶颈在哪。 仍然可以用堆做 best-first 搜索(每次取出当前边权和
A:给搜索加一个"剩余距离"的估计。* 对每个点
它可以在反图上从
即"已走代价 + 到终点的估计剩余代价"。这样堆顶总是"整体上最有希望成为下一条短路"的路径,搜索被集中到真正可能进入前
这个估计不会改变答案的顺序,原因有两层(边权全正是前提):
- 可采纳:
是真实剩余代价的下界,所以任何路径被弹出时的 不小于它的真实长度——弹出 的 (此时 )序列不会"提前"出现比真实第 短路更小的值。 - 一致:对每条边
有 (最短路满足三角不等式),于是 沿路径单调不减。这保证第 次弹出 时,它的 恰好就是第 短路的长度——不会漏、不会乱序,也无需对同一状态重复展开。
归纳证明的骨架:设当前堆顶为
两个工程细节:
- 若
( 沿有向边根本到不了 ),扩展它永远产不出答案,直接不压堆; 时整体无解,输出 -1。 时堆里初始就有 ,这个 的"空路径"不含任何边,题目要求路径至少一条边,所以命中终点时要求 。
用样例把 A* 的过程走一遍(起点
| 步骤 | 弹出 |
说明 | 新压入的条目 |
|---|---|---|---|
| 1 | 初始化 | — | |
| 2 | 非终点,扩展边 |
||
| 3 | 第 1 次弹出终点, |
||
| 4 | 第 2 次弹出终点, |
— |
第 3 步弹出的路径是
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-07-05 21:47
# update_at: 2026-07-05 21:47
import sys
import heapq
from collections.abc import Iterator
def h_values(t: int, radj: list[list[tuple[int, int]]]) -> list[int | None]:
"""反图 Dijkstra:h[v] = v 到终点 t 的最短路,作为 A* 的启发函数。"""
h: list[int | None] = [None] * len(radj)
h[t] = 0
pq = [(0, t)]
while pq:
d, u = heapq.heappop(pq)
if h[u] != d: # 过期堆条目
continue
for v, w in radj[u]:
if h[v] is None or d + w < h[v]:
h[v] = d + w
heapq.heappush(pq, (h[v], v))
return h
def kth_walks(s: int, t: int, k: int, adj: list[list[tuple[int, int]]],
h: list[int | None]) -> Iterator[int]:
"""A* 依次产出从 s 走到 t 的各条路径长度(按不降序,含重复),最多产出 k 条。
堆条目 (f, g, u):f = g + h[u]。h 可采纳且一致(边权满足三角不等式),
所以第 cnt 次弹出 t 时,g 恰好是第 cnt 短的路长。
"""
cnt = 0
heap = [(h[s], 0, s)]
while heap:
f, g, u = heapq.heappop(heap)
if u == t and g > 0: # g>0 排除 s==t 时的零边平凡"路径"(题目要求至少一条边)
cnt += 1
yield g
if cnt == k:
return
for v, w in adj[u]:
if h[v] is not None: # v 到不了 t,扩展它永远不会到达答案
heapq.heappush(heap, (g + w + h[v], g + w, v))
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
adj = [[] for _ in range(n + 1)] # 正图:沿路径方向走
radj = [[] for _ in range(n + 1)] # 反图:从终点倒着求 h
for _ in range(m):
a, b, l = next(data), next(data), next(data)
adj[a].append((b, l))
radj[b].append((a, l))
s, t, k = next(data), next(data), next(data)
h = h_values(t, radj)
if h[s] is None: # 起点连一条到终点的路都没有,任何条数的路都不存在
print(-1)
return
found = list(kth_walks(s, t, k, adj, h)) # 生成器最多产出 k 条,取尽即停
print(found[-1] if len(found) == k else -1)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 17:16
* update_at: 2026-10-06 17:16
*/
#include <cstdio>
#include <queue>
#include <vector>
#include <utility>
#include <functional>
using namespace std;
typedef long long ll;
const int MAXN = 1005; // 点数上界(N <= 1000)
const ll INF = 1000000000000000000LL; // 最短路无穷大标记
struct Edge {
int to; // 边的另一端点
ll w; // 边权
};
vector<Edge> g[MAXN]; // 正图:g[u] 保存 u 的全部出边
vector<Edge> rg[MAXN]; // 反图:rg[v] 保存指向 v 的边,用于从终点倒推 h
ll h[MAXN]; // h[v] = v 到终点 T 的最短路长度(A* 启发函数),INF 表示到不了终点
// A* 堆中的一条部分路径:f = g + h[u]
struct Node {
ll f; // 估价 f = g + h[u]
ll g; // 从起点走到 u 已经花费的边权和
int u; // 当前所在点
};
// 小根堆比较:f 小的先弹出;f 相同时 g 小的先弹出
struct CmpNode {
bool operator()(const Node &a, const Node &b) const {
if (a.f != b.f) return a.f > b.f;
return a.g > b.g;
}
};
int n, m; // 点数、边数
int s, t, k; // 起点、终点、第 K 短路
// 在反图上从终点 T 跑 Dijkstra,求出每个点到 T 的最短路,作为启发函数 h
void dijkstra_reverse() {
for (int i = 1; i <= n; i++) h[i] = INF;
priority_queue<pair<ll, int>, vector<pair<ll, int> >, greater<pair<ll, int> > > pq;
h[t] = 0;
pq.push(make_pair(0, t));
while (!pq.empty()) {
pair<ll, int> top = pq.top();
pq.pop();
ll d = top.first;
int u = top.second;
if (d != h[u]) continue; // 过期的堆条目
int deg = rg[u].size();
for (int i = 0; i < deg; i++) {
int v = rg[u][i].to;
ll w = rg[u][i].w;
if (h[u] + w < h[v]) { // u 到 T 的这段最短路可以更新 v
h[v] = h[u] + w;
pq.push(make_pair(h[v], v));
}
}
}
}
// A* 依次弹出终点:第 cnt 次弹出终点时 g 恰为第 cnt 短路长度;不足 K 条返回 -1
ll kth_shortest() {
priority_queue<Node, vector<Node>, CmpNode> pq;
Node start;
start.f = h[s];
start.g = 0;
start.u = s;
pq.push(start);
int cnt = 0; // 已经弹出终点的次数
while (!pq.empty()) {
Node cur = pq.top();
pq.pop();
if (cur.u == t && cur.g > 0) { // g = 0 是 S = T 时的零边空路径,题目要求至少一条边
cnt++;
if (cnt == k) return cur.g;
}
int deg = g[cur.u].size();
for (int i = 0; i < deg; i++) {
int v = g[cur.u][i].to;
ll w = g[cur.u][i].w;
if (h[v] == INF) continue; // v 到不了终点,扩展它永远得不到答案
Node nxt;
nxt.g = cur.g + w;
nxt.f = nxt.g + h[v];
nxt.u = v;
pq.push(nxt);
}
}
return -1; // 堆空说明不足 K 条路
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i++) {
int a, b;
ll l;
scanf("%d%d%lld", &a, &b, &l);
Edge e;
e.to = b;
e.w = l;
g[a].push_back(e);
e.to = a; // 反图上边由 b 指向 a
rg[b].push_back(e);
}
scanf("%d%d%d", &s, &t, &k);
dijkstra_reverse();
if (h[s] == INF) { // 起点连一条到终点的路都没有
printf("-1\n");
return 0;
}
printf("%lld\n", kth_shortest());
return 0;
}实现与推导一一对应:h_values 是反图 Dijkstra(预处理 kth_walks 是 A* 主循环,用生成器按序产出每条到达终点的路径长度,产出满 solve 负责读入、建正反图、-1"的收尾。
复杂度
反图 Dijkstra 为
与 C++ 正解同阶;空间同为堆中条目的
总结
- 第
短路的本质是在一棵无穷搜索树上按根到节点权求第 个终点,不能枚举、只能 best-first。 - 把排序键从"已走代价
"换成" ",其中 是反图 Dijkstra 预处理的到终点最短路,就把搜索集中在有希望的路径上。 - 边权全正使
可采纳且一致,因此第 次弹出终点时的 恰为第 短路长度;弹出 次即得答案,堆空则无解。 - 实现要点:
的点不扩展; 时排除 的零边路径;产出满 条立即停止。
图示解析
从建模到答案的主线:
有向图 (边权全正)
│ 建反图, 从 T 跑 Dijkstra
▼
h[v] = v→T 的最短路 ┌─ h 可采纳: 弹出序不提前 ─┐
│ │ h 一致: f 沿路单调不减 │
▼ └──────────┬───────────────┘
A* 堆, 键 f = g + h ──────────────────┘
│ 第 cnt 次弹出 T ⇒ g = 第 cnt 短路长度
▼
数到第 K 次 ──► 输出 g ;堆空 ──► 输出 -1上半部分是预处理:反图一次 Dijkstra 得到每个点的"剩余距离估计"