1. 从“鸡兔同笼”到“大衍求一”:中国古代数学的魅力与智慧

聊起数学,很多人第一反应可能是欧几里得的几何、牛顿的微积分,或者现代计算机科学里的算法。但如果你静下心来,翻开《九章算术》或者《孙子算经》,你会发现另一个充满生活气息与实用智慧的数学世界。中国古代数学,它不像西方公理化体系那样追求抽象的逻辑演绎,而是扎根于土地丈量、粮食分配、工程计算等实际问题,发展出了一套独特的算法体系。我们今天聊的“中国古代数学问题”,远不止是教科书里那道经典的“鸡兔同笼”,它背后是一整套解决实际问题的思维模式、计算工具和算法思想。无论是想了解传统文化中的科学精神,还是想从古人的智慧中寻找解决现代问题的灵感,甚至只是想给孩子讲几个有趣的数学故事,这片领域都值得深挖。它不艰深,却充满巧思;它很古老,但其中的算法思想至今仍在计算机科学、密码学等领域焕发新生。

2. 核心思路:以“术”驭“题”的实用主义数学

中国古代数学的核心,可以概括为“以题为纲,以术为法”。这里的“题”是具体的生产、生活问题,“术”则是解决这类问题的固定算法程序。古人并不热衷于证明“为什么”,而是更关注“怎么做”才能得到正确答案。这种实用主义的导向,塑造了中国古算的几个鲜明特点。

2.1 算法化与程序化思维

这是中国古算最精髓的部分。每一个经典问题,都配套有一个甚至多个成熟的“术”(算法)。比如“更相减损术”求最大公约数,“盈不足术”解盈亏类问题,“方程术”解线性方程组。这些“术”步骤清晰,如同今天的计算机程序,只要按部就班执行,就能得到结果。这种思维模式,与现代的程序设计思想有异曲同工之妙。学习这些“术”,不仅是学一个数学方法,更是锻炼一种将复杂问题分解为可执行步骤的逻辑能力。

2.2 筹算:独特的计算工具与位值制

在纸笔普及之前,中国古人用算筹进行运算。这些小竹棍摆成的数字系统,是当时世界上最先进的十进制位值制记数法之一。通过纵横两种摆法表示数字,并遵循“一纵十横,百立千僵”的规则,可以实现复杂的加、减、乘、除乃至开方运算。筹算的过程是动态和可视化的,运算过程本身就在桌面上展开。理解筹算,就能理解为何中国古代的算术著作中充满了“置”、“列”、“以……乘之”、“退位”等操作术语,这些都是对实际拨弄算筹动作的描述。

2.3 几何的代数化倾向

与古希腊几何追求图形纯粹的逻辑关系不同,中国古代几何(称“勾股”或“少广”)强烈地与代数计算相结合。最著名的勾股定理,在中国被称为“勾股术”,其重点不在于证明直角三角形的三边关系永恒成立,而在于利用“勾三股四弦五”这类特例或一般公式(勾股各自乘,并而开方除之,即弦)去解决实际测量问题,如测山高、谷深、距离等。几何问题最终都转化为代数方程来求解。

2.4 开放性问题的构造与解决

许多经典问题,如“百鸡问题”、“物不知数”(中国剩余定理原型),本身都是不定方程或同余方程组问题。它们往往有多个解,甚至无穷多组解。古人不仅满足于找到一组解,还会探讨解的构造规律和通解形式。这种对问题结构本身的深入探究,已经超越了单纯的实用计算,触及了数论的核心。

3. 经典问题深度解析与算法实现

我们选取几个最具代表性的问题,不仅看其表面,更深入其算法内核,并用现代的眼光和工具(比如Python)来重新理解和实现它们,看看古人的智慧如何穿越时空。

3.1 鸡兔同笼:二元一次方程组的“抬脚法”智慧

问题原型 (出自《孙子算经》):今有雉兔同笼,上有三十五头,下有九十四足。问雉兔各几何?

古法“抬脚法”解析 : 这可能是最广为人知的古算解题巧思。其思路不是设未知数X、Y,而是通过一个巧妙的假设来简化问题。

  1. 假设与操作 :假设让所有的鸡和兔子都抬起一半的脚(或者理解成每只动物抬起两只脚)。这个操作的关键在于,鸡有2只脚,抬起2只就坐地上了;兔子有4只脚,抬起2只还剩2只站着。
  2. 逻辑推导
    • 总脚数94,抬起一半是47只脚(94/2)。这47可以理解为“抬脚之后,还站在地上的脚的总数”。
    • 此时,每只鸡的脚全抬起来了,站着的脚数为0。每只兔子还剩2只脚站着。
    • 总头数是35,代表有35只动物。如果全是鸡,站着的脚数应为0。现在有47只脚站着,这多出来的47只脚,全是兔子贡献的。
    • 每只兔子贡献2只站着的脚,所以兔子的数量就是 47 / 2 = 23.5?这里显然不对。 关键点来了 :我们之前除以2的操作(抬一半脚)是物理意义的,但计算时,更严谨的古法叙述是:“足数减去头数的两倍,剩余的就是兔子脚数的两倍(再除以2得兔子数)”。
    • 正确推导:总足数94,如果全是鸡,应有 35 * 2 = 70 足。实际多出 94 - 70 = 24 足。每只兔子比鸡多2足,所以兔子数为 24 / 2 = 12。则鸡数为 35 - 12 = 23。 “抬脚法”的生动描述,本质上就是这个计算过程的形象化比喻。

现代代码实现与对比

def chicken_rabbit_classic(heads, legs):
    """
    经典解法(对应古法):解方程组
    鸡 + 兔 = heads
    2*鸡 + 4*兔 = legs
    """
    # 兔子数 = (总腿数 - 2 * 总头数) / 2
    rabbits = (legs - 2 * heads) // 2
    chickens = heads - rabbits
    if rabbits < 0 or chickens < 0 or (2*chickens + 4*rabbits) != legs:
        return None # 无解
    return chickens, rabbits

def chicken_rabbit_lift_feet(heads, legs):
    """
    模拟“抬脚法”思维:让所有动物抬起两只脚
    抬脚后,地上剩余脚数 = legs - 2 * heads
    这些脚全是兔子的(每兔剩2脚)
    """
    feet_on_ground = legs - 2 * heads
    rabbits = feet_on_ground // 2
    chickens = heads - rabbits
    # 验证条件同上
    if rabbits < 0 or chickens < 0 or feet_on_ground % 2 != 0:
        return None
    return chickens, rabbits

# 测试
heads, legs = 35, 94
print(f"经典解法: 鸡{chicken_rabbit_classic(heads, legs)[0]}只, 兔{chicken_rabbit_classic(heads, legs)[1]}只")
print(f"抬脚法思维: 鸡{chicken_rabbit_lift_feet(heads, legs)[0]}只, 兔{chicken_rabbit_lift_feet(heads, legs)[1]}只")
# 输出均为:鸡23只,兔12只

注意 :“抬脚法”在教导孩子时极具启发性,它绕开了抽象的代数符号,用场景化的操作引导逻辑推理。但在编程实现时,你会发现其核心计算式与方程组解法是等价的。这正体现了古算“寓理于算”的特点:道理隐藏在算法操作中。

3.2 物不知数:中国剩余定理的雏形与“大衍求一术”

问题原型 (出自《孙子算经》):今有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?

问题翻译 :一个数,除以3余2,除以5余3,除以7余2,求这个数的最小正整数解。

古法“孙子定理”歌诀解析 : 明朝数学家程大位在《算法统宗》中将其总结成歌诀:“三人同行七十稀,五树梅花廿一枝,七子团圆正半月,除百零五便得知。”

  1. 找出“衍数” :对于模数3、5、7,分别找出能被另外两个模数整除,且除以自身模数余1的数。
    • 对于模数3:找一个是5和7公倍数(即35的倍数),且除以3余1的数。35除以3余2,70除以3余1。所以“衍数”是70。对应“七十稀”。
    • 对于模数5:找一个是3和7公倍数(21的倍数),且除以5余1的数。21除以5余1。所以“衍数”是21。对应“廿一枝”。
    • 对于模数7:找一个是3和5公倍数(15的倍数),且除以7余1的数。15除以7余1。所以“衍数”是15。“正半月”指15(半月十五天)。
  2. 计算“总数” :将每个余数乘以对应的“衍数”,然后相加。
    • 总数 = 2 * 70 + 3 * 21 + 2 * 15 = 140 + 63 + 30 = 233
  3. 求最小解 :总数减去3、5、7的最小公倍数105的整数倍,直到得到小于105的正数。
    • 233 - 2 * 105 = 23。所以答案是23。

背后的“大衍求一术” : 上述歌诀是特例(模数两两互质)。宋代秦九韶在《数书九章》中将其推广到一般情况,提出了“大衍求一术”,系统解决了一次同余方程组问题。其核心是求解形如 a * x ≡ 1 (mod m) x ,这个 x 就是“乘率”,也就是上文中的“衍数”的求解通法。这被认为是现代数论中“中国剩余定理”的算法化先驱。

现代代码实现(通用中国剩余定理)

def extended_gcd(a, b):
    """扩展欧几里得算法,返回 (gcd, x, y) 使得 a*x + b*y = 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 chinese_remainder_theorem(moduli, remainders):
    """
    解同余方程组:x ≡ remainders[i] (mod moduli[i])
    要求 moduli 两两互质
    """
    n = len(moduli)
    # 计算所有模数的乘积 N
    N = 1
    for m in moduli:
        N *= m

    result = 0
    for i in range(n):
        # 计算 Ni = N / moduli[i]
        Ni = N // moduli[i]
        # 使用扩展欧几里得算法求 Ni 在模 moduli[i] 下的逆元 inv_Ni
        gcd, inv_Ni, _ = extended_gcd(Ni, moduli[i])
        if gcd != 1:
            raise ValueError("模数不两两互质,无法使用标准CRT")
        # 累加:remainders[i] * Ni * inv_Ni
        result += remainders[i] * Ni * inv_Ni

    # 返回最小正整数解
    return result % N

# 解“物不知数”问题
moduli = [3, 5, 7]
remainders = [2, 3, 2]
solution = chinese_remainder_theorem(moduli, remainders)
print(f“物不知数问题的解(最小正整数)是:{solution}”) # 输出 23

实操心得 :理解“大衍求一术”的关键在于“求一”,即寻找那个“乘率”。在现代密码学(如RSA算法)和计算机科学(如哈希表冲突解决、分布式系统时钟同步)中,中国剩余定理有广泛应用。手动推导一遍“孙子歌诀”的过程,比直接套公式更能深刻理解模运算的奥妙。

3.3 百鸡问题:不定方程与整数解枚举

问题原型 (出自《张邱建算经》):今有鸡翁一,值钱五;鸡母一,值钱三;鸡雏三,值钱一。凡百钱买鸡百只,问鸡翁、母、雏各几何?

问题翻译 :公鸡5文1只,母鸡3文1只,小鸡1文3只。用100文钱买100只鸡,问公鸡、母鸡、小鸡各多少只?

古法解析与“方程术”的局限 : 这是一个三元一次不定方程组。设公鸡x只,母鸡y只,小鸡z只,则有:

  1. x + y + z = 100 (总数)
  2. 5x + 3y + z/3 = 100 (总价) 古人没有“负数”和完整的分数运算概念,但通过“方程术”(即线性方程组消元法)的变通,可以将其化为二元问题。由方程1和2消去z,得到:7x + 4y = 100。 接下来,就需要寻找这个二元一次方程的正整数解。古算中常用的是“试值法”或“求一术”的变体,通过分析系数的性质来缩小搜索范围。

算法思路与现代枚举法

  1. 范围确定 :由 7x + 4y = 100 且 x, y 为正整数,可知 x 最大不超过 100/7 ≈ 14。同时,总价约束下,公鸡最多买 20只(100/5),但结合方程,实际范围更小。
  2. 整数解条件 :由 4y = 100 - 7x,要求 (100 - 7x) 能被4整除。这等价于 100 - 7x ≡ 0 (mod 4)。因为100 mod 4 = 0,所以要求 7x ≡ 0 (mod 4)。7 mod 4 = 3,即要求 3x ≡ 0 (mod 4),所以 x 必须是4的倍数。
  3. 枚举求解 :x 是4的倍数,且在合理范围内(x>0,且 y = (100-7x)/4 > 0)。所以 x 可能为 4, 8, 12。代入计算:
    • x=4: y = (100-28)/4 = 18, z = 100-4-18 = 78。检查价格:5 4 + 3 18 + 78/3 = 20+54+26=100。符合。
    • x=8: y = (100-56)/4 = 11, z = 100-8-11 = 81。价格:40+33+27=100。符合。
    • x=12: y = (100-84)/4 = 4, z = 100-12-4 = 84。价格:60+12+28=100。符合。
    • x=0(全买母鸡和小鸡)或 x=16(y为负)不在正整数解内。 故有三组解:(4,18,78), (8,11,81), (12,4,84)。

现代代码实现(优化枚举)

def hundred_chickens():
    solutions = []
    # 直接根据约束条件枚举公鸡数量 x
    for x in range(0, 21): # 公鸡最多买100/5=20只
        # 枚举母鸡数量 y
        for y in range(0, 34): # 母鸡最多买100/3≈33只
            z = 100 - x - y
            if z < 0 or z % 3 != 0: # 小鸡数量必须是非负整数且是3的倍数(因为3只一起卖)
                continue
            # 检查总价
            if 5*x + 3*y + z//3 == 100:
                solutions.append((x, y, z))
    return solutions

sols = hundred_chickens()
for sol in sols:
    print(f“公鸡:{sol[0]}只, 母鸡:{sol[1]}只, 小鸡:{sol[2]}只”)
# 输出三组解

注意事项 :百鸡问题展示了古代对不定方程整数解的处理。在编程枚举时,合理利用数学约束(如z必须是3的倍数)可以大幅减少循环次数,从最笨的100 100 100降到20*30级别。这正是古算“优化”思想的体现:在计算工具受限的时代,通过数学洞察力来简化计算。

4. 从古算到现代:算法思想的传承与工具模拟

理解了具体问题,我们再来看看如何系统性地学习和“复现”古算。这不仅仅是解题,更是体验一种不同的数学文化。

4.1 筹算的模拟与理解

要真正理解《九章算术》中的运算步骤,最好能模拟筹算过程。我们可以用字符画来模拟算筹布列。

算筹记数规则

  • 纵式与横式交替,从右向左,个位用纵式,十位用横式,百位用纵式,以此类推。
  • 数字1-5用相应数目的算筹表示;6-9用上面一根(代表5)加下面相应数目的算筹(代表1-4)表示。
  • 零用空位表示。

模拟一个乘法计算(以《孙子算经》中的“九九表”和乘法为例) : 假设计算 123 × 45。

  1. 布数 :将被乘数123布于上位(上方一行),乘数45布于下位(下方一行)。中间和下面留出空位存放部分积和最终积。
  2. 乘算 :从乘数的最高位(4)开始,依次去乘被乘数的每一位(1,2,3),将部分积依次错位相加。这个过程需要不断在筹盘上移动、叠加算筹。
  3. 最终结果 :所有部分积累加完毕,中位留下的数字就是积。

用纯文本模拟这个过程非常繁琐,但我们可以用程序逻辑来理解其步骤,并输出关键步骤的“筹式”快照。其核心是 位值制和机械化的错位相加 ,这与今天我们在纸上列竖式乘法的本质完全相同。

4.2 《九章算术》中的核心“术”及其现代实现

《九章算术》作为中国古算的奠基之作,包含了246个问题及其“术”。我们可以挑选几个核心算法,用现代代码实现,感受其通用性。

4.2.1 更相减损术(求最大公约数)

def gengxiang_jiansun(a, b):
    """更相减损术求最大公约数"""
    steps = []
    # “可半者半之”,先约去公因子2
    factor = 1
    while a % 2 == 0 and b % 2 == 0:
        a //= 2
        b //= 2
        factor *= 2
        steps.append(f“约去公因子2:a={a}, b={b}, 累积因子={factor}”)

    # “以少减多,更相减损”
    while a != b:
        if a > b:
            a = a - b
            steps.append(f“{a+b} - {b} = {a}”)
        else:
            b = b - a
            steps.append(f“{b+a} - {a} = {b}”)
    gcd = a * factor # “等数”乘以之前约去的2的幂
    return gcd, steps

# 示例:求 98 和 63 的 gcd
gcd, process = gengxiang_jiansun(98, 63)
print(f“更相减损术求得的GCD是:{gcd}”)
print(“计算过程:”)
for step in process:
    print(f“  {step}”)
# 输出:GCD为7。过程包含约去2(98,63不能同约),然后98-63=35, 63-35=28, 35-28=7, 28-7=21, 21-7=14, 14-7=7。

核心要点 :“更相减损术”是欧几里得算法的减法版本,原理相同( gcd(a,b) = gcd(a-b,b) ),但效率较低(特别是两数相差大时)。其“可半者半之”的预处理是亮点,体现了优化思想。

4.2.2 盈不足术(解盈亏类问题与一般线性问题) 盈不足术最初用于解决“多人买物,人出X盈Y,人出Z不足W”这类问题,后来发展为一种求解一般线性方程 ax + b = 0 的万能方法(双假设法)。

问题 :今有共买物,人出八,盈三;人出七,不足四。问人数、物价各几何? 现代方程 :设人数p,物价m。有 8p = m + 3; 7p = m - 4。

盈不足术解法

  1. 置所出率:8,7。置盈、不足:盈3,不足4。
  2. 交叉相乘:8 4 + 7 3 = 32 + 21 = 53。此为“维乘之并”。
  3. 相并:盈+不足 = 3+4=7。此为“并盈不足为法”。
  4. 人数 = 维乘并 / 并盈不足 = 53 / 7。(这不是整数,古人用分数表示)
  5. 物价 = 出率1 * 不足 + 出率2 * 盈 / 并盈不足?更通用的公式是:物价 = (8 4 + 7 3) / (3+4)?这里需要根据公式推导。实际上,标准盈不足公式为:
    • 设两次假设每人出钱为 a1, a2,对应盈不足为 b1, b2(不足为负)。
    • 则人数 = (b2 - b1) / (a1 - a2)?不对。正确公式(来自《九章算术》):
    • 物价 = (a2 b1 + a1 b2) / (b1 + b2) (注意:b1是盈,b2是不足,b2视为负数?古人不分正负,用“不足”表示。在公式中,b1和b2都取正值相加)。
    • 代入:a1=8,b1=3(盈);a2=7,b2=4(不足)。物价 = (7 3 + 8 4) / (3+4) = (21+32)/7 = 53/7。
    • 人数 = (a1 - a2) / (b1 + b2) ?检验:人数 = (8-7)/(3+4)=1/7?这显然不对。实际上,由 8p = m+3 和 7p = m-4,两式相减得 p = 7,代入得 m=53。所以人数是7,物价是53。
    • 古法计算人数可能是:物价 / 出率 再调整?这里显示出盈不足术在解决非标准盈亏问题时的复杂性。更一般的“双假设法”适用于任何线性关系。

现代视角下的“双假设法” : 对于方程 f(x)=0,如果我们猜两个值 x1, x2,得到 f(x1)=y1, f(x2)=y2(y1, y2一正一负),则可以用线性插值求根: x = (x1*y2 - x2*y1) / (y2 - y1) 。这正是盈不足术的推广。

def yingbuzu(x1, y1, x2, y2):
    """
    盈不足术/双假设法求解 f(x)=0 的根。
    x1, x2 为两个猜测值,y1=f(x1), y2=f(x2),且 y1*y2 < 0。
    """
    if y1 * y2 >= 0:
        raise ValueError(“函数值在两点需异号(一盈一不足)”)
    # 线性插值公式
    root = (x1 * y2 - x2 * y1) / (y2 - y1)
    return root

# 解上面的买物问题:设人数为p,则物价m有两种表达:m=8p-3 和 m=7p+4。
# 令 f(p) = (8p-3) - (7p+4) = p - 7。我们要求 f(p)=0,即 p=7。
# 用双假设法:猜 p1=6, 则 f(6)=6-7=-1 (不足)。猜 p2=8, f(8)=8-7=1 (盈)。
p_solution = yingbuzu(6, -1, 8, 1)
print(f“用盈不足术(双假设法)求得的人数为:{p_solution}”) # 输出 7.0

实操心得 :盈不足术体现了古人用试错和比例逼近求解问题的智慧。它是早期数值分析中“试位法”的雏形。在不知道确切函数形式,但能通过实验得到两个观测点时,这种方法非常实用。

5. 古算研习中的常见困惑与释疑

在学习和复现古代数学问题时,常会遇到一些因时代差异和表述不同带来的困惑。

5.1 术语与单位换算的“坑”

古算题中充斥着“丈、尺、寸、步、亩、斛、斗、升、铢、两、斤”等单位。例如《九章算术》方田章,大量涉及面积单位“亩”(1亩=240平方步)和长度单位“步”。如果不进行单位统一,计算会一团糟。

避坑指南 :遇到任何古算题,第一步不是列方程,而是 查清楚所有出现的度量衡单位与现代公制或市制的换算关系,并在计算前全部转换为同一基准单位 。可以制作一个常用单位换算表备用。

5.2 “方程术”与现代矩阵消元的差异

《九章算术》的“方程章”介绍的是线性方程组解法,其“方程”指将系数和常数项摆成方阵(用算筹)的形式进行“直除”(即行之间的加减消元)。这与高斯消元法本质一致。但需要注意:

  • 没有负数 :古人用红黑算筹或正斜摆列来表示“欠”或“负”,但概念上不完善。在复现时,直接使用负数更方便。
  • 全部是整数运算 :古法通过反复的“乘”和“直除”来避免分数,直到消元完成。现代编程中,我们更关注算法稳定性,可以使用分数( Fraction 模块)或浮点数。

5.3 古算题的多解性与开放性思维

像“百鸡问题”有三组解,“河上荡杯”等问题也可能有多解。古人有时只给出一组解(通常是最小正整数解),有时会给出全部解的通项公式(如“物不知数”问题中,答案是“23+105k”)。这提醒我们,读古算时要有 寻找通解、理解解的结构 的意识,而不是满足于一个特解。

5.4 算法效率与现代应用的结合

古算算法如“更相减损术”效率不高,“盈不足术”需要好的初始猜测。我们在用代码实现时,可以:

  • 理解原理,优化实现 :比如“更相减损术”可以很容易改造成更高效的欧几里得除法算法。
  • 古为今用,寻找场景 :例如,“中国剩余定理”在需要处理周期性重叠事件或模运算系统时非常有用。可以尝试用其解决诸如“寻找一个数,它满足多个周期性条件”的实际编程问题。

一个结合场景的思考题 :有一个灯饰,红灯每3秒亮一次,绿灯每5秒亮一次,蓝灯每7秒亮一次。若某一时刻三灯同时亮起,请问至少过多少秒后,三灯会再次同时亮起? 这就是一个求最小公倍数的问题,但若问“三灯同时亮起后,又过了若干秒,此时红灯刚亮完、绿灯已亮2秒、蓝灯已亮1秒,问从三灯同亮到现在过了多久?”,这就转化成了一个“物不知数”问题(模3余0,模5余2?需仔细建模)。将古算问题置于这样的动态场景中,理解会更加深刻。

学习中国古代数学问题,就像与一群跨越千年的智者对话。他们用竹棍排列出数字的森林,在田亩与粮仓之间构建了算法的桥梁。这些算法或许已被更高效的工具取代,但其背后化繁为简的思维、构造模型的智慧、以及执着于解决实际问题的务实精神,依然闪耀着光芒。我个人的体会是,每次用代码重新实现这些古老算法时,都是一次对计算思维本源的追溯。它让你跳出现代编程语言的语法糖和现成库函数,去思考一个最根本的问题:如何用一组清晰、有限的步骤,教会机器(或古人手中的算筹)解决一个具体问题。这种训练,对于今天任何一个与逻辑、算法打交道的人来说,都是一种宝贵的思维淬炼。

更多推荐