无权图最短路计数:BFS 按层求距离的同时递推 cnt[v],首次到达赋 cnt[u]、同层前驱累加 cnt[u],边算边对 100003 取模,O(N+M)。
OJ: roj
题目 ID: 1499
难度:普及+/提高-
标签:图论bfs最短路计数python
创建: 2026-09-30 14:52
更新: 2026-10-07 12:15
形式化题目
给定无向无权图
对每个顶点
下图是题面样例图(顶点
graph LR n1((1)) --- n2((2)) n1 --- n3((3)) n2 --- n3 n2 --- n4((4)) n3 --- n4 n4 -->|a| n5((5)) n4 -->|b| n5
注意
正解
思路
朴素做法是"先求距离、再枚举路径":对每个点搜出所有 1 到它的路径,挑最短的计数。
在分层图上用 DFS 枚举路径,路径条数本身就是答案,可以指数级增长,必然超时。
瓶颈不在于"求距离"(BFS 已经是最优),而在于把路径计数当成了枚举路径:
同一个前驱
注意到最短路有可加结构:一条以
这是一条只依赖上一层的加法递推。而无权图的 BFS 按
关键在于出队
以样例为例,BFS 逐层推进的演算如下(
| 出队 |
检查的邻居 |
命中分支 | 更新后 |
|
|---|---|---|---|---|
| 1 | 0 / 1 | 2 | 未访问 | |
| 1 | 0 / 1 | 3 | 未访问 | |
| 2 | 1 / 1 | 1 | 更浅,忽略 | 不变 |
| 2 | 1 / 1 | 4 | 未访问 | |
| 2 | 1 / 1 | 3 | 不变 | |
| 3 | 1 / 1 | 1 | 更浅,忽略 | 不变 |
| 3 | 1 / 1 | 4 | 已访问且 |
|
| 3 | 1 / 1 | 2 | 同层( |
不变 |
| 4 | 2 / 2 | 2 | 更浅,忽略 | 不变 |
| 4 | 2 / 2 | 3 | 更浅,忽略 | 不变 |
| 4 | 2 / 2 | 5 | 未访问(重边第一条) | |
| 4 | 2 / 2 | 5 | 已访问且 |
|
| 5 | 3 / 4 | 4 | 更浅,忽略(两条重边各一次) | 不变 |
表格说明:每行是一次「出队 dist_u / cnt_u 是出队时 1 1 1 2 4。
实现上有两条分支,都写在遍历 adj[u] 的内层循环里:
dist[v] < 0(尚未访问):说明的最短距离刚被确定为 , 此刻把 作为初值,并把 入队。这里用 -1兼任"未访问"标记, 比额外开visited数组省一次空间和一次判断(顶点 1 的,不会与 -1混淆)。dist[v] == dist[u] + 1(已访问且正好在下一层):是递推求和里的一项,执行 。
其余情形都不满足 忽略。
在无权 BFS 中不会出现
代码
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-09-30 14:52
# update_at: 2026-09-30 14:52
import sys
from collections import deque
MOD = 100003 # 题面要求的取模值
def bfs_count(adj: list[list[int]], n: int) -> list[int]:
"""从顶点 1 出发 BFS,返回每个顶点被多少条最短路走到(对 MOD 取模)。"""
dist = [-1] * (n + 1) # -1 表示尚未访问,同时充当"层号"
cnt = [0] * (n + 1)
dist[1], cnt[1] = 0, 1
queue = deque([1])
while queue:
u = queue.popleft()
du, cu = dist[u], cnt[u] # 出队时 cnt[u] 已收齐同一层全部前驱的贡献
for v in adj[u]:
if dist[v] < 0: # 第一次到达 v:这正是最短路,层号由 u 决定
dist[v] = du + 1
cnt[v] = cu
queue.append(v)
elif dist[v] == du + 1: # v 与 u 同属"u 的下一层",再多一条最短路
cnt[v] = (cnt[v] + cu) % MOD
return cnt[1:]
def solve() -> None:
data = sys.stdin.buffer.read().split()
it = iter(data)
n, m = int(next(it)), int(next(it))
adj: list[list[int]] = [[] for _ in range(n + 1)]
for _ in range(m):
u, v = int(next(it)), int(next(it))
adj[u].append(v)
adj[v].append(u) # 无向图,两个方向都要存
sys.stdout.write('\n'.join(map(str, bfs_count(adj, n))) + '\n')
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-05 04:41
* update_at: 2026-10-05 04:41
*/
// main.cpp:「一本通 3.2 练习 3」最短路计数
// 无向无权图(允许重边):BFS 按层推进求最短距离,同时递推统计到每个点的最短路条数。
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
typedef long long ll;
const int MAXN = 100005; // 顶点数上限 N <= 100000
const int MOD = 100003; // 题面要求的取模值
// adj[u]:与顶点 u 相邻的所有顶点(一条无向边在两个邻接表里各存一次,重边原样保留)
vector<int> adj[MAXN];
// dist[u]:从顶点 1 到 u 的最短边数,-1 表示尚未访问(同时充当"层号")
int dist[MAXN];
// cnt[u]:从顶点 1 到 u 的最短路条数,边算边对 MOD 取模
ll cnt[MAXN];
// 从顶点 1 出发做 BFS,并同步递推最短路条数。
// 关键不变式:dist 相同的点按 BFS 出队顺序处理,第 dist[u]-1 层的点全部先于 u 出队,
// 所以出队 u 时 cnt[u] 已经收齐全部前驱的贡献,可以安全地传给下一层。
void bfs_count(ll n) {
for (ll i = 1; i <= n; i++) {
dist[i] = -1;
cnt[i] = 0;
}
dist[1] = 0;
cnt[1] = 1; // 顶点 1 到自己算 1 条最短路,输出第 1 行为 1
queue<ll> q; // BFS 队列,出队顺序按 dist 非降
q.push(1);
while (!q.empty()) {
ll u = q.front();
q.pop();
ll du = dist[u];
ll cu = cnt[u];
ll deg = adj[u].size();
for (ll i = 0; i < deg; i++) {
ll v = adj[u][i];
if (dist[v] == -1) {
// 第一次到达 v:最短路长度由 u 确定为 du + 1,先把 cnt[u] 作为初值
dist[v] = du + 1;
cnt[v] = cu;
q.push(v);
} else if (dist[v] == du + 1) {
// v 正好是 u 的下一层:多出一条经过 (u, v) 的最短路
// 重边会让同一对 (u, v) 再次落到这里,语义正是两条不同的路径
cnt[v] = (cnt[v] + cu) % MOD;
}
// 其余情形(v 更浅或与 u 同层)都不构成最短路,直接忽略
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll n, m;
cin >> n >> m;
for (ll i = 0; i < m; i++) {
ll u, v;
cin >> u >> v;
adj[u].push_back(v); // 无向图,两个方向都要存
adj[v].push_back(u);
}
bfs_count(n);
for (ll i = 1; i <= n; i++) {
cout << cnt[i] << '\n'; // 不可达点的 cnt 保持 0,输出自然为 0
}
return 0;
}复杂度
- 时间复杂度:
。每个顶点入队、出队各一次,每个邻接表项被检查一次,检查是 。 - 空间复杂度:
。邻接表存 个整数, dist、cnt各。
总结
- 带权最短路的计数要靠 Dijkstra/DAG 上的拓扑 DP;本题权值全为 1,BFS 的层序就是拓扑序, 于是"求距离"和"计数递推"能合并进同一次 BFS。
- 递推式
的两个分支在代码中分别是 “首次到达时赋值"和"同层前驱累加”,用 dist[v] < 0与dist[v] == dist[u] + 1区分; 同层横边(如样例的)和自环都会被这两条判断自动排除。 - 重边是这题的隐藏坑:按邻接表原样存两条边,第二次遍历会落到累加分支上,语义与"两条不同路径"一致。
- 计数必须边加边取模;不可达点
cnt保持 0,输出自然为 0。