PYTHON LESSON 125
易错辨析:八级五大翻车现场
逐个诊断八级五大高频错误,看清错因、记住正确写法,把考场自查清单内化成习惯。
00 · 学习目标
这一课要解决什么?
学完后,你应该能够
- 模意义下除法必须乘逆元 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 条,再开始写程序
模意义下除法必须乘逆元 pow(x, MOD - 2, MOD)先准确读出这条写法的结构与作用。
快速幂 result 初始为 1,开头先 a %= m换一组最小数据,手工推演一次结果。
heapq 里放 (距离, 点),距离必须在前再用边界值或反例确认它的适用条件。
03 · 完整实例
代码、运行结果和解释放在一起看
# 易错辨析:五个最坑的写法,错例 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)坑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 · 逐步理解
每一步只解决一个问题
- 01
坑 1:模意义下直接做除法
C(100, 50) 取模后再整除 2,得到 269496021;正确做法是乘 2 的逆元,得 769496025——两个数天差地别。错因:取模后的数已经不是原来那个数了,除法自然不成立。记牢:模世界里只有加、减、乘,除法一律改乘逆元。
right = total * pow(2, MOD - 2, MOD) % MOD - 02
坑 2:快速幂 result 初始写成 0
b = 0 时 while 循环一次都不执行,result 是几答案就是几。初始写成 0,2^0 就错算成 0;正确写法初始为 1。顺手复习另一个细节:开头先 a %= m,防止底数本身就比模大。
result = 1 a = a % m - 03
坑 3:Floyd 的 k 被关进内层
在 0→3→2→1 这条链上,错误顺序算出 inf(误判不可达),正确顺序是 3。错因:k 最外层的含义是“逐轮解锁允许经过的中转站”,关进内层后,d[0][2] 还没算出来就被拿去用了。有些图侥幸算对,所以错了还很难查。
for k in range(n): # k 永远最外层 for i in range(n): for j in range(n): - 04
坑 4:堆里放 (点, 距离)
heapq 按元组第一个元素排队:放 (1, 100) 和 (2, 5),先弹出的是点 1——可它的距离是 100,根本不最近!Dijkstra 全靠“最近的先出堆”,顺序写反整个算法就失效。正确写法:(距离, 点),距离永远在前。
heapq.heappush(heap, (nd, v)) # 距离在前 - 05
坑 5:公式求和用了浮点除法
n = 10^18 时,(n×(n+1))/2 输出 5e+35——科学计数法意味着精度全丢了;而 // 整除给出精确的 36 位整数。浮点数只有 15-16 位有效数字,凡是整数答案的公式题,一律整除,一个 / 都不能有。
total = n * (n + 1) // 2 - 06
考前自查清单
交卷前对着扫一遍:①取模题里有没有偷偷做除法;②快速幂 result 初值和取模位置;③Floyd 的 k 在最外层;④堆里是 (距离, 点);⑤大数公式用 // 不用 /。五条清单花两分钟,换回来的可能是二三十分。
05 · 练习与检验
自己写出来,才算真正学会
- 1运行诊断室,对比五个坑的两种输出
- 2用自己的话解释坑 1 为什么要用逆元
- 3把坑 3 的循环改回 k 最外层再运行
- 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'。
| 标记 | 作用 |
|---|---|
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 忽略深度限制
适用边界
- 有向无环图、拓扑排序、强连通分量和负权最短路是图论后续主题。
- 网格搜索也是图搜索:格子是顶点,可移动关系是边;不要只背二维数组模板。
完成检查