4. 方程与方程组符号求解

在符号计算中,方程求解是最核心的能力之一。SymPy 的 solve、solveset、dsolve 等函数分别面向代数方程、集合形式解与微分方程。理解它们各自适用的对象、返回结构以及如何验证解的正确性,是避免"算出结果却用错结果"的关键。本节围绕代数方程、线性与非线性方程组、微分方程三类对象展开,并给出符号解与数值解对比的完整流程。

一、代数方程:solve 与 solveset 的分工

对于单变量代数方程,solve 返回解的列表,适合快速取用;solveset 返回集合对象,能更完整地表达解集(包括空集、区间、无穷多解)。例如求解 x**2 - 5*x + 6 = 0:

from sympy import symbols, solve, solveset, S
x = symbols('x')
solve(x**2 - 5*x + 6, x)        # [2, 3]
solveset(x**2 - 5*x + 6, x, domain=S.Reals)  # {2, 3}

当方程无实根时,solve(x**2 + 1, x) 返回 [-I, I],而 solveset(x**2 + 1, x, domain=S.Reals) 返回空集 EmptySet。这一差异说明:求解前必须明确定义域。工程问题中若变量代表长度、浓度等物理量,应主动限定 domain=S.Reals 或附加 positive=True 假设,否则复数解会混入结果。

对于含参数的方程,solve 会把参数当作符号保留在解中。例如 solve(a*x + b, x) 得到 [-b/a],但该解在 a = 0 时失效。严谨做法是配合 solveset 或手动讨论参数分支,避免遗漏退化情形。

二、线性与非线性方程组

多变量方程组同样交给 solve,传入方程列表与未知量列表,返回字典列表。线性方程组 2*x + y = 5, x - y = 1 可直接求得 {x: 2, y: 1}。对于欠定系统,SymPy 会用自由符号表示通解;对于矛盾系统则返回空列表,提示无解。

非线性方程组更能体现符号求解的价值。以圆与直线的交点为例:

from sympy import symbols, solve
x, y = symbols('x y')
eqs = [x**2 + y**2 - 25, x - y + 1]
sol = solve(eqs, [x, y])
# [(-4, -3), (3, 4)]

非线性系统可能有多组解,solve 返回全部符号解,但高次系统可能因无法因式分解而返回 ConditionSet 或空结果。此时应改用 nonlinsolve,它对非线性多项式系统更稳健,返回的是有限集或条件集,结构比 solve 更规范。

求解方程组时,未知量列表的顺序必须与解字典的键一致;若省略未知量,SymPy 会自行推断,但在含参数或符号较多时容易误判,建议显式写出。

三、微分方程:dsolve 与初始条件

常微分方程使用 dsolve。先定义函数 y = Function('y'),再构造方程 Eq(y(x).diff(x), -2*y(x)),求解得到通解 C1*exp(-2*x)。若给出初始条件 y(0) = 3,通过 ics 参数或 dsolve(..., ics={y(0): 3}) 可定出 C1 = 3,得到特解 3*exp(-2*x)。

需要注意,dsolve 对高阶、非线性或含变系数的方程不一定能给出初等函数形式的解,可能返回未求值形式或提示无法求解。此时应转向数值方法(如 scipy.integrate.odeint 或 SymPy 的 lambdify 配合数值积分),而不是强行等待符号结果。

四、解的结构与验证

符号解必须验证。最直接的方式是把解代回原方程,用 simplify 或 equals 判断是否恒为零:

from sympy import simplify
sol = solve(x**2 - 5*x + 6, x)
[simplify(x**2 - 5*x + 6).subs(x, s) for s in sol]  # [0, 0]

对微分方程,验证时需将解函数代入方程两边并化简差值。若差值化简为 0,说明解成立;若不为 0,常见原因是遗漏了积分常数、定义域限制或 dsolve 返回的是隐式解。隐式解需用 checkodesol 专门检验,它比手工代入更可靠。

解的结构也值得关注:代数方程的解是离散点集,线性方程组是点、线或平面,微分方程通解含任意常数、特解则被初始条件固定。理解这一层级关系,才能在结果异常时快速定位是"解错了"还是"解的形式与预期不同"。

五、符号解与数值解对比

符号解给出精确表达式,便于分析参数影响;数值解给出近似值,适合符号求解失败或表达式过于复杂的情形。以 x**5 - x - 1 = 0 为例,solve 无法给出根式解,但 nsolve 可在给定初值下求得数值根:

from sympy import nsolve
nsolve(x**5 - x - 1, x, 1.0)   # 约 1.1673

对比时应关注:符号解是否完整覆盖所有根、数值解是否收敛到正确分支、两者在相同点上是否一致。下图概括了从方程输入到符号求解、验证、数值对比的完整判断路径。

方程求解与验证的判断路径 方程 / 方程组 代数式、微分方程 方程类型? 单变量 / 多变量 / 微分 单变量代数方程 solve / solveset 注意定义域与参数分支 线性 / 非线性方程组 solve / nonlinsolve 多组解、欠定与矛盾 微分方程 dsolve + 初始条件 通解含常数,特解定常数 解的结构检查与代回验证 simplify 差值是否为 0;checkodesol 检验隐式解 验证通过? 差值恒为 0 接受符号解 改用 nsolve 数值解 并与符号解交叉比对

数值对比的实用做法是:先用 lambdify 把符号解转成可调用函数,在若干采样点上计算符号解与数值解的差值,若差值在容差(如 1e-9)内,则可认为两者一致。对于多解系统,还要逐组比对,防止数值方法只收敛到其中一支而漏掉其他分支。

符号求解失败不等于方程无解。遇到 ConditionSet、空列表或未求值结果时,应先检查定义域、假设条件与方程是否可因式分解,再决定是否转向数值方法;直接放弃会遗漏真实解。

综上,方程求解的完整闭环是:明确方程类型与定义域 → 选择 solve/solveset/nonlinsolve/dsolve → 检查解的结构是否完整 → 代回或 checkodesol 验证 → 必要时用 nsolve 做数值交叉验证。把这条路径固化为习惯,符号计算的结果才真正可信、可用。