SymPy 丢番图方程求解指南:用 diophantine 函数求整数解与参数化通解
【免费下载链接】sympyA computer algebra system written in pure Python项目地址: https://gitcode.com/GitHub_Trending/sy/sympy
本篇技术指南聚焦 SymPy(纯 Python 实现的计算机代数系统)中的丢番图方程(Diophantine Equation)代数求解能力,以官方求解指南 solve-diophantine-equation.md 为骨架,结合 diophantine.py 源码与 test_diophantine.py 测试深入展开。读完本文,你将掌握diophantine函数的完整用法、参数化通解的提取与代入技巧、五类可解方程型的适用范围,以及求解结果在底层是如何被分类与生成的。
什么是丢番图方程
丢番图方程得名于古希腊数学家丢番图(Diophantus,约公元 250 年活跃于亚历山大城),其著作《算术》(Arithmetica)中的 150 道问题奠定了数论研究的早期基础。丢番图方程是形如
$$f(x_1, x_2, \ldots, x_n) = 0, \quad n \geq 2$$
的方程,其中 $x_1, x_2, \ldots, x_n$ 全部限定为整数变量。若能找到 $n$ 个整数 $a_1, a_2, \ldots, a_n$ 使等式成立,则称该方程可解(背景介绍详见 diophantine.rst)。
SymPy 的 Diophantine 模块不仅能判断方程是否有整数解,还能在可能的情况下直接返回参数化的通解。例如求解勾股方程 $a^2 + b^2 = c^2$,会得到:
$$a = 2pq, \quad b = p^2 - q^2, \quad c = p^2 + q^2$$
其中 $p, q \in \mathbb{Z}$ 是求解过程中自动引入的新参数,取遍所有整数值即可参数化无限多个勾股三元组(Pythagorean triples)。
何时使用:与其他求解方式的取舍
寻找丢番图方程的参数化通解,可选方案并不多,官方指南列出了两类替代路径:
- 数值类替代方案:Sage 的
EllipticCurve命令可能为每个变量求出一组相对数值解;也可以直接暴力测试整数取值,例如用嵌套 for 循环遍历一定范围内的值——这种方法效率低,但若你只关心数值较小的解,它完全够用。 - 通用
solve函数:solve默认把变量当作实数或复数处理,只是把一个变量用其他变量表示出来,得到的解类型完全不同。例如对 $a^2 + b^2 = c^2$ 求 $a, b, c$,它只能给出 $a = \pm\sqrt{c^2 - b^2}$ 这种形式,无法体现整数结构。
因此,当需要整数解且希望得到参数化通解时,应使用 Diophantine 模块的diophantine函数。
快速上手:求解勾股方程
下面是官方指南中的入门示例,求解 $a^2 + b^2 = c^2$:
>>> from sympy.solvers.diophantine import diophantine >>> from sympy import symbols, Eq >>> a, b, c = symbols("a, b, c", integer=True) >>> my_syms = (a, b, c) >>> pythag_eq = Eq(a**2 + b**2, c**2) >>> # Solve Diophantine equation >>> d = diophantine(pythag_eq, syms=my_syms) >>> d {(2*p*q, p**2 - q**2, p**2 + q**2)}diophantine接受一个Eq对象或表达式作为输入,返回解的集合(set),其中每个元素是一个元组,元组内各表达式按指定符号顺序对应各变量的解。完整函数签名见源码 diophantine.py:
diophantine(eq, param=symbols("t", integer=True), syms=None, permute=False)eq:待求解方程,可以是Eq对象,也可以是"等于零"的表达式;param:通解中使用的参数符号(默认t),求解时会自动生成t_0, t_1, ...等整数参数;syms:可选的符号序列,决定返回元组中元素的排列顺序;permute:置为True时,对基解进行符号/数值置换,返回尽可能多的等价解(详见后文)。
更丰富的分类型求解示例,可参考 Diophantine API 参考。
基本用法要点
方程可以写成"等于零"的表达式
如果你已经有一个恒等于零的表达式,可以直接求解该表达式。例如把勾股方程写成 $a^2 + b^2 - c^2$ 同样有效:
>>> from sympy.solvers.diophantine import diophantine >>> from sympy import symbols >>> a, b, c = symbols("a, b, c", integer=True) >>> my_syms = (a, b, c) >>> pythag = a**2 + b**2 - c**2 >>> diophantine(pythag, syms=my_syms) {(2*p*q, p**2 - q**2, p**2 + q**2)}底层实现中,源码会先把输入_sympify化,若传入的是Eq则自动转换为eq.lhs - eq.rhs,随后对表达式做展开、因式分解与多项式规整(见 diophantine.py)。
用 syms 参数指定结果中符号的顺序
官方指南强烈建议显式传入syms(元组或列表),以确保返回元组中的元素顺序与之一致,避免混淆。不传syms时,源码按default_sort_key对自由符号排序(diophantine.py),结果同样确定,但显式指定可读性更好。
用 permute 参数获得符号/数值置换解
permute=True会返回基解经符号或数值置换后的全部等价解。例如求解 $a^4 + b^4 = 2^4 + 3^4$:
>>> from sympy import diophantine >>> from sympy.abc import a, b >>> eq = a**4 + b**4 - (2**4 + 3**4) >>> diophantine(eq) {(2, 3)} >>> diophantine(eq, permute=True) {(-3, -2), (-3, 2), (-2, -3), (-2, 3), (2, -3), (2, 3), (3, -2), (3, 2)}源码中置换逻辑依据方程类型分别调用permute_signs与signed_permutations(diophantine.py),仅对「和平方」「偶次幂和」以及齐次三元二次等类型启用,且会检查交叉项系数以决定置换策略。
当前支持的五类丢番图方程
官方指南与 API 参考(diophantine.rst)一致指出,目前diophantine及其辅助函数可求解以下五类方程:
| 方程类型 | 一般形式 | 源码中的类 |
|---|---|---|
| 线性丢番图方程 | $a_1x_1 + a_2x_2 + \ldots + a_nx_n = b$ | Linear(diophantine.py) |
| 一般二元二次方程 | $ax^2 + bxy + cy^2 + dx + ey + f = 0$ | BinaryQuadratic(diophantine.py) |
| 齐次三元二次方程 | $ax^2 + by^2 + cz^2 + dxy + eyz + fzx = 0$ | HomogeneousTernaryQuadratic(diophantine.py) |
| 广义勾股方程 | $a_1x_1^2 + a_2x_2^2 + \ldots + a_nx_n^2 = a_{n+1}x_{n+1}^2$ | GeneralPythagorean(diophantine.py) |
| 一般平方和 | $x_1^2 + x_2^2 + \ldots + x_n^2 = k$ | GeneralSumOfSquares(diophantine.py) |
除上述五类外,源码还内置了Univariate(单变量整系数多项式)与GeneralSumOfEvenPowers(偶次幂和)两种方程型(diophantine.py 与 diophantine.py),遇到不在识别范围内的方程会抛出NotImplementedError。
使用求解结果
从结果中提取表达式
diophantine返回"元组组成的集合",集合内的每个元组按syms顺序对应各个变量的解表达式。例如勾股方程的结果是含一个元组的集合,元组依次对应 $(a, b, c)$。由于无法按下标从 set 中取元素,官方推荐先构造"符号→表达式"字典,再按符号名提取:
>>> from sympy.solvers.diophantine import diophantine >>> from sympy import symbols >>> a, b, c = symbols("a, b, c", integer=True) >>> my_syms = (a, b, c) >>> pythag = a**2 + b**2 - c**2 >>> solution, = diophantine(pythag, syms=my_syms) >>> solution (2*p*q, p**2 - q**2, p**2 + q**2) >>> # Convert set to list >>> solution_dict = dict(zip(my_syms, solution)) >>> solution_dict {a: 2*p*q, b: p**2 - q**2, c: p**2 + q**2} >>> # Extract an expression for one variable using its symbol, here a >>> solution_dict[a] 2*p*q另一种不那么优雅的方式是把集合转成列表再按下标访问。由于极易记错参数顺序,这种方法更容易出错:
>>> from sympy.solvers.diophantine import diophantine >>> from sympy import symbols >>> a, b, c, p, q = symbols("a, b, c, p, q", integer=True) >>> my_syms = (a, b, c) >>> pythag = a**2 + b**2 - c**2 >>> d = diophantine(pythag, syms=my_syms) >>> d {(2*p*q, p**2 - q**2, p**2 + q**2)} >>> # Convert set to list >>> solution_list = list(d) >>> solution_list [(2*p*q, p**2 - q**2, p**2 + q**2)] >>> # Extract a tuple corresponding to a solution >>> solution_first = solution_list[0] >>> solution_first (2*p*q, p**2 - q**2, p**2 + q**2) >>> # Extract an expression for one variable using its order, here a is element number zero >>> solution_first[0] 2*p*q注意:原指南中"c = p2 - q2"系笔误,正确结果应为 $c = p^2 + q^2$,验证可见源码 docstring 与测试输出一致。
操作通解参数
通解中的参数(如 $p, q$)由diophantine自动生成。你可以把它们声明为符号后通过subs代入具体值,得到满足原方程的整数实例。官方推荐的流程是:
- 将参数声明为符号;
- 用
Basic.subs代入取值。
下面把每个变量与其示例值用字典关联起来:
>>> from sympy.solvers.diophantine import diophantine >>> from sympy import symbols >>> my_syms = (a, b, c) >>> pythag = a**2 + b**2 - c**2 >>> d = diophantine(pythag, syms=my_syms) >>> solution_list = list(d) >>> solution_list [(2*p*q, p**2 - q**2, p**2 + q**2)] >>> p, q = symbols("p, q", integer=True) >>> # Substitute in values as the dictionary is created >>> solution_p4q3 = dict(zip(my_syms, [var.subs({p:4, q:3}) for var in solution_list[0]])) >>> solution_p4q3 {a: 24, b: 7, c: 25}注意:必须为生成参数(p、q)声明integer=True,代入数值才会生效;原方程中的变量(a、b、c)倒不强制要求该假设,但显式声明是好习惯(源码 diophantine.py 生成默认参数时同样带integer=True)。
要遍历整个解集,可以用嵌套循环遍历参数取值:
>>> from sympy.solvers.diophantine import diophantine >>> from sympy import symbols >>> a, b, c, p, q = symbols("a, b, c, p, q", integer=True) >>> my_syms = (a, b, c) >>> pythag = a**2 + b**2 - c**2 >>> d = diophantine(pythag, syms=my_syms) >>> solution_list = list(d) >>> # Iterate over the value of parameters p and q >>> for p_val in range(-1,2): ... for q_val in range(-1,2): ... # Substitute in the values of p and q ... pythag_vals = dict(zip(my_syms, [var.subs({p:p_val, q:q_val}) for var in solution_list[0]])) ... # Print out the values of the generated parameters, and the Pythagorean triple a, b, c ... print(f"p: {p_val}, q: {q_val} -> {pythag_vals}") p: -1, q: -1 -> {a: 2, b: 0, c: 2} p: -1, q: 0 -> {a: 0, b: 1, c: 1} p: -1, q: 1 -> {a: -2, b: 0, c: 2} p: 0, q: -1 -> {a: 0, b: -1, c: 1} p: 0, q: 0 -> {a: 0, b: 0, c: 0} p: 0, q: 1 -> {a: 0, b: -1, c: 1} p: 1, q: -1 -> {a: -2, b: 0, c: 2} p: 1, q: 0 -> {a: 0, b: 1, c: 1} p: 1, q: 1 -> {a: 2, b: 0, c: 2}输出中 $p=0$ 或 $q=0$ 会出现退化的平凡解(如 $a=0, b=1, c=1$),这是参数化通解的正常表现,可通过限定参数非零过滤。
验证一个解是否正确
把整数解代回原方程(等于零的表达式),结果应为 0。既可以用字典方式,也可以手动代入:
>>> from sympy.solvers.diophantine import diophantine >>> from sympy import symbols >>> a, b, c, p, q = symbols("a, b, c, p, q", integer=True) >>> my_syms = (a, b, c) >>> pythag = a**2 + b**2 - c**2 >>> d = diophantine(pythag, syms=my_syms) >>> solution_list = list(d) >>> solution_p4q3 = dict(zip(my_syms, [var.subs({p:4, q:3}) for var in solution_list[0]])) >>> # Substitute values in using a dictionary >>> pythag.subs({a: solution_p4q3[a], b: solution_p4q3[b], c: solution_p4q3[c]}) 0 >>> # Manually substitute in values >>> pythag.subs({a: 24, b: 7, c: 25}) 0$24^2 + 7^2 = 625 = 25^2$,验证通过。这一步对任何"手工推导"的解都适用,是排查问题的有效手段。
编程方式提取参数符号
如果需要在程序中自动获取某个解引用的全部参数,可用free_symbols收集:
>>> from sympy.solvers.diophantine import diophantine >>> from sympy import symbols >>> a, b, c, p, q = symbols("a, b, c, p, q", integer=True) >>> my_syms = (a, b, c) >>> pythag = a**2 + b**2 - c**2 >>> # Solve Diophantine equation >>> solution, = diophantine(pythag, syms=my_syms) >>> solution (2*p*q, p**2 - q**2, p**2 + q**2) >>> # Extract parameter symbols >>> set().union(*(s.free_symbols for s in solution)) {p, q}在底层,这类集合运算与解集容器DiophantineSolutionSet的设计相呼应——它继承自set,并额外提供了dict_iterator()(按序产出符号→表达式字典)、subs()(整体代换)和__call__(按参数位置求值)等便捷方法(见 diophantine.py),需要批量处理解集时可直接复用。
底层原理:求解流程与模块结构
理解diophantine的调用链有助于判断何时该用哪个辅助函数。模块结构(详见 diophantine.rst)自上而下为:
diophantine:对外主入口,负责表达式规整、因式分解,并把各因子的解合并diop_solve:对单个(已分解的)方程分派到具体求解器classify_diop:识别方程类型,返回(变量列表, 系数字典, 类型名)diop_linear/diop_quadratic/diop_ternary_quadratic/diop_ternary_quadratic_normal/diop_general_pythagorean/diop_general_sum_of_squares/diop_general_sum_of_even_powers
merge_solution:把子方程的解合并回原方程变量的完整解
主入口diophantine的流程(diophantine.py)大致是:
- 展开表达式、提取自由符号并排序;
- 若传入
syms,先递归求解再按指定顺序重排元组; - 提取分子分母(处理分式方程),因式分解为若干因子;
- 对每个因子调用
classify_diop+diop_solve求解,再用merge_solution合并、按假设过滤无效解; - 若
permute=True,按类型对解做符号/数值置换。
classify_diop依次用all_diop_classes中的每个方程型实例尝试matches()匹配(diophantine.py),例如Linear要求total_degree == 1、Univariate要求变量维数为 1。这一"类型注册 + 逐类匹配"的架构让新增方程类型变得容易。测试文件 test_diophantine.py 中test_diophantine(第 491 行)以及test_diop_ternary_quadratic(第 404 行)、test_diop_general_sum_of_squares_quick(第 595 行)等用例覆盖了各类方程的参数化解输出,可作为预期行为的权威参考。
并非所有方程都可解
无解的情形
某些丢番图方程没有整数解,此时diophantine返回空集set()。例如表达式 $2x + 4y - 3$(视为等于零):系数 $2$、$4$ 均为偶数,故 $2x + 4y$ 恒为偶数,而常数 $3$ 是奇数,偶数不可能等于奇数,因此无解:
>>> from sympy.solvers.diophantine import diophantine >>> from sympy import symbols >>> x, y = symbols("x, y", integer=True) >>> diophantine(2*x + 4*y - 3, syms=(x, y)) set()从源码看,主入口在规整表达式时若发现分子为纯数值(n.is_number)也会直接返回空集(diophantine.py),与这里的行为一致。
延伸阅读与问题反馈
- 更多分类型的求解示例与模块结构说明,见 Diophantine API 参考;
- 求解指南系列的其他主题(多项式求根、不等式、ODE 等)见 guides/solving 索引;
- 全部源码位于 diophantine.py,对应测试在 test_diophantine.py;
- 若在使用
diophantine时发现 bug,可在问题解决前改用本文开头"替代方案"中提到的数值方法应急,并在 SymPy 邮件列表上反馈问题详情。
【免费下载链接】sympyA computer algebra system written in pure Python项目地址: https://gitcode.com/GitHub_Trending/sy/sympy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考