纠错建议 35 分钟学习等级 8/8

PYTHON LESSON 125

易错辨析:八级五大翻车现场

逐个诊断八级五大高频错误,看清错因、记住正确写法,把考场自查清单内化成习惯。

00 · 学习目标

这一课要解决什么?

先想一想同样的算法思路,为什么别人 AC 你却 WA?
完成任务考场翻车诊断室
学习顺序观察错误 → 定位原因 → 最小修改 → 回归测试

学完后,你应该能够

  • 模意义下除法必须乘逆元 pow(x, MOD - 2, MOD)
  • 快速幂 result 初始为 1,开头先 a %= m
  • heapq 里放 (距离, 点),距离必须在前

01 · 核心概念

取模除法、快速幂初值、Floyd 循环顺序、堆元素顺序、浮点精度

取模除法、快速幂初值、Floyd 循环顺序、堆元素顺序、浮点精度:C(100, 50) 取模后再整除 2,得到 269496021;正确做法是乘 2 的逆元,得 769496025——两个数天差地别。错因:取模后的数已经不是原来那个数了,除法自然不成立。记牢:模世界里只有加、减、乘,除法一律改乘逆元。

考级对应GESP Python 8 级 · 倍增法(快速幂)GESP Python 8 级 · 时间与空间复杂度分析
学习边界

在线判题的学生代码由独立 Worker 执行;文件系统、网络和资源权限必须受平台限制。

02 · 语法与规则

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

01模意义下除法必须乘逆元 pow(x, MOD - 2, MOD)

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

02快速幂 result 初始为 1,开头先 a %= m

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

03heapq 里放 (距离, 点),距离必须在前

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

03 · 完整实例

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

g8-pitfall-clinic.pyPYTHON 3.12
# 易错辨析:五个最坑的写法,错例 vs 正例当场对比
from math import comb
import heapq

MOD = 10**9 + 7

# 坑 1:模意义下直接做除法 —— 除法必须先变成逆元
total = comb(100, 50) % MOD
wrong1 = (total // 2) % MOD                     # 错!取模后的数再除 2,结果无意义
right1 = total * pow(2, MOD - 2, MOD) % MOD     # 对:除以 2 等于乘 2 的逆元
print("坑1 错误写法:", wrong1, " 正确写法:", right1)

# 坑 2:快速幂 result 初始写成 0 —— 任何数的 0 次方都应是 1
def qpow_buggy(a, b, m):
    result = 0                                  # 错!初始值必须是 1
    while b > 0:
        if b % 2 == 1:
            result = result * a % m
        a = a * a % m
        b = b // 2
    return result

def qpow_right(a, b, m):
    result = 1                                  # 对
    a = a % m
    while b > 0:
        if b % 2 == 1:
            result = result * a % m
        a = a * a % m
        b = b // 2
    return result

print("坑2 2^0 错误写法:", qpow_buggy(2, 0, MOD), " 正确写法:", qpow_right(2, 0, MOD))

# 坑 3:Floyd 把 k 写进内层 —— 中转站必须最外层枚举
def floyd_wrong(d):
    n = len(d)
    for i in range(n):                          # 错!k 被关进最内层
        for j in range(n):
            for k in range(n):
                if d[i][j] > d[i][k] + d[k][j]:
                    d[i][j] = d[i][k] + d[k][j]
    return d

def floyd_right(d):
    n = len(d)
    for k in range(n):                          # 对:k 在最外层
        for i in range(n):
            for j in range(n):
                if d[i][j] > d[i][k] + d[k][j]:
                    d[i][j] = d[i][k] + d[k][j]
    return d

INF = float("inf")
g = [  # 0→3→2→1 各一条边,0 到 1 的最短路是 3
    [0, INF, INF, 1],
    [INF, 0, INF, INF],
    [INF, 1, 0, INF],
    [INF, INF, 1, 0],
]
print("坑3 0到1 错误写法:", floyd_wrong([r[:] for r in g])[0][1], " 正确写法:", floyd_right([r[:] for r in g])[0][1])

# 坑 4:堆里放 (点, 距离) —— 堆按点编号排队,不按距离
h_wrong = []
heapq.heappush(h_wrong, (1, 100))               # 点 1,距离 100
heapq.heappush(h_wrong, (2, 5))                 # 点 2,距离 5
print("坑4 错误写法弹出:", heapq.heappop(h_wrong), "(点 1 并不最近)")
h_right = []
heapq.heappush(h_right, (100, 1))               # 对:(距离, 点)
heapq.heappush(h_right, (5, 2))
print("坑4 正确写法弹出:", heapq.heappop(h_right), "(距离 5 的点 2 先出队)")

# 坑 5:公式求和用 / 浮点除法 —— 大数丢失精度
n = 10**18
wrong5 = (n * (n + 1)) / 2                      # 错!浮点只有 15-16 位有效数字
right5 = (n * (n + 1)) // 2                     # 对:整除精确
print("坑5 浮点写法:", wrong5)
print("坑5 整除写法:", right5)
运行结果OUTPUT
坑1 错误写法: 269496021  正确写法: 769496025
坑2 2^0 错误写法: 0  正确写法: 1
坑3 0到1 错误写法: inf  正确写法: 3
坑4 错误写法弹出: (1, 100) (点 1 并不最近)
坑4 正确写法弹出: (5, 2) (距离 5 的点 2 先出队)
坑5 浮点写法: 5e+35
坑5 整除写法: 500000000000000000500000000000000000

五个坑全部当场复现:左边是错误写法的输出,右边是正确写法。坑 3 的 inf 表示“误判成不可达”,最隐蔽。

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

04 · 逐步理解

每一步只解决一个问题

  1. 01

    坑 1:模意义下直接做除法

    C(100, 50) 取模后再整除 2,得到 269496021;正确做法是乘 2 的逆元,得 769496025——两个数天差地别。错因:取模后的数已经不是原来那个数了,除法自然不成立。记牢:模世界里只有加、减、乘,除法一律改乘逆元。

    PYTHON
    right = total * pow(2, MOD - 2, MOD) % MOD
  2. 02

    坑 2:快速幂 result 初始写成 0

    b = 0 时 while 循环一次都不执行,result 是几答案就是几。初始写成 0,2^0 就错算成 0;正确写法初始为 1。顺手复习另一个细节:开头先 a %= m,防止底数本身就比模大。

    PYTHON
    result = 1
    a = a % m
  3. 03

    坑 3:Floyd 的 k 被关进内层

    在 0→3→2→1 这条链上,错误顺序算出 inf(误判不可达),正确顺序是 3。错因:k 最外层的含义是“逐轮解锁允许经过的中转站”,关进内层后,d[0][2] 还没算出来就被拿去用了。有些图侥幸算对,所以错了还很难查。

    PYTHON
    for k in range(n):      # k 永远最外层
        for i in range(n):
            for j in range(n):
  4. 04

    坑 4:堆里放 (点, 距离)

    heapq 按元组第一个元素排队:放 (1, 100) 和 (2, 5),先弹出的是点 1——可它的距离是 100,根本不最近!Dijkstra 全靠“最近的先出堆”,顺序写反整个算法就失效。正确写法:(距离, 点),距离永远在前。

    PYTHON
    heapq.heappush(heap, (nd, v))  # 距离在前
  5. 05

    坑 5:公式求和用了浮点除法

    n = 10^18 时,(n×(n+1))/2 输出 5e+35——科学计数法意味着精度全丢了;而 // 整除给出精确的 36 位整数。浮点数只有 15-16 位有效数字,凡是整数答案的公式题,一律整除,一个 / 都不能有。

    PYTHON
    total = n * (n + 1) // 2
  6. 06

    考前自查清单

    交卷前对着扫一遍:①取模题里有没有偷偷做除法;②快速幂 result 初值和取模位置;③Floyd 的 k 在最外层;④堆里是 (距离, 点);⑤大数公式用 // 不用 /。五条清单花两分钟,换回来的可能是二三十分。

05 · 练习与检验

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

本课实作步骤
  1. 1运行诊断室,对比五个坑的两种输出
  2. 2用自己的话解释坑 1 为什么要用逆元
  3. 3把坑 3 的循环改回 k 最外层再运行
  4. 4默写五条考前自查清单
打开本课编程实验室编辑、运行、测试、判题都在一个页面完成

06 · 完整知识

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

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

工程能力异常、文件、测试与调试读懂报错、缩小问题、设计测试,并安全地打开、读取和关闭文本文件。

正式定义

异常是在运行期间表示错误或特殊情况的对象。调试是用可复现输入和证据定位实际行为与预期行为差异的过程;文件对象连接程序与持久化字节数据。

必须掌握

  • 先读 traceback 最后一行的异常类型与消息,再从最靠近自己代码的栈帧向上追踪。
  • try 只包可能失败的最小代码;except 捕获具体异常;else 处理成功路径;finally 做必需清理。
  • raise 主动报告不满足的前置条件;assert 用于开发期内部假设,不用于校验不可信用户输入。
  • with open(...) as file 会在退出代码块时可靠关闭文件。文本模式必须明确编码,本站统一推荐 encoding='utf-8'。
  • 测试至少包含正常值、边界值、空数据、极端值和反例;每个测试只应有明确目的。
  • 定位错误时一次只改一个假设,保留能稳定复现问题的最小输入。

OPEN MODES

文件打开模式速查

基本动作 r/w/x/a 与文本或二进制 t/b、更新标记 + 组合使用,例如 rb、w+。文本文件建议明确 encoding='utf-8'。

open() 常用文件模式
标记作用
r读取;文件必须存在
w写入;先清空已有文件,不存在则创建
x独占创建;已存在则报错
a追加;写入位置在文件末尾
t文本模式(默认)
b二进制模式,读写 bytes
+更新模式,同时允许读写;与 r/w/x/a 组合

常见误区

  • 使用 except: 吞掉所有错误
  • 只测题目样例就认为程序正确
  • 文本文件不写 encoding
  • 修复报错表象却不验证根因

适用边界

  • 在线判题的学生代码由独立 Worker 执行;文件系统、网络和资源权限必须受平台限制。
  • 二进制文件、JSON/CSV 和数据库各有专门格式与错误处理方式,不能按普通文本随意拆分。
打开本章完整示例与独立阅读页 →
算法方法算法、复杂度与解题验证把题意转成输入、状态、规则与输出,用正确性和复杂度共同评价解法。

正式定义

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

必须掌握

  • 先明确输入规模 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 的约数或模数非质数,必须先明确数学定义。
打开本章完整示例与独立阅读页 →
核心数据结构树、图、DFS、BFS 与最短路从节点与边的模型出发,掌握遍历、连通性、树结构、最小生成树和最短路径的适用条件。

正式定义

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

必须掌握

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

常见误区

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

适用边界

  • 有向无环图、拓扑排序、强连通分量和负权最短路是图论后续主题。
  • 网格搜索也是图搜索:格子是顶点,可移动关系是边;不要只背二维数组模板。
打开本章完整示例与独立阅读页 →

完成检查

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