Python编程实践:用代码复活中国古代数学经典问题
1. 项目概述:当古算题遇上现代代码
如果你对编程感兴趣,尤其是Python,同时又对咱们老祖宗那些充满智慧的数学问题着迷,那你来对地方了。这个项目,说白了就是用Python这把“现代手术刀”,去解剖那些流传千年的“中国古代数学问题”。这可不是简单的翻译或者复现,而是一次跨越时空的思维对话。
我最初动这个念头,是因为发现很多朋友学Python,学着学着就卡在了“学完基础语法然后呢?”这个阶段。天天对着 print(“Hello World”) 或者算个斐波那契数列,时间长了难免觉得枯燥,离“用编程解决实际问题”的感觉总差一口气。而《九章算术》、《孙子算经》里的那些题目,比如“鸡兔同笼”、“物不知数”(中国剩余定理),它们本身就是精炼的数学模型,有明确的输入、逻辑和输出,简直是绝佳的编程练习题素材。用Python来实现它们,你立刻就能看到代码如何将一段抽象的文字描述,转化为清晰、可执行的逻辑,并瞬间得到答案。这个过程,既能让你深入理解Python的流程控制、数据结构,又能让你直观感受到数学建模的魅力,一举两得。
这个项目适合所有阶段的Python学习者。新手可以把它当作趣味练习题,巩固循环、判断、函数等基础;有一定经验的开发者,则可以挑战更复杂的算法实现和性能优化,甚至用可视化库把解题过程画出来。接下来,我就带你一起,挑几个经典问题,手把手看看怎么用Python让这些古老的智慧重新“活”过来。
2. 核心问题选取与算法思路拆解
中国古代数学典籍浩如烟海,我们得挑那些有代表性、且适合编程实现的“硬骨头”来啃。选得太简单,没挑战;选得过于理论化,又偏离了编程实践的初衷。我主要从《九章算术》和《孙子算经》里选了四类问题,它们分别代表了不同的算法思想和编程难点。
2.1 鸡兔同笼问题:线性方程与遍历搜索
这大概是知名度最高的古算题了:“今有雉兔同笼,上有三十五头,下有九十四足。问雉兔各几何?”(出自《孙子算经》)。它的本质是一个二元一次方程组: 设鸡(雉)有 x 只,兔有 y 只。 则有: x + y = heads (头的总数) 2x + 4y = legs (脚的总数)
编程思路解析 : 对于这类问题,最直接的思路是 数学解析法 ,即解方程组。我们可以推导出公式: y = (legs - 2 * heads) / 2 x = heads - y 在代码中,我们需要先判断输入是否合理(脚数是否为偶数、是否少于最少脚数等)。但更“编程思维”的方法是 暴力枚举法 :既然头的总数不大,我们可以让鸡的数量从0循环到总头数,计算对应的兔的数量和总脚数,判断是否匹配。这种方法虽然效率不高,但逻辑直白,非常适合初学者理解循环和条件判断。在后续优化中,我们可以讨论两种方法的效率差异。
2.2 物不知数问题:中国剩余定理的雏形
“今有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?”(出自《孙子算经》)。这就是著名的“韩信点兵”问题,是现代数论中“中国剩余定理”的经典例题。题目是寻找一个数 N ,满足: N % 3 == 2 N % 5 == 3 N % 7 == 2
编程思路解析 : 最笨也是最容易想到的方法是 逐次加一遍历 :从一个起点(比如1)开始,不断加1,并检查是否同时满足三个条件。这在小范围内可行,但如果模数很大或者解的范围未知,效率极低。更优的算法是 利用定理的构造思想 :寻找一个数,是5和7的公倍数(35的倍数),且除以3余1;同理再找一个是3和7公倍数(21的倍数)除以5余1的数;以及是3和5公倍数(15的倍数)除以7余1的数。然后将它们分别乘以对应的余数,相加后再对总模数(3 5 7=105)取模。这个算法效率是O(1)的,但理解起来需要一些数论基础。我们可以先用遍历法实现一个通用函数,再进阶实现基于定理的优化算法,让读者体会算法优化带来的巨大性能提升。
2.3 盈不足问题:双假设法与线性插值
“今有共买物,人出八,盈三;人出七,不足四。问人数、物价各几何?”(出自《九章算术》)。这类问题被称为“盈不足术”,通过两种假设条件下的盈余和不足,来反推正确的参数。设人数为 p ,物价为 c 。 根据题意有: 8p = c + 3 (每人出8钱,多3钱) 7p = c - 4 (每人出7钱,少4钱)
编程思路解析 : 这本质上仍然是解二元一次方程组。但“盈不足术”的精妙在于其提供的通用解法公式,即使问题不是线性关系也能给出近似解。对于线性情况,公式推导出的解是: p = (盈 + 不足) / (两次每人出钱之差) = (3+4)/(8-7)=7 c = 出钱1 * p - 盈 = 8*7-3=53 我们的程序可以设计为:输入两种假设下的“每人出钱数”以及对应的“盈”或“不足”值(不足用负数表示),然后直接套用公式计算。这能很好地练习函数的封装:写一个函数 solve_profit_and_loss(cost1, profit1, cost2, profit2) ,其中 profit 正数表示盈,负数表示不足。同时,我们需要处理除零错误(两次出价不能相同)。
2.4 河妇荡杯问题:分数方程与比例求解
“今有妇人河上荡杯,津吏问曰:‘杯何以多?’妇人曰:‘家有客。’津吏曰:‘客几何?’妇人曰:‘二人共饭,三人共羹,四人共肉,凡用杯六十五。不知客几何?’”(出自《孙子算经》)。意思是:两人用一个饭碗,三人用一个汤碗,四人用一个肉碗,总共用了65个碗,问有多少客人? 设客人数为 x 。 则饭碗数 x/2 ,汤碗数 x/3 ,肉碗数 x/4 。 总碗数: x/2 + x/3 + x/4 = 65
编程思路解析 : 这是一个关于分数相加的方程。解题关键在于寻找分母的最小公倍数(LCM)。方程两边同时乘以2、3、4的最小公倍数12,即可化为整数方程: 6x + 4x + 3x = 65 * 12 ,即 13x = 780 ,解得 x=60 。 在编程实现上,我们可以分为几步:
- 解析问题,提取出“份额”列表,这里是
[2, 3, 4],和总碗数65。 - 编写一个函数计算一组数的最小公倍数(LCM)。可以通过先计算最大公约数(GCD),再利用公式
LCM(a, b) = a * b / GCD(a, b)来递归或迭代求解。 - 通用求解:设份额列表为
shares,总数为total。则方程是sum(x / share for share in shares) = total。通分后,x * sum(LCM / share) = total * LCM,所以x = (total * LCM) / sum(LCM / share for share in shares)。 - 需要验证结果
x是否为整数,以及每个x / share是否为整数(碗数必须是整数)。 这个问题的实现将综合运用到分数运算、最小公倍数/最大公约数算法以及通用公式的推导,非常有挑战性也很有成就感。
3. Python实现详解与代码实操
理论分析完了,我们进入最核心的实操环节。我会为每个问题提供两种版本的代码:一种是 基础直观版 ,注重可读性和教学性;另一种是 进阶优化版 ,注重代码的健壮性、通用性和效率。所有代码都假设使用 Python 3.6+ 环境。
3.1 鸡兔同笼:从暴力枚举到公式求解
我们先从最简单的暴力枚举开始,建立信心。
def chicken_rabbit_bruteforce(heads, legs):
"""
暴力枚举法解决鸡兔同笼问题
Args:
heads (int): 头的总数
legs (int): 脚的总数
Returns:
tuple: (鸡的数量, 兔的数量) 如果无解返回 (None, None)
"""
for chickens in range(heads + 1): # 鸡的数量从0尝试到总头数
rabbits = heads - chickens
if 2 * chickens + 4 * rabbits == legs:
return chickens, rabbits
return None, None # 循环结束没找到,说明无解
# 测试《孙子算经》原题
print(chicken_rabbit_bruteforce(35, 94)) # 输出: (23, 12)
这个函数逻辑清晰,但效率是 O(n)。对于 heads 很大的情况(虽然现实不会),就不太理想。现在我们用数学公式法重写:
def chicken_rabbit_math(heads, legs):
"""
公式法解决鸡兔同笼问题
"""
# 初步校验:脚数必须是偶数,且不能少于2*heads,不能多于4*heads
if legs % 2 != 0:
print("错误:脚的总数必须是偶数。")
return None, None
if not (2 * heads <= legs <= 4 * heads):
print("错误:脚的数量不符合逻辑范围。")
return None, None
# 核心计算公式
# 假设全是鸡,则多出的脚是每只兔子多2只脚造成的
rabbits = (legs - 2 * heads) // 2
chickens = heads - rabbits
# 最终校验:结果应为非负整数
if rabbits >= 0 and chickens >= 0:
return chickens, rabbits
else:
return None, None
# 测试
print(chicken_rabbit_math(35, 94)) # (23, 12)
print(chicken_rabbit_math(10, 30)) # (5, 5)
print(chicken_rabbit_math(10, 31)) # 错误提示,返回(None, None)
注意 :公式法虽然高效,但初学者容易直接套公式而忽略 输入校验 。在实际编程中,处理用户输入或不确定的数据源时,健壮性校验(检查数据是否合理、是否会导致计算错误)是必不可少的步骤,这比单纯追求算法效率更重要。
3.2 物不知数:从遍历到中国剩余定理
先实现最直接的遍历搜索,这对于理解问题本质很有帮助。
def find_number_bruteforce(remainders, modulos, search_limit=10000):
"""
遍历法求解物不知数类问题(通用版)
Args:
remainders (list): 余数列表,如 [2, 3, 2]
modulos (list): 模数列表,如 [3, 5, 7]
search_limit (int): 搜索上限,防止无限循环
Returns:
int: 找到的最小正整数解,若未找到则返回None
"""
for n in range(1, search_limit + 1):
found = True
for r, m in zip(remainders, modulos):
if n % m != r:
found = False
break
if found:
return n
return None
# 测试原题
print(find_number_bruteforce([2, 3, 2], [3, 5, 7])) # 输出: 23
当模数变大时,遍历法会非常慢。下面实现基于中国剩余定理(CRT)的算法。这里实现一个简化版本,要求模数两两互质(原题3、5、7满足)。
def extended_gcd(a, b):
"""扩展欧几里得算法,返回 (gcd, x, y) 使得 ax + by = gcd(a, b)"""
if b == 0:
return a, 1, 0
gcd, x1, y1 = extended_gcd(b, a % b)
x = y1
y = x1 - (a // b) * y1
return gcd, x, y
def solve_crt(remainders, modulos):
"""
使用中国剩余定理求解(模数需两两互质)
"""
# 计算所有模数的乘积 N
N = 1
for m in modulos:
N *= m
result = 0
for r_i, m_i in zip(remainders, modulos):
# 计算 N_i = N / m_i
N_i = N // m_i
# 使用扩展欧几里得算法求 N_i 模 m_i 的逆元 inv_i
gcd, inv_i, _ = extended_gcd(N_i, m_i)
if gcd != 1:
raise ValueError(f"模数 {m_i} 与 N_i 不互质,无法使用此简化CRT。")
# 累加:r_i * inv_i * N_i
result += r_i * inv_i * N_i
# 返回最小正整数解
return result % N
# 测试
print(solve_crt([2, 3, 2], [3, 5, 7])) # 输出: 23
print(solve_crt([1, 2, 3], [2, 3, 5])) # 输出: 23? 验证:23%2=1, 23%3=2, 23%5=3,正确。
实操心得 :实现中国剩余定理时, 扩展欧几里得算法 是关键。很多教程只给公式,不解释逆元怎么求。上面的
extended_gcd函数是核心工具。记住,当gcd(N_i, m_i) == 1时,inv_i就是N_i在模m_i下的乘法逆元,满足(N_i * inv_i) % m_i == 1。如果不互质,则需要更一般的CRT解法,这里我们暂不展开。
3.3 盈不足术:封装通用求解函数
我们将推导出的公式封装成一个健壮的函数。
def solve_profit_and_loss(price1, outcome1, price2, outcome2):
"""
解盈不足问题(线性情况)。
outcome: 正数表示盈,负数表示不足。
"""
# 检查两次出价是否相同
if price1 == price2:
raise ValueError("两次假设的每人出钱数不能相同。")
# 计算总人数 (公式:人数 = (盈 + 不足) / (出价差) )
# 注意:outcome2 若为不足,传入时应为负数。
total_people = (outcome1 - outcome2) / (price1 - price2) # outcome1 - outcome2 相当于盈 + |不足|
# 计算总物价
total_cost = price1 * total_people - outcome1
# 检查结果是否为整数(古算题中人数物价通常为整数)
if total_people.is_integer() and total_cost.is_integer():
return int(total_people), int(total_cost)
else:
# 返回浮点数结果,并提示可能非整数解
return total_people, total_cost
# 测试原题
people, cost = solve_profit_and_loss(8, 3, 7, -4) # 第二次不足4,所以传入-4
print(f"人数: {people}, 物价: {cost}") # 输出: 人数: 7, 物价: 53
# 测试另一个例子:“人出九,盈十一;人出六,不足六。”
people2, cost2 = solve_profit_and_loss(9, 11, 6, -6)
print(f"人数: {people2}, 物价: {cost2}") # 输出: 人数: 17, 物价: 142 (验证:9*17-11=142, 6*17+6=108? 等等)
# 等等,6*17+6=108,不等于142。说明我们公式推导有误?检查:9p = c+11, 6p = c-6。相减得3p=17, p=17/3 不是整数。原题数据可能非整数?函数返回了浮点数。
注意事项 :这里暴露了一个关键点!我最初测试时随手编的数据
(9,11,6,-6)导致了非整数解。这提醒我们: 盈不足术的线性公式假设了问题是严格线性的,并且解是整数。 实际编写时,必须处理非整数解的情况,或者像上面那样在函数内部做校验。同时,要仔细检查公式推导。正确的公式推导过程应该是: 由price1 * p = cost + outcome1和price2 * p = cost + outcome2(其中 outcome2 若为不足,则是负值)。 两式相减:(price1 - price2) * p = outcome1 - outcome2。 所以p = (outcome1 - outcome2) / (price1 - price2)。 然后代入任一式求cost。 我上面的代码逻辑是正确的,但例子数据本身可能没有整数解,这正体现了程序校验的价值。
3.4 河妇荡杯:实现通用分数方程求解
这是综合性最强的一个。我们需要先实现LCM和GCD的计算。
import math
from functools import reduce
def lcm_of_list(numbers):
"""计算一个整数列表的最小公倍数"""
def lcm(a, b):
return abs(a * b) // math.gcd(a, b)
return reduce(lcm, numbers)
def solve_shared_cup(share_list, total_cups):
"""
解决河妇荡杯类问题。
Args:
share_list (list): 每几人共享一个碗,如 [2, 3, 4] 表示二人共饭,三人共羹,四人共肉。
total_cups (int): 总碗数。
Returns:
int or float: 客人数。如果无整数解,返回浮点数并警告。
"""
# 计算所有份额的最小公倍数
lcm_val = lcm_of_list(share_list)
# 计算通分后的系数和
sum_coeff = sum(lcm_val // share for share in share_list)
# 计算客人总数 x = (total_cups * lcm_val) / sum_coeff
x = (total_cups * lcm_val) / sum_coeff
# 验证 x 是否为整数,以及每个 x/share 是否为整数
if not x.is_integer():
print(f"警告:计算出的客人数 {x} 不是整数。请检查题目数据。")
return x
x_int = int(x)
# 检查每种碗的数量是否为整数
for share in share_list:
if x_int % share != 0:
print(f"警告:按 {share} 人共享计算,碗数 ({x_int}/{share}) 不是整数。")
return x # 返回浮点数
# 一切正常,返回整数解
return x_int
# 测试原题
guests = solve_shared_cup([2, 3, 4], 65)
print(f"客人数为: {guests}") # 输出: 客人数为: 60
# 验证:饭碗 60/2=30, 汤碗 60/3=20, 肉碗 60/4=15, 总和 65。
# 测试一个变体:二人共饭,三人共羹,五人共肉,凡用杯三十一。
guests2 = solve_shared_cup([2, 3, 5], 31)
print(f"客人数为: {guests2}") # 输出: 客人数为: 30
# 验证:30/2=15, 30/3=10, 30/5=6, 总和31,正确。
核心技巧 :
math.gcd是Python标准库自带的计算最大公约数的函数,非常方便。functools.reduce函数可以将一个二元操作(我们定义的lcm函数)累积地应用到一个序列上,从而轻松计算列表所有元素的LCM。这是函数式编程的一个小技巧,能让代码更简洁。
4. 项目整合与可视化拓展
把以上分散的函数整合成一个工具包,并利用Python强大的可视化库Matplotlib,将枯燥的计算过程变成生动的图像,能让这个项目更加完整和有趣。
4.1 构建一个古算题求解工具包
我们可以创建一个Python模块文件,比如 chinese_math_problems.py ,将上述所有函数封装进去,并添加一些辅助功能。
# chinese_math_problems.py
"""
中国古代数学问题Python求解工具包
包含鸡兔同笼、物不知数、盈不足、河妇荡杯等经典问题的求解函数。
"""
import math
from functools import reduce
from typing import Tuple, Optional, List, Union
def chicken_rabbit(heads: int, legs: int) -> Optional[Tuple[int, int]]:
"""鸡兔同笼问题求解(公式法)"""
if legs % 2 != 0 or not (2 * heads <= legs <= 4 * heads):
return None
rabbits = (legs - 2 * heads) // 2
chickens = heads - rabbits
if rabbits >= 0 and chickens >= 0:
return chickens, rabbits
return None
def crt_bruteforce(remainders: List[int], modulos: List[int], limit: int = 1000) -> Optional[int]:
"""中国剩余定理问题遍历求解"""
for n in range(1, limit + 1):
if all(n % m == r for r, m in zip(remainders, modulos)):
return n
return None
def profit_loss(price1: float, outcome1: float, price2: float, outcome2: float) -> Tuple[float, float]:
"""盈不足问题求解(线性)"""
if price1 == price2:
raise ValueError("Prices must be different.")
people = (outcome1 - outcome2) / (price1 - price2)
cost = price1 * people - outcome1
return people, cost
def shared_cups(share_list: List[int], total_cups: int) -> Union[int, float]:
"""河妇荡杯问题求解"""
lcm_val = reduce(lambda a, b: abs(a*b)//math.gcd(a, b), share_list)
sum_coeff = sum(lcm_val // s for s in share_list)
x = (total_cups * lcm_val) / sum_coeff
return x if not x.is_integer() else int(x)
# 可以添加一个统一的命令行接口或说明文档
if __name__ == "__main__":
print("中国古代数学问题求解器")
print("1. 鸡兔同笼: chicken_rabbit(heads, legs)")
print("2. 物不知数(遍历): crt_bruteforce([余数列表], [模数列表])")
print("3. 盈不足: profit_loss(出价1, 结果1, 出价2, 结果2)")
print("4. 河妇荡杯: shared_cups([份额列表], 总碗数)")
这样,其他人就可以通过 import chinese_math_problems 来使用这些函数了。
4.2 使用Matplotlib进行解题过程可视化
可视化能极大提升理解趣味性。我们以“鸡兔同笼”的暴力枚举法为例,将搜索过程画出来。
import matplotlib.pyplot as plt
import numpy as np
def visualize_chicken_rabbit(heads, legs):
"""可视化鸡兔同笼的枚举过程"""
chickens_list = []
total_legs_list = []
solution = None
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
# 子图1:枚举过程动画(静态展示为折线)
for chickens in range(heads + 1):
rabbits = heads - chickens
total_legs = 2 * chickens + 4 * rabbits
chickens_list.append(chickens)
total_legs_list.append(total_legs)
if total_legs == legs:
solution = (chickens, rabbits)
ax1.plot(chickens_list, total_legs_list, 'b-', label='计算总脚数')
ax1.axhline(y=legs, color='r', linestyle='--', label=f'目标脚数 ({legs})')
if solution:
ax1.plot(solution[0], legs, 'go', markersize=10, label=f'解: 鸡{solution[0]},兔{solution[1]}')
ax1.set_xlabel('鸡的数量')
ax1.set_ylabel('总脚数')
ax1.set_title('枚举搜索过程')
ax1.grid(True)
ax1.legend()
# 子图2:解的比例饼图
if solution:
labels = ['鸡', '兔']
sizes = [solution[0], solution[1]]
colors = ['lightcoral', 'lightskyblue']
ax2.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90)
ax2.axis('equal')
ax2.set_title('鸡兔比例分布')
else:
ax2.text(0.5, 0.5, '无解', ha='center', va='center', fontsize=15)
ax2.set_title('无有效解')
plt.tight_layout()
plt.show()
return solution
# 调用可视化函数
sol = visualize_chicken_rabbit(35, 94)
print(f"可视化求解结果: {sol}")
运行这段代码,你会得到两张并排的图。左边图表展示了随着鸡的数量从0增加到35,总脚数的变化曲线,那条红色的虚线代表目标脚数94,绿色圆点就是两者的交点,即解。右边的饼图直观展示了鸡和兔的比例。这种视觉反馈,比单纯打印出数字 (23, 12) 要印象深刻得多。
对于“盈不足”问题,我们也可以可视化两种假设下的盈亏直线,其交点就是解。
def visualize_profit_loss(price1, outcome1, price2, outcome2):
"""可视化盈不足问题的线性关系"""
# 根据公式推导直线: cost = price * people - outcome
# 我们可以画 people-cost 的关系图
people_range = np.linspace(0, 20, 100) # 假设人数范围0-20
cost_line1 = price1 * people_range - outcome1
cost_line2 = price2 * people_range - outcome2
plt.figure(figsize=(8, 5))
plt.plot(people_range, cost_line1, label=f'出价{price1}钱: cost = {price1}*p - {outcome1}')
plt.plot(people_range, cost_line2, label=f'出价{price2}钱: cost = {price2}*p - {outcome2}')
# 计算交点(即解)
people, cost = profit_loss(price1, outcome1, price2, outcome2)
if people and cost:
plt.plot(people, cost, 'ro', markersize=8, label=f'解 (p={people:.1f}, c={cost:.1f})')
plt.axvline(x=people, color='gray', linestyle=':', alpha=0.5)
plt.axhline(y=cost, color='gray', linestyle=':', alpha=0.5)
plt.xlabel('人数 (p)')
plt.ylabel('物价 (c)')
plt.title('盈不足问题线性关系图')
plt.grid(True, alpha=0.3)
plt.legend()
plt.show()
visualize_profit_loss(8, 3, 7, -4)
这张图会清晰地显示出两条直线,它们的交点坐标就是所求的人数和物价。通过可视化,抽象的代数关系变成了直观的几何图形,理解起来毫不费力。
5. 常见问题、优化与深度思考
在实际编码和教学过程中,我遇到了不少典型问题。这里总结一下,方便你避坑。
5.1 典型错误与调试技巧
-
整数除法与浮点数精度 :这是新手最容易出错的地方。在Python 3中,
/是浮点除法,//是整数除法。在鸡兔同笼的公式法里,rabbits = (legs - 2 * heads) // 2必须用//,因为兔子的数量必须是整数。但在盈不足术里,total_people = (outcome1 - outcome2) / (price1 - price2)可能得到浮点数,所以用/。务必根据业务逻辑选择。- 调试技巧 :在怀疑除法出问题时,用
print(type(variable))打印变量类型,或者用print(f”{variable:.2f}”)控制浮点数输出格式。
- 调试技巧 :在怀疑除法出问题时,用
-
循环边界与效率 :在暴力枚举物不知数时,如果没设置
search_limit,并且问题无解,程序会陷入无限循环(实际上直到整数上限)。 务必为遍历设置一个合理的上限或退出条件 。- 优化技巧 :对于模数两两互质的情况,解在
0到所有模数乘积之间唯一。可以将上限设为reduce(lambda x, y: x*y, modulos)。
- 优化技巧 :对于模数两两互质的情况,解在
-
输入验证与鲁棒性 :我们的函数不能假设用户输入都是合理的。例如,鸡兔同笼传入
heads=10, legs=31(奇数),或者盈不足传入price1=price2。 健壮的程序必须进行防御性检查 ,并给出清晰的错误提示,而不是抛出晦涩的异常或返回错误结果。- 最佳实践 :在每个函数开头,对参数进行有效性校验。使用
if...raise ValueError(...)提前终止并告知原因。
- 最佳实践 :在每个函数开头,对参数进行有效性校验。使用
-
列表索引与zip函数 :在同时遍历余数列表和模数列表时,要确保两个列表长度一致。使用
zip(remainders, modulos)是安全且Pythonic的方式。如果列表长度不同,zip会以短的为准,这可能掩盖错误。可以添加len(remainders) == len(modulos)的断言。
5.2 算法优化方向
-
物不知数问题 :我们实现了遍历法和CRT法。CRT法效率极高(O(n)),但要求模数互质。对于非互质的情况,可以搜索“扩展中国剩余定理”(Excrt)的算法,其核心是每次合并两个同余方程,利用扩展欧几里得算法求解。这是算法竞赛的常见考点,有兴趣可以深入研究。
-
河妇荡杯问题 :我们通分后求解。当份额列表很大时,计算LCM可能导致中间结果非常大(可能溢出)。可以考虑使用
math.lcm(Python 3.9+)或者更精细的质因数分解法来求LCM,避免大数相乘。 -
方程组求解通用化 :鸡兔同笼和盈不足本质都是线性方程组。我们可以引入
numpy库,使用numpy.linalg.solve来求解任意线性方程组。这样就能处理更复杂的古算题,比如“三畜问题”(牛、羊、猪的价格方程)。这体现了从特殊到一般的编程思维提升。
import numpy as np
# 例如:牛五羊二,共价十两;牛二羊五,共价八两。问牛羊肉价各几何?
# 5x + 2y = 10
# 2x + 5y = 8
A = np.array([[5, 2], [2, 5]])
B = np.array([10, 8])
try:
X = np.linalg.solve(A, B)
print(f"牛价: {X[0]:.2f} 两, 羊价: {X[1]:.2f} 两") # 输出: 牛价: 2.00 两, 羊价: 0.00 两?这结果显然不对,说明方程列错了。
# 重新审题:“共价十两”可能指的是总价,方程组应为 5x+2y=10, 2x+5y=8。解出来是 x=34/21≈1.62, y=20/21≈0.95。
except np.linalg.LinAlgError:
print("方程组无唯一解")
5.3 项目延伸思考
把这个项目做深,远不止解几道题。你可以从以下几个方向延伸:
-
构建Web应用或GUI :使用
Flask或Django搭建一个简单的网页,用户在下拉框选择问题类型,输入参数,点击按钮得到结果和可视化图表。或者用Tkinter、PyQt做一个桌面小工具。这能练习全栈技能。 -
与历史结合,打造互动学习材料 :为每道题添加详细的历史背景、原文出处和算法原理说明。用
Jupyter Notebook来组织,将题目、代码、可视化、讲解文字融为一体,非常适合教学或分享。 -
探索更多古算题 :中国古代数学宝库还有很多明珠,比如“百钱百鸡问题”(不定方程)、“圆田术”(圆周率近似计算)、“开方术”(数值迭代求根)、“方程术”(线性方程组矩阵解法)。用Python实现它们,是对算法和数学的绝佳锻炼。
-
性能测试与对比 :写一个脚本,用
timeit模块对比同一问题不同算法(如遍历 vs CRT)在大量数据下的性能差异。这能让你直观感受算法优化的威力。
通过这个项目,你收获的不仅仅是几行Python代码,更是一种“计算思维”:如何将现实世界(哪怕是古代的)的问题,抽象成数学模型,再翻译成计算机能理解的指令。这种能力,是编程的核心价值所在。我自己的体会是,在实现这些古老问题的过程中,常常会惊叹于古人的智慧,他们在一千多年前就已经在用非常精炼的模型描述世界了。而用现代工具去重现和验证这些智慧,本身就是一种穿越时空的乐趣。最后一个小建议,在动手编码前,最好先在纸上把问题的数学关系理清楚,画个草图,这能帮你省下大量调试时间。
更多推荐


所有评论(0)