算法讲解:短路求方案数问题
问题描述
给定一个无向图,求从起点 1 1 1 到每个节点的最短路径的方案数(即有多少条不同的路径可以到达该节点,且路径长度是最短的)。
关键点
-
拓扑性:
- 只有 BFS 和 Dijkstra 算法天然具备拓扑性(每次扩展节点时,路径一定是当前最短的)。
- SPFA 不具备这种特性,因为队列中可能会重复处理某些节点,打破路径的拓扑顺序,导致结果错误。
-
核心思想:
- 在判断
dist[j] = dist[t] + w[i]
时,累计当前方案数。 - 如果发现另一条到达同样距离的路径,继续累加方案数。
- 在判断
代码解析
1. 数据结构定义
const int N = 100010, M = 400010, mod = 100003;int n, m;
int h[N], e[M], ne[M], idx; // 邻接表存储图
int dist[N], cnt[N]; // 最短路径距离 & 方案数
- 邻接表:
h[u]
:节点 u u u 的第一条边的索引。e[idx]
、ne[idx]
:链式前向星存储目标节点和下一条边的索引。
- 路径属性:
dist[u]
:从起点 1 1 1 到节点 u u u 的最短路径长度。cnt[u]
:从起点 1 1 1 到节点 u u u 的最短路径方案数。
2. 添加边
void add(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
- 使用链式前向星添加边。
- 因为是无向图,每次输入需要调用两次
add
,构建双向边。
3. BFS 算法
void bfs() {queue<int> q;q.push(1); // 从起点开始搜索memset(dist, 0x3f, sizeof dist); // 初始化所有点的距离为无穷大dist[1] = 0; // 起点的最短路径为 0cnt[1] = 1; // 起点的方案数为 1while (q.size()) {int t = q.front(); // 当前节点q.pop();for (int i = h[t]; ~i; i = ne[i]) { // 遍历当前节点的所有邻边int j = e[i]; // 目标节点if (dist[j] > dist[t] + 1) { // 找到更短路径dist[j] = dist[t] + 1; // 更新距离cnt[j] = cnt[t]; // 继承当前方案数q.push(j); // 入队} else if (dist[j] == dist[t] + 1) { // 找到同样短的路径cnt[j] = (cnt[j] + cnt[t]) % mod; // 累加方案数并取模}}}
}
关键点解析
-
初始化:
- 起点的
dist[1] = 0
,cnt[1] = 1
,表示从起点到起点只有一种方案。 - 其他节点的
dist
初始化为无穷大,cnt
初始化为 0。
- 起点的
-
松弛操作:
- 如果发现更短路径
dist[j] > dist[t] + 1
:- 更新目标节点的最短距离。
- 当前节点的方案数继承给目标节点。
- 如果发现同样短的路径
dist[j] == dist[t] + 1
:- 累加当前节点的方案数。
- 如果发现更短路径
-
拓扑性:
- BFS 确保了路径的拓扑顺序,先处理距离较短的节点。
- 因此,每次更新
cnt[j]
时,已经保证了当前路径是最短路径。
4. 主函数
int main() {cin >> n >> m;memset(h, -1, sizeof h); // 初始化邻接表while (m--) {int a, b;cin >> a >> b;add(a, b), add(b, a); // 添加双向边}bfs();for (int i = 1; i <= n; i++) cout << cnt[i] << endl; // 输出每个点的方案数return 0;
}
-
输入图:
- 使用邻接表存储无向图。
- 每条边调用两次
add
,分别加入两个方向。
-
调用 BFS:
- 计算从起点到每个节点的最短路径长度和方案数。
-
输出:
- 输出每个节点的方案数。
完整流程
- 图的构建:
- 使用邻接表存储无向图。
- BFS 遍历:
- 初始化起点的
dist
和cnt
。 - 使用 BFS 依次处理每个节点,更新邻接节点的最短路径和方案数。
- 初始化起点的
- 输出方案数:
- 遍历所有节点,输出到达该节点的方案数。
示例
输入:
4 4
1 2
2 3
3 4
1 3
输出:
1
2
2
2
解释:
- 从节点 1 1 1 到节点 3 3 3 有两条最短路径: 1 → 3 1 \to 3 1→3 和 1 → 2 → 3 1 \to 2 \to 3 1→2→3。
- 从节点 1 1 1 到节点 4 4 4 有两条最短路径: 1 → 3 → 4 1 \to 3 \to 4 1→3→4 和 1 → 2 → 3 → 4 1 \to 2 \to 3 \to 4 1→2→3→4。
时间复杂度
- 邻接表构建: O ( m ) O(m) O(m)。
- BFS 遍历:每条边最多被访问一次,时间复杂度 O ( m ) O(m) O(m)。
- 总复杂度: O ( m + n ) O(m + n) O(m+n)。
空间复杂度
- 邻接表存储边: O ( m ) O(m) O(m)。
- 距离和方案数组: O ( n ) O(n) O(n)。
优点与局限性
-
优点:
- BFS 自然适用于无权图,确保路径拓扑顺序。
- 使用
cnt
数组可以高效统计路径方案数。 - 通过
mod
防止数值溢出。
-
局限性:
- 仅适用于无权图或边权恒为 1 的场景。
- 对于有权图,需要使用 Dijkstra 替代 BFS。
这段代码利用 BFS 的拓扑性,巧妙地解决了无权图中的最短路径方案数问题,并且结构清晰,易于扩展!