PYTHON LESSON 080
综合项目:数论密室逃脱
把本单元的辗转相除、唯一分解、素数筛法、高精度加法与二分查找组合成一个完整的密室逃脱项目。
00 · 学习目标
这一课要解决什么?
学完后,你应该能够
- 把算法封装成可复用的工具函数
- 工具串联:一个函数的输出喂给下一个
- 主流程:解谜 → 合成 → 开锁
01 · 核心概念
数论 + 筛法 + 高精度 + 二分的综合运用
数论 + 筛法 + 高精度 + 二分的综合运用:密室里藏着三把数字钥匙:古老卷轴上的最大公因数、360 扇门的约数个数、第 15 个质数。三把钥匙用高精度加法合成最终密码,再用二分查找试开密码锁——整个游戏把本单元五大武功串成一条故事线。
GESP Python 5 级 · 初等数论GESP Python 5 级 · 高精度运算GESP Python 5 级 · 二分查找大整数质性测试和密码学分解需要更高级算法,不应把试除法扩展到任意规模。
02 · 语法与规则
先记住这 3 条,再开始写程序
把算法封装成可复用的工具函数先准确读出这条写法的结构与作用。
工具串联:一个函数的输出喂给下一个换一组最小数据,手工推演一次结果。
主流程:解谜 → 合成 → 开锁再用边界值或反例确认它的适用条件。
03 · 完整实例
代码、运行结果和解释放在一起看
# 综合项目:数论密室逃脱——集齐三把钥匙,打开最终密码锁
# 工具一:辗转相除法
def gcd(a, b):
while b != 0:
a, b = b, a % b
return a
# 工具二:质因数分解(唯一分解定理)
def factorize(n):
parts = []
i = 2
m = n
while i * i <= m:
c = 0
while m % i == 0:
m = m // i
c = c + 1
if c > 0:
parts.append((i, c))
i = i + 1
if m > 1:
parts.append((m, 1))
return parts
# 工具三:素数筛法
def sieve(n):
is_p = [True] * (n + 1)
is_p[0] = is_p[1] = False
for i in range(2, n + 1):
if is_p[i]:
for j in range(i * i, n + 1, i):
is_p[j] = False
return [x for x in range(2, n + 1) if is_p[x]]
# 工具四:高精度加法(字符串大数)
def big_add(a, b):
x = [int(c) for c in a[::-1]]
y = [int(c) for c in b[::-1]]
n = max(len(x), len(y))
x = x + [0] * (n - len(x))
y = y + [0] * (n - len(y))
carry = 0
res = []
for i in range(n):
s = x[i] + y[i] + carry
res.append(s % 10)
carry = s // 10
if carry > 0:
res.append(carry)
return "".join(str(d) for d in res[::-1])
# 第一把钥匙:古老卷轴上的最大公因数
key1 = gcd(1071, 462)
print("第一把钥匙:gcd(1071, 462) =", key1)
# 第二把钥匙:360 扇门的约数个数
cnt = 1
for p, c in factorize(360):
cnt = cnt * (c + 1)
print("第二把钥匙:360 的约数个数 =", cnt)
# 第三把钥匙:第 15 个质数
primes = sieve(60)
key3 = primes[14]
print("第三把钥匙:第 15 个质数 =", key3)
# 高精度合成最终密码
password = big_add(str(key1), str(cnt))
password = big_add(password, str(key3))
print("三把钥匙合成密码:", password)
# 密码锁 1~100:用二分查找最快试开
def unlock(target):
left, right = 1, 100
times = 0
while left <= right:
times = times + 1
mid = (left + right) // 2
if mid == target:
return times
elif mid < target:
left = mid + 1
else:
right = mid - 1
return -1
print("二分试开", password, "号锁,只用了", unlock(int(password)), "次")
print("密室逃脱成功!")第一把钥匙:gcd(1071, 462) = 21 第二把钥匙:360 的约数个数 = 24 第三把钥匙:第 15 个质数 = 47 三把钥匙合成密码: 92 二分试开 92 号锁,只用了 6 次 密室逃脱成功!
三把钥匙 21、24、47 用高精度加法合成 92;二分在 1~100 里试开 92,轨迹 50→75→88→94→91→92,正好 6 次。
04 · 逐步理解
每一步只解决一个问题
- 01
项目蓝图
密室里藏着三把数字钥匙:古老卷轴上的最大公因数、360 扇门的约数个数、第 15 个质数。三把钥匙用高精度加法合成最终密码,再用二分查找试开密码锁——整个游戏把本单元五大武功串成一条故事线。
- 02
打造工具箱
把前面几课写的 gcd、factorize、sieve、big_add 原样搬来,改造成可复用的工具函数。写好一个项目的秘诀,就是每个函数只干一件事,并且干得漂亮。
def gcd(a, b): while b != 0: a, b = b, a % b return a - 03
第一把钥匙:辗转相除
卷轴上写着 1071 和 462,gcd 算出 21。想一想为什么不用短除法——因为辗转相除只要几步就能跑完,再大的数也不在话下。
key1 = gcd(1071, 462) - 04
第二把钥匙:约数个数
360 = 2³ × 3² × 5,factorize 给出(质因数, 次数)清单,指数各加 1 再相乘得 24。分解加计数公式,两步都是质因数分解机那一课的手艺。
for p, c in factorize(360): cnt = cnt * (c + 1) - 05
第三把钥匙:筛法点名
sieve(60) 把 60 以内的质数一网打尽,第 15 个(下标 14)是 47。注意名单下标从 0 开始数,下标比序号小 1,可别数错了。
primes = sieve(60) key3 = primes[14] - 06
高精度合成与二分开锁
三把钥匙用 big_add 合成 92。这次的数字不大,但 big_add 能扛住几百位的密码——换成天文数字也照开。最后二分试开 1~100 的锁,6 次命中。
password = big_add(str(key1), str(cnt)) password = big_add(password, str(key3)) - 07
设计你自己的密室
换掉线索数字,或者再加一把“第 k 个质数”“约数和”钥匙,把游戏讲给同学玩。能把算法给别人讲明白,才是真正学会了。
05 · 练习与检验
自己写出来,才算真正学会
- 1运行游戏,看三把钥匙怎么到手
- 2读懂 factorize 并手算 360 的分解
- 3解释 big_add 为什么能算任意大的和
- 4改动线索数字,设计自己的密室
06 · 完整知识
继续理解定义、规则和适用边界
第一次学习先完成上面的六个步骤;需要查定义、核对规则、分析误区或理解“为什么”时,再展开对应知识章。
算法方法质数、约数、最大公因数与筛法建立整数整除体系,掌握试除、欧几里得算法、唯一分解和筛法的条件与复杂度。+
正式定义
若整数 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 的约数或模数非质数,必须先明确数学定义。
算法方法算法、复杂度与解题验证把题意转成输入、状态、规则与输出,用正确性和复杂度共同评价解法。+
正式定义
算法是解决一类问题的有限、明确步骤。正确性说明算法对所有满足前置条件的输入都得到规定结果;时间和空间复杂度描述输入规模增长时资源使用的增长量级。
必须掌握
- 先明确输入规模 n、数据范围、目标和允许误差,再选择数据结构与算法。
- O(1)、O(log n)、O(n)、O(n log n)、O(n²)、O(2ⁿ) 表示增长量级,不是精确运行秒数。
- 顺序代码复杂度取较大项,嵌套循环常相乘,二分每步把范围缩小一半。
- 正确性可用循环不变量、数学归纳、交换论证、反证或状态定义来说明。
- 样例只验证少量输入;必须自己设计边界、极端、重复、有序/逆序和无解数据。
- 优化前先得到正确基线并测量瓶颈,不为小数据盲目增加复杂实现。
常见误区
- 只看样例通过就宣称正确
- 不看数据范围使用 O(n²)
- 二分区间开闭混用
- 把 O(n) 当成永远比 O(log n) 慢固定倍数
适用边界
- 复杂度隐藏常数与硬件差异,但仍是比较规模增长的核心工具。
- 考场策略、课程完成度和算法能力是不同证据,任何单项都不能保证考级通过。
Python 基础运算符、表达式与优先级完整区分算术、比较、逻辑、成员、身份和位运算,并用优先级表消除歧义。+
正式定义
表达式求值得到一个值。运算符规定如何组合操作数;当一个表达式含多个运算符时,优先级和结合方向决定求值顺序,括号可以明确改变顺序。
必须掌握
- / 总是得到浮点结果;// 是向负无穷方向取整的整除;% 与 // 满足 a == (a // b) * b + a % b。
- 比较可以链式书写,如 0 <= x < 10;and/or 会短路并返回最后求值的操作数,不一定返回 bool。
- == 比较值是否相等,is 比较是否为同一个对象;判断 None 应写 is None。
- in/not in 做成员测试;对 dict 测试的是键。
- 位运算作用于整数的二进制位;负整数按无限长二进制补码语义理解。
- 复杂表达式即使能靠优先级正确运行,也应使用括号表达意图。
常见误区
- 把 // 当成简单截断
- 用 is 比较数字或字符串的值
- 忘记 and 的优先级高于 or
- 连续位移、比较和逻辑运算却不加括号
适用边界
- 浮点数比较受二进制表示误差影响,需要按问题选择容差。
- 运算符可由自定义类重载,因此相同符号对不同类型可能有不同语义。
程序组织模块、导入与常用标准库正确导入模块,并按用途查找 math、random、statistics、collections、heapq、bisect 等标准工具。+
正式定义
模块是可导入的 Python 代码单元,包把模块组织成层次。import 先加载模块并创建模块对象,再把名字绑定到当前命名空间;标准库随 Python 分发,不等于第三方包。
必须掌握
- import module 保留清晰命名空间;from module import name 只导入指定名字;避免 from module import *。
- math 提供 sqrt、floor、ceil、gcd、log、sin 等数学函数和 pi、e 等常量。
- random 用于伪随机模拟,不适合密码安全;需要安全随机时使用 secrets。
- collections.deque 适合双端队列,Counter 适合计数,defaultdict 可按工厂创建缺省值。
- heapq 实现最小堆,bisect 在已排序序列中二分定位,itertools 提供高效迭代组合工具。
- if __name__ == '__main__': 用来区分文件被直接运行还是被导入。
常见误区
- 文件名写成 math.py 导致遮蔽标准库
- import * 污染命名空间
- 把 random 当成加密随机数
- 没有理解工具的数据复杂度就盲目调用
适用边界
- 标准库非常大,本章覆盖课程会实际依赖的入口;具体函数签名应结合本站条目与运行时帮助查询。
- Turtle 需要图形窗口,本站文本判题环境不伪造其画布。
完成检查