到达目的地的方案数 图解题解
这道题到底在问什么
- 输入
- n=3, roads=[[0,1,1],[1,2,1],[0,2,2]]
- 输出
- 2(直达 0→2 与中转 0→1→2 都长 2)
- 输入
- n=2, roads=[[1,0,10]]
- 输出
- 1(唯一一条路)
最优解:为什么这么做
一句话答案:LeetCode 1976 到达目的地的方案数:Dijkstra 求 0 到 n−1 的最短用时,松弛时同步计数——更短则条数整盘继承、等长则累加,条数对 1e9+7 取模,时间 O((n+e)log n)、空间 O(n+e)。
1976 数的到底是哪一种路径
n 个路口,roads[i]=[u,v,time] 是一条走 time 的双向路,问 0 到 n−1 用时恰好等于最短用时(总耗时最小的值)的路径有几条,条数对 1e9+7 取模(除以它取余数,防大数存不下)。示例 n=3、roads=[[0,1,1],[1,2,1],[0,2,2]]:直达 0→2 花 2,绕 0→1→2 也花 2,并列最短,输出 2。
把每条路径都走一遍为什么数不完
路径条数随岔口指数膨胀——每多一个能绕的中间点走法成倍翻,逐条搜出来比用时数不完;大量路径还共用前半程,同一截被反复重算。真正要的只有两个量:各点的最短用时,和凑出它的路数。
dist 旁边为什么还要养一个 ways
最短用时交给 Dijkstra(经典最短路算法):每轮把离起点最近、还没定死的点定下来,拿它改善邻居,候选放优先队列(一个每次都能弹出当前最小值的容器)。条数是搭在它身上的一层 DP(动态规划,把子答案存表复用:dist[v] 记 0 到 v 的最短用时,ways[v] 记凑出这个用时的路径条数)。起点 dist[0]=0、ways[0]=1,到自己只有原地不动这一种。
更短继承、等长累加,怎么保证一条不多一条不少
弹出距离最小的点 u,对 u 的每个邻居 v(这条边耗时 w)做松弛——用经 u 的新用时 nd=dist[u]+w 试探 dist[v]:更短,到 v 的旧走法全体作废,dist[v]=nd、条数整盘改写 ways[v]=ways[u];相等,又冒出一批同样短的路,ways[v]=(ways[v]+ways[u]) 再取模;更长,不动。
相加不重不漏的底气在出队顺序:近的点先弹出定死,u 出队那一刻 ways[u] 已把它所有最短前驱数齐;经 u 与经别的等距前驱到 v 的路,最后一步不同,天然不会重。
n=3 的小图,堆弹三次就见分晓
起手 dist=[0,∞,∞]、ways=[1,0,0],堆里放 (0,0)。弹出 0:边 0-1 得 nd=0+1=1 比 ∞ 短,dist[1]=1、ways[1]=ways[0]=1;边 0-2 得 nd=0+2=2 也更短,dist[2]=2、ways[2]=1。弹出 1:边 1-2 得 nd=1+1=2 恰好等于 dist[2],累加 ways[2]=1+1=2。弹出 2,邻居已无可改善。收尾 ways[2]=2,两条并列最短都数上,对上输出 2。
过期的堆记录不扔,条数为什么会数多
同一个点会带着新旧距离反复进堆,弹出时要核对 d 仍等于 dist[u],不等即过期记录、直接扔——拿旧距离接着累加,同一批路会数两遍。等长写成覆盖也出错:示例里先到的直达路被丢,答案由 2 缩成 1。C++、Java 还得把 dist 存 long,初值用足够大的常量而非真无穷,免得相加溢出。
开销和裸 Dijkstra 同级:每条边至多入堆一次,堆操作 O(log n)(大 O 是数据翻倍时工作量跟几倍的记法),时间 O((n+e)log n),e 为边数;边表加 dist、ways 和堆,空间 O(n+e)。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住「更短覆盖、等长累加」这两条松弛规则,下面逐帧套它。
- 4初始化:起点 0 的最短距离 dist[0]=0、最短路条数 ways[0]=1(到自己只有空路这一种)。其余点距离记 ∞、条数 0。把 (距离 0, 节点 0) 放进优先队列。终点是节点 4。
- 5从队列取出距离最小的节点 0(距离 0)并定下来:它的最短距离 0、最短路条数 1 已确定。下面看它每条边,松弛邻居。
- 60-1(耗时 1):到 1 的新距离 = 0+1 = 1,比原来的 ∞ 更短。更新 dist[1]=1,条数整盘继承 ways[1]=ways[0]=1(旧路全作废),并入队。
- 70-2(耗时 1):到 2 的新距离 = 0+1 = 1,比原来的 ∞ 更短。更新 dist[2]=1,条数整盘继承 ways[2]=ways[0]=1(旧路全作废),并入队。
- 80-3(耗时 2):到 3 的新距离 = 0+2 = 2,比原来的 ∞ 更短。更新 dist[3]=2,条数整盘继承 ways[3]=ways[0]=1(旧路全作废),并入队。
- 9从队列取出距离最小的节点 1(距离 1)并定下来:它的最短距离 1、最短路条数 1 已确定。下面看它每条边,松弛邻居。
- 101-0 这条边(耗时 1):邻居 0 已定下,跳过。
- 111-4(耗时 2):到 4 的新距离 = 1+2 = 3,比原来的 ∞ 更短。更新 dist[4]=3,条数整盘继承 ways[4]=ways[1]=1(旧路全作废),并入队。
- 12从队列取出距离最小的节点 2(距离 1)并定下来:它的最短距离 1、最短路条数 1 已确定。下面看它每条边,松弛邻居。
- 132-0 这条边(耗时 1):邻居 0 已定下,跳过。
- 142-4(耗时 2):到 4 的新距离 = 1+2 = 3,恰好等于已有的 dist[4]=3。又发现一条同样短的路!条数累加 ways[4] = 1+ways[2](1) = 2。
- 15从队列取出距离最小的节点 3(距离 2)并定下来:它的最短距离 2、最短路条数 1 已确定。下面看它每条边,松弛邻居。
- 163-0 这条边(耗时 2):邻居 0 已定下,跳过。
- 173-4(耗时 1):到 4 的新距离 = 2+1 = 3,恰好等于已有的 dist[4]=3。又发现一条同样短的路!条数累加 ways[4] = 2+ways[3](1) = 3。
- 18从队列取出距离最小的节点 4(距离 3)并定下来:它的最短距离 3、最短路条数 3 已确定。下面看它每条边,松弛邻居。
- 194-1 这条边(耗时 2):邻居 1 已定下,跳过。
- 204-2 这条边(耗时 2):邻居 2 已定下,跳过。
- 214-3 这条边(耗时 1):邻居 3 已定下,跳过。
- 22队列处理完毕。终点节点 4 的最短距离 = 3,最短路条数 ways[4] = 3。这 3 条最短路分别经 1、2、3 三个中转点(0→1→4、0→2→4、0→3→4),长度都恰为 3。
- 23答案就是 ways[n−1] = ways[4] = 3(本题规模内未触发取模)。关键在松弛时分清「更短就覆盖条数、等长就累加条数」,Dijkstra 的逐点定值顺序保证每个点被定下时,它的条数已经把所有最短前驱都数齐了。
⚠️ 容易写错的地方
✗ 错:发现更短路时忘了重置 ways
✓ 对:nd 更小则 ways[v]=ways[u](整盘继承)
更短路出现后,旧距离对应的所有计数都作废,必须把条数换成新前驱的条数,而不是累加
✗ 错:把「等长」也写成覆盖
✓ 对:nd 等于 dist[v] 要累加 ways[v]+=ways[u]
等长说明又多了一批同样短的路,应在原有条数上加,不能覆盖,否则会漏数
✗ 错:dist 用真·无穷或 int 存导致溢出
✓ 对:用 long 与一个足够大的有限初值
C++、Java 里相加可能越界;用 long 且初值取 LLONG_MAX/4 之类既够大又不会加爆
完整代码(Python / C++ / Java)
Python
from typing import List
import heapq
class Solution:
def countPaths(self, n: int, roads: List[List[int]]) -> int:
MOD = 10**9 + 7
g = [[] for _ in range(n)]
for a, b, w in roads:
g[a].append((b, w)); g[b].append((a, w))
dist = [10**30] * n
ways = [0] * n
dist[0] = 0; ways[0] = 1
heap = [(0, 0)]
while heap:
d, u = heapq.heappop(heap)
if d != dist[u]:
continue
for v, w in g[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
ways[v] = ways[u]
heapq.heappush(heap, (nd, v))
elif nd == dist[v]:
ways[v] = (ways[v] + ways[u]) % MOD
return ways[n - 1]C++
#include <queue>
#include <vector>
using namespace std;
class Solution {
public:
int countPaths(int n, vector<vector<int>>& roads) {
const int MOD = 1000000007;
vector<vector<pair<int,int>>> g(n);
for (auto &e : roads) { g[e[0]].push_back({e[1], e[2]}); g[e[1]].push_back({e[0], e[2]}); }
vector<long long> dist(n, LLONG_MAX / 4), ways(n);
priority_queue<pair<long long,int>, vector<pair<long long,int>>, greater<pair<long long,int>>> pq;
dist[0] = 0; ways[0] = 1; pq.push({0, 0});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d != dist[u]) continue;
for (auto [v, w] : g[u]) {
long long nd = d + w;
if (nd < dist[v]) { dist[v] = nd; ways[v] = ways[u]; pq.push({nd, v}); }
else if (nd == dist[v]) ways[v] = (ways[v] + ways[u]) % MOD;
}
}
return ways[n - 1];
}
};Java
import java.util.*;
class Solution {
public int countPaths(int n, int[][] roads) {
int MOD = 1_000_000_007;
List<int[]>[] g = new ArrayList[n];
for (int i = 0; i < n; i++) g[i] = new ArrayList<>();
for (int[] e : roads) { g[e[0]].add(new int[]{e[1], e[2]}); g[e[1]].add(new int[]{e[0], e[2]}); }
long[] dist = new long[n], ways = new long[n];
Arrays.fill(dist, Long.MAX_VALUE / 4);
PriorityQueue<long[]> pq = new PriorityQueue<>(Comparator.comparingLong(a -> a[0]));
dist[0] = 0; ways[0] = 1; pq.offer(new long[]{0, 0});
while (!pq.isEmpty()) {
long[] cur = pq.poll();
long d = cur[0]; int u = (int) cur[1];
if (d != dist[u]) continue;
for (int[] e : g[u]) {
int v = e[0], w = e[1];
long nd = d + w;
if (nd < dist[v]) { dist[v] = nd; ways[v] = ways[u]; pq.offer(new long[]{nd, v}); }
else if (nd == dist[v]) ways[v] = (ways[v] + ways[u]) % MOD;
}
}
return (int) ways[n - 1];
}
}复杂度
时间
O((n+e)·log n)
e 条边,每条最多触发一次入堆,堆操作 O(log n);维护 ways 只是常数附加,整体仍是带堆 Dijkstra 的 O((n+e)log n)
空间
O(n+e)
邻接表 O(n+e),dist 与 ways 各 O(n),堆最坏 O(e),合计 O(n+e)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 到达目的地的方案数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
更短时为什么 ways 要整盘改写成 ways[u],不能把旧值也加上?+
旧的 ways[v] 数的是旧距离那批走法,新的更短距离一出现,它们就不再是最短,全体作废;把它们加进来等于把更慢的路径也算进答案。只有 nd 与 dist[v] 相等时,新旧两批的总用时相同,才允许相加。
边权如果出现 0,「更短继承、等长累加」还成立吗?+
本题每条边的耗时都是正数,Dijkstra「先弹出的点已定死」的前提成立。一旦允许 0 权边,两个等距的点可能互相到达、互相贡献条数,某个点弹出时它的条数未必数齐。那种情形要先算好各点最短距离,再在最短路 DAG(只保留满足 dist[u]+w=dist[v] 的边构成的有向无环图)上按拓扑顺序(先把一个点的全部前驱算完、再算它自己)数路径。
为什么每次累加都要取模,最后一次性取不行吗?+
最短路条数可以随图的规模指数增长,long 也存不下,中途不取模会先溢出成错值。模加法有个好性质:每步加完就取余,和最后再统一取余结果相同,所以逐步取模既不出错也不改答案。
还没单独写熟 Dijkstra 本身,该拿哪道题先练?+
LeetCode 743 网络延迟时间是 Dijkstra 的裸板:只求各点最短用时、不数条数,dist 数组、优先队列、松弛这套骨架和本题一模一样。先把 743 写顺,再回来加 ways 这一层,哪几行属于最短路、哪几行属于计数 DP 就分得清了。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 到达目的地的方案数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。