深入建议 50 分钟学习等级 8/8

PYTHON LESSON 122

最短路进阶:路径条数与路线还原

在 Dijkstra 模板上学会统计最短路条数与还原具体路线,拿下“最短距离 + 方案数”复合题型。

00 · 学习目标

这一课要解决什么?

先想一想导航软件说“还有 2 条路线时间相同”,它是怎么数出来的?
完成任务导航路线分析器
学习顺序正式定义 → 机制推演 → 边界与反例 → 迁移

学完后,你应该能够

  • nd < dist[v] 更短:dist、cnt、pre 一起更新
  • nd == dist[v] 一样短:cnt[v] += cnt[u]
  • 从终点沿 pre 回溯再 reverse 得到完整路线

01 · 核心概念

Dijkstra 的两大扩展:cnt 数组数方案、pre 数组还原路线

Dijkstra 的两大扩展:cnt 数组数方案、pre 数组还原路线:GESP 真题很爱把两个问题绑在一起:最短距离是多少?这样的路线有几条?好消息是骨架不用换——Dijkstra 照跑,只要多带一个 cnt 数组和一个 pre 字典,两个扩展一次学会。

考级对应GESP Python 8 级 · 单源最短路(Dijkstra)GESP Python 8 级 · 计数原理
学习边界

有向无环图、拓扑排序、强连通分量和负权最短路是图论后续主题。

02 · 语法与规则

先记住这 3 条,再开始写程序

01nd < dist[v] 更短:dist、cnt、pre 一起更新

先准确读出这条写法的结构与作用。

02nd == dist[v] 一样短:cnt[v] += cnt[u]

换一组最小数据,手工推演一次结果。

03从终点沿 pre 回溯再 reverse 得到完整路线

再用边界值或反例确认它的适用条件。

03 · 完整实例

代码、运行结果和解释放在一起看

g8-shortest-path-plus.pyPYTHON 3.12
# Dijkstra 两大扩展:数一数最短路有几条,再把路线打印出来
import heapq

graph = {  # 邻接表:{路口: [(下一路口, 距离), ...]}
    1: [(2, 2), (3, 3)],
    2: [(3, 1), (4, 4)],
    3: [(4, 2)],
    4: [],
}
start, end = 1, 4
dist = {start: 0}
cnt = {start: 1}        # cnt[v] = 到 v 的最短路有多少条
pre = {}                # pre[v] = 最短路上 v 的前一个路口
heap = [(0, start)]
while heap:
    d, u = heapq.heappop(heap)
    if d > dist[u]:
        continue                    # 过期记录,跳过
    for v, w in graph[u]:
        nd = d + w
        if v not in dist or nd < dist[v]:   # 找到更短的路
            dist[v] = nd
            cnt[v] = cnt[u]         # 方案数直接继承
            pre[v] = u
            heapq.heappush(heap, (nd, v))
        elif nd == dist[v]:         # 一样短,方案数累加
            cnt[v] += cnt[u]

print("1 到 4 的最短距离:", dist[end])
print("最短路线共有:", cnt[end], "条")
path = [end]
while path[-1] != start:            # 从终点沿 pre 一路回溯
    path.append(pre[path[-1]])
path.reverse()
print("其中一条路线:", " -> ".join(map(str, path)))
运行结果OUTPUT
1 到 4 的最短距离: 5
最短路线共有: 2 条
其中一条路线: 1 -> 3 -> 4

两条并列最短路线是 1→2→3→4 与 1→3→4,长度都是 5;程序沿 pre 回溯只打印其中一条,另一条请你在图上找出来。

在新标签运行和修改这个实例已装入本课代码 · 可自定义输入 · 可提交判题

04 · 逐步理解

每一步只解决一个问题

  1. 01

    从“最短”到“有几条”

    GESP 真题很爱把两个问题绑在一起:最短距离是多少?这样的路线有几条?好消息是骨架不用换——Dijkstra 照跑,只要多带一个 cnt 数组和一个 pre 字典,两个扩展一次学会。

  2. 02

    cnt 数组的两条更新规则

    规则一:找到更短的路(nd < dist[v]),v 的方案数直接“继承”u 的方案数——因为所有更短路线都是先到 u 再走这条边。规则二:一样短(nd == dist[v]),把 u 的方案数累加进 cnt[v]。就这两条,背下来。

    PYTHON
    if nd < dist[v]:
        cnt[v] = cnt[u]
    elif nd == dist[v]:
        cnt[v] += cnt[u]
  3. 03

    为什么更短时要“重置”而不是累加

    dist[v] 被刷新,说明之前记录的那些路线全部不再是“最短”的了,方案数必须推倒重来、从新前驱继承。如果这时还累加,就会把已经落伍的长路线也数进去——这是本题型最容易踩的坑。

    PYTHON
    cnt[v] = cnt[u]  # 更短:推翻旧方案,整体继承
  4. 04

    pre 字典与路线回溯

    每次更新最短路时顺手记下 pre[v] = u,相当于给每个路口留了一张“你从哪来”的纸条。查询时从终点出发一路读纸条回到起点,再 reverse 一下,正向路线就出来了。

    PYTHON
    path = [end]
    while path[-1] != start:
        path.append(pre[path[-1]])
    path.reverse()
  5. 05

    条数太大要取模

    在网格状的图上,最短路条数可以是组合数级别(想想 123 课的网格路径),题目会要求对 10^9+7 取模。改动只有一处:累加时顺手 % MOD。注意继承不用取模,因为 cnt[u] 本来就已经取过了。

    PYTHON
    cnt[v] = (cnt[v] + cnt[u]) % MOD
  6. 06

    复杂度没有变

    两个扩展都只是数组的 O(1) 读写,整体仍是 O(m log n)。配套真题 py-exam-l8-a2 给到 10^5 个点、2×10^5 条边,邻接表加堆优化的这套写法稳过——模板熟了,考场上就是默写。

05 · 练习与检验

自己写出来,才算真正学会

本课实作步骤
  1. 1运行分析器,核对最短距离 5、共 2 条
  2. 2在图上找出两条并列的最短路线
  3. 3解释为什么找到更短路时 cnt 要重置而不是累加
  4. 4把 1→3 的距离改成 5,预测并核对条数变化
打开本课编程实验室编辑、运行、测试、判题都在一个页面完成

06 · 完整知识

继续理解定义、规则和适用边界

第一次学习先完成上面的六个步骤;需要查定义、核对规则、分析误区或理解“为什么”时,再展开对应知识章。

核心数据结构树、图、DFS、BFS 与最短路从节点与边的模型出发,掌握遍历、连通性、树结构、最小生成树和最短路径的适用条件。

正式定义

图由顶点和边构成;树是连通且无环的无向图。DFS 沿路径深入后回溯,BFS 按距离层次扩展。算法的正确选择取决于图是否有向、边权是否为负、是否稠密以及目标是遍历、连通还是最短路。

必须掌握

  • 邻接矩阵占 O(V²) 空间,适合稠密图和快速查边;邻接表占 O(V+E),适合稀疏图。
  • DFS 常用递归或显式栈,BFS 使用队列;一般图都必须记录 visited 防止重复和死循环。
  • 无权图的 BFS 首次到达即得到最少边数距离,可用 parent 还原路径。
  • 树有 V-1 条边且任意两点路径唯一;二叉树前/中/后序描述根的访问时机。
  • Dijkstra 只适用于非负边权;Floyd 求所有点对最短路并允许负边,但不能有可达负环。
  • Kruskal 按边权排序并用并查集避环,得到连通无向带权图的最小生成树。

常见误区

  • 遍历一般图时不记录 visited
  • 对负权边使用 Dijkstra
  • 混淆最短路径树与最小生成树
  • 递归 DFS 忽略深度限制

适用边界

  • 有向无环图、拓扑排序、强连通分量和负权最短路是图论后续主题。
  • 网格搜索也是图搜索:格子是顶点,可移动关系是边;不要只背二维数组模板。
打开本章完整示例与独立阅读页 →
算法方法算法、复杂度与解题验证把题意转成输入、状态、规则与输出,用正确性和复杂度共同评价解法。

正式定义

算法是解决一类问题的有限、明确步骤。正确性说明算法对所有满足前置条件的输入都得到规定结果;时间和空间复杂度描述输入规模增长时资源使用的增长量级。

必须掌握

  • 先明确输入规模 n、数据范围、目标和允许误差,再选择数据结构与算法。
  • O(1)、O(log n)、O(n)、O(n log n)、O(n²)、O(2ⁿ) 表示增长量级,不是精确运行秒数。
  • 顺序代码复杂度取较大项,嵌套循环常相乘,二分每步把范围缩小一半。
  • 正确性可用循环不变量、数学归纳、交换论证、反证或状态定义来说明。
  • 样例只验证少量输入;必须自己设计边界、极端、重复、有序/逆序和无解数据。
  • 优化前先得到正确基线并测量瓶颈,不为小数据盲目增加复杂实现。

常见误区

  • 只看样例通过就宣称正确
  • 不看数据范围使用 O(n²)
  • 二分区间开闭混用
  • 把 O(n) 当成永远比 O(log n) 慢固定倍数

适用边界

  • 复杂度隐藏常数与硬件差异,但仍是比较规模增长的核心工具。
  • 考场策略、课程完成度和算法能力是不同证据,任何单项都不能保证考级通过。
打开本章完整示例与独立阅读页 →
算法方法质数、约数、最大公因数与筛法建立整数整除体系,掌握试除、欧几里得算法、唯一分解和筛法的条件与复杂度。

正式定义

若整数 a 能被非零整数 b 整除,则 b 是 a 的约数。大于 1 且只有 1 和自身两个正约数的整数是质数;每个大于 1 的整数都能唯一分解为质数幂的乘积(忽略次序)。

必须掌握

  • 0 和 1 都不是质数;判定 n 是否为质数只需试除到 floor(sqrt(n))。
  • gcd(a,b)=gcd(b,a mod b) 构成欧几里得算法;lcm(a,b)=abs(a//gcd(a,b)*b) 并要处理 0。
  • 约数成对出现,可枚举到平方根;完全平方数的平方根只计一次。
  • 埃氏筛从 p² 开始标记质数 p 的倍数,总体 O(n log log n);线性筛保证每个合数被最小质因子筛一次。
  • 分解质因数后,约数个数与约数和可以由各质因数指数公式计算。
  • 模运算支持加减乘分配;模除法不能直接用整数除法,需满足可逆条件并求逆元。

常见误区

  • 把 1 判成质数
  • 试除上界漏掉平方根
  • 完全平方数的约数重复统计
  • 取模后直接做普通除法

适用边界

  • 大整数质性测试和密码学分解需要更高级算法,不应把试除法扩展到任意规模。
  • 题目若涉及负数约数、0 的约数或模数非质数,必须先明确数学定义。
打开本章完整示例与独立阅读页 →
数学基础计数原理、组合数与计算几何基础明确有序/无序、互斥/分步与几何退化边界,再把公式转为稳定程序。

正式定义

加法原理用于互斥方案分类求和,乘法原理用于依次完成步骤求积;排列关心顺序,组合不关心顺序。计算几何用坐标、向量、距离和方向关系把图形条件转成数值判定。

必须掌握

  • A(n,m)=n!/(n-m)!,C(n,m)=n!/(m!(n-m)!),并有 C(n,m)=C(n,n-m)。
  • 杨辉三角递推 C(n,m)=C(n-1,m-1)+C(n-1,m),边界 C(n,0)=C(n,n)=1。
  • 取模下的组合数算法取决于 n、模数是否为质数、查询次数和是否允许预处理。
  • 距离比较应优先比较平方,避免不必要的 sqrt 和浮点误差。
  • 三角形必须满足任意两边之和大于第三边;共线、重合、零长度是几何边界。
  • 浮点比较应按题目误差使用容差;整数坐标能用叉积时优先保持整数计算。

常见误区

  • 未判断顺序是否重要就套排列组合公式
  • 模意义下直接做普通除法
  • 浮点数直接用 ==
  • 漏掉共线或退化图形

适用边界

  • 概率需要在计数结果上再建立样本空间,不等同于组合数本身。
  • 复杂计算几何涉及方向、相交、凸包等更多主题,本章只覆盖课程实际使用的基础判定。
打开本章完整示例与独立阅读页 →

完成检查

确认自己会解释、会编写、会验证