把每个城市拆成「相对最短路超出 0/1」两层状态,跑一遍分层 Dijkstra 统计条数,答案是最短路与次短路层条数之和。
OJ: roj
题目 ID: 3209
难度:提高
标签:图论最短路单源次短路及其条数python
创建: 2026-10-02 01:57
更新: 2026-10-10 11:30
形式化题目
给定有向带权图
记
其中
因为
正解
思路
1. 朴素做法与瓶颈
最直接的做法是 DFS 枚举
dfs(u, 长度):
若 u == F: 记录长度; 返回
枚举 u 的每条出边 (u -> v, w):
若 长度 + w <= d + 1: dfs(v, 长度 + w)剪枝是对的,但复杂度是路径条数而不是
题面保证"答案不超过
2. 关键观察:超额只有 0 或 1
设
叫做它在
若整条路径
在 之后那一段是从 到 的一条真实路径,所以 ,即 ; - 从
走最短路到 、再走最短路到 是一条真实的 路径,所以 。
两式对同一个
约掉
也就是说:沿途每个点的超额只可能是 0 或 1,三层以上的信息一律不需要。
这个推导只用到
,并没有用到“路径不重复经过城市”, 所以它同样覆盖“绕了一次权为 1 的自环”这种走法。也就是说无论题目把“路线”理解为简单路径 还是允许重复的走法,只要长度在 以内,状态模型都完整地数到。 而本题的测试数据里没有自环,两种理解结果一致。
3. 建模:点拆成两层状态
把每个城市
决定;
下图是样例第 1 组的状态模型:实线是「超额不增」的转移(
flowchart LR s10["(1,0) 长0"] -->|3| s20["(2,0) 长3"] s10 -->|2| s30["(3,0) 长2"] s10 -.->|5| s41["(4,1) 长5"] s30 -->|2| s40["(4,0) 长4"] s30 -->|4| s50["(5,0) 长6"] s20 -->|3| s50 s40 -.->|3| s51["(5,1) 长7"] s20 -.->|1| drop["超额2 丢弃"] style drop fill:#e5e7eb,stroke:#9ca3af linkStyle 7 stroke:#9ca3af,stroke-dasharray: 3 3
观察重点:从
4. 计数:一次 Dijkstra 就够
令
关键性质:状态 (总长, 状态) 即可),
弹出
代码用 node = 2*v + k 把状态编码成整数;ways[nxt] == 0 表示首次到达该状态,
此时才把 (nd, nxt) 入堆,避免同一个状态重复入堆(这是常数优化,不影响正确性)。
最后回到原问题:
5. 样例追踪表
下表把样例第 1 组的状态弹出过程摊开(
| 弹出顺序 | 状态 |
总长 |
该状态线路数 | 转移结果 |
|---|---|---|---|---|
| 1 | 0 | 1 | ||
| 2 | 2 | 1 | ||
| 3 | 3 | 1 | ||
| 4 | 4 | 1 | ||
| 5 | 5 | 1 | ||
| 6 | 6 | 2 | — | |
| 7 | 7 | 1 | — |
终点两条状态的条数相加:
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 1000000000000000000LL; // 不可达哨兵
int n, m, s, f;
// 邻接表:first 是边权,second 是去向
vector<pair<ll, int> > adj[1005];
ll base[1005]; // src 到每个点的最短路长度
ll ways[2005]; // 拆点 2*v+k 的路径条数
ll dist2[2005]; // 拆点状态的总长(base[v]+k)
// 第一遍 Dijkstra:求 src 到每个点的最短路 base[]
void shortest_from(int src) {
for (int i = 1; i <= n; i++) base[i] = INF;
base[src] = 0;
priority_queue<pair<ll, int>, vector<pair<ll, int> >, greater<pair<ll, int> > > pq;
pq.push(make_pair(0LL, src));
while (!pq.empty()) {
ll d = pq.top().first;
int u = pq.top().second;
pq.pop();
if (d > base[u]) continue;
for (size_t i = 0; i < adj[u].size(); i++) {
ll w = adj[u][i].first;
int v = adj[u][i].second;
ll nd = d + w;
if (nd < base[v]) {
base[v] = nd;
pq.push(make_pair(nd, v));
}
}
}
}
// 第二遍分层 Dijkstra:统计最短路与「恰好长 1」的次短路条数之和
ll count_routes(int src, int dst) {
shortest_from(src);
for (int i = 0; i < 2 * n + 2; i++) {
ways[i] = 0;
dist2[i] = INF;
}
int start_node = 2 * src; // k = 0 层
ways[start_node] = 1;
dist2[start_node] = 0;
priority_queue<pair<ll, int>, vector<pair<ll, int> >, greater<pair<ll, int> > > pq;
pq.push(make_pair(0LL, start_node));
while (!pq.empty()) {
ll d = pq.top().first;
int node = pq.top().second;
pq.pop();
int v = node >> 1;
ll cnt = ways[node];
for (size_t i = 0; i < adj[v].size(); i++) {
ll w = adj[v][i].first;
int to = adj[v][i].second;
ll nd = d + w;
ll over = nd - base[to]; // 相对最短路超出的单位数
if (over > 1) continue; // 比最短路多 1 以上,丢弃
int nxt = 2 * to + (int)over;
if (ways[nxt] == 0) { // 首次到达该状态,总长已定型
dist2[nxt] = nd;
pq.push(make_pair(nd, nxt));
}
ways[nxt] += cnt;
}
}
return ways[2 * dst] + ways[2 * dst + 1];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
cin >> n >> m;
for (int i = 1; i <= n; i++) adj[i].clear();
for (int i = 0; i < m; i++) {
int a, b;
ll len;
cin >> a >> b >> len;
adj[a].push_back(make_pair(len, b)); // 平行边各自独立
}
cin >> s >> f;
cout << count_routes(s, f) << "\n";
}
return 0;
}#!/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-10-02 01:59
# update_at: 2026-10-02 02:07
import sys
from heapq import heappop, heappush
INF = 10 ** 18 # 不可达哨兵;任何真实路径长度都不超过 10^7
def shortest_from(src: int, adj: list[list[tuple[int, int]]], n: int) -> list[int]:
"""src 到每个点的最短路长度:本题的基准值 base,不可达记 INF。"""
dist = [INF] * (n + 1)
dist[src] = 0
heap = [(0, src)]
while heap:
d, u = heappop(heap)
if d > dist[u]:
continue
for w, v in adj[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
heappush(heap, (nd, v))
return dist
def count_routes(src: int, dst: int, adj: list[list[tuple[int, int]]], n: int) -> int:
"""src→dst 的最短路条数 + 恰好比最短路长 1 的路径条数。
状态编码 node = 2*v + k:k 表示走到 v 时总长相对 base[v] 已经超出几个单位,
只可能取 0 或 1。每走一条边 (v→to, 权 w),新状态由 nd - base[to] 决定;
超出 1 的路径直接丢弃。因为 base 是最短路,nd ≤ base[to] + 1 时必有
nd ≥ base[to],所以 (v, k) 的总长恒等于 base[v] + k,同一状态只会在首次
到达时入堆。
"""
base = shortest_from(src, adj, n)
ways = [0] * (2 * n + 2)
ways[2 * src] = 1 # 出发点还处在“最短”这一层
heap = [(0, 2 * src)]
while heap:
d, node = heappop(heap)
v = node >> 1
cnt = ways[node]
for w, to in adj[v]:
nd = d + w
over = nd - base[to] # 这条边让路径相对最短路超前了多少
if over > 1:
continue # 比最短路多出 1 个单位以上,不算
nxt = 2 * to + over
if ways[nxt] == 0: # 首次到达该状态,它的总长 nd 已是定值
heappush(heap, (nd, nxt))
ways[nxt] += cnt
return ways[2 * dst] + ways[2 * dst + 1]
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
T = next(data)
for _ in range(T):
n, m = next(data), next(data)
adj: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]
for _ in range(m):
a, b, length = next(data), next(data), next(data)
adj[a].append((length, b)) # 平行边各自独立,天然计入不同线路
s, f = next(data), next(data)
out.append(str(count_routes(s, f, adj, n)))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
- 第一遍 Dijkstra 求
: ; - 第二遍分层 Dijkstra:
个状态、 条转移, 。
每组数据
总结
- 超额夹逼:关心
长度 的路径时,沿途每个点相对最短路的超额只能取 或 , 因为 。 - 状态自带长度:
的长度恒为 ,所以一次按长度升序的 Dijkstra 就能定死每个状态的条数, 不需要迭代松弛。 - 平行边即线路:邻接表原样保留重复边即可,不要用集合或矩阵去重。
图示解析
这张图串起本题从建模到得到答案的主线:
有向带权图 + S, F
`- 先求 base[v] = S 到 v 的最短路
`- 观察:长度 <= d+1 的路径,每点超额 over = 长度 - base[v] 只能是 0 或 1
`- 建模:点 v 拆成状态 (v,0) 和 (v,1)
`- 转移:over = base[u] + k + w - base[v],over > 1 丢弃
`- 计数:按总长升序弹出状态,ways 累加给后继
`- 答案 = ways[(F,0)] + ways[(F,1)]先看每个节点代表的推理结论:最短路是第一层、次短路是第二层;
再顺着分支检查"超额 > 1 就丢弃"这一条如何保证状态数只有
