自动化简的边界 · 为什么 sin²+cos² 不自动等于 1
x+x 自动变成 2x,sin²+cos² 却不自动变成 1——这不是 SymPy 偷懒,而是一个深刻的事实:判断一个表达式是否等于零,在一般情况下是「不可判定」的。这一章讲清自动化简的边界、「最简」的模糊,以及为什么 simplify 只能是一个尽力而为的启发式操作。
自动化简的边界 · 为什么 sin²+cos² 不自动等于 1
新手常有个期待:输入一个式子,CAS 就该「自动化到最简」。现实却微妙得多——有些化简自动发生,有些死活不发生。理解这条边界在哪、为什么在那,是用好符号计算的关键,也是我们第一次触到「计算的本质」。
有些化简,自动发生
SymPy 在你构造表达式的同时,就会做一批廉价、且永远正确的化简:
from sympy import *
x = symbols('x')
print(x + x) # 2*x
print(2*x + 3*x) # 5*x
print(x / x) # 1
print(sqrt(8) - 2*sqrt(2)) # 0 —— √8 规范成 2√2,于是相减为 0
print(exp(x) * exp(-x)) # 1 —— 同底幂相乘,指数相加为 0这些叫自动化简(automatic simplification)。它们有个共同点:又快又无脑地对——合并同类项、约分、同底幂相加,这些规则任何时候都成立,而且代价极低。SymPy 用它们把表达式维持在一个规范形式(canonical form),好让 和 被认成同一棵树。
有些化简,死活不自动发生
但下面这些,SymPy 不会主动帮你做:
print((x + 1)**2) # (x + 1)**2 —— 不自动展开
print(sin(x)**2 + cos(x)**2) # sin(x)**2 + cos(x)**2 —— 不自动等于 1
print(log(x**2)) # log(x**2) —— 不自动变 2*log(x)明明等于 ,为什么不自动化简?两个原因,一浅一深。
浅层原因:「最简」根本没有定义
先看 。它不自动展开成 ——可反过来, 也不自动因式分解成 。哪个更简单?
「最简形式」是个模糊、甚至自相矛盾的概念。 和 哪个简单?看你要干什么:想求根,因式分解的 更好;想求导或看各项系数,展开的 更方便。没有放之四海皆「最简」的形式。所以 CAS 不敢替你擅自决定朝哪个方向化——它把选择权交给你,用有明确方向的操作:expand(展开)、factor(因式分解)、collect(合并)、trigsimp(三角化简)……
print(expand((x + 1)**2)) # x**2 + 2*x + 1
print(factor(x**2 + 2*x + 1)) # (x + 1)**2你要展开还是因式分解?你说了算,而不是系统猜。
深层原因:判断「是否等于零」是不可判定的
需要一条三角恒等式——这不像「合并同类项」那样无脑,得搜索该用哪条恒等式。而搜索是昂贵的:如果每构造一个式子都自动跑一遍所有可能的恒等式,符号计算会慢到无法使用。所以这类化简被挪出「自动」范畴,交给一个你显式调用的操作:
print(simplify(sin(x)**2 + cos(x)**2)) # 1 —— 你得开口问但代价高只是表面。真正深刻的原因是:
一个「完美的化简算法」根本不存在——这是数学上被证明的不可能。 化简的核心是判断「两个式子是否相等」,等价于判断「它们的差是否恒等于零」。而 Richardson 定理(1968) 证明:对于包含 、指数、对数、、绝对值等的一大类表达式,判断一个表达式是否恒等于零,是不可判定的——不存在任何算法,能对所有这类式子给出「是零 / 非零」的正确回答。
这意味着:没有、也不可能有一个 simplify 能把任何式子都化到真正的最简。它只能是一个启发式:试一堆常见变换,挑个看起来最简的返回——大多数时候好用,但有时化不动、甚至越化越复杂。这不是 SymPy 或 Mathematica 不够聪明,而是问题本身就没有完美解。我们在上一章说精确数消灭了误差;这里则看到,精确的世界自有它无法逾越的边界——而这条边界,直指「计算」的极限。
所以:用有方向的工具,别指望「一键最简」
认清边界后,实践策略很清楚:别指望一个万能的 simplify,而是根据目的选对准的工具。 常用的一批:
| 你想要 | 用 | 例子 |
|---|---|---|
| 乘开括号 | expand | |
| 因式分解 | factor | |
| 按某变量合并 | collect | 整理成 的多项式 |
| 三角化简 | trigsimp | |
| 拆对数 | expand_log | (需 ) |
| 通用启发式 | simplify | 不确定时的兜底,可能慢 |
simplify 是「不知道用啥就先试它」的兜底,但生产代码里更推荐用有明确方向的操作——它们快、可预期,不会给你一个「我也不知道为啥长这样」的结果。
顺带一提 log(x**2) 那个:它不自动变 ,是因为当 时这一步并不成立( 的定义域)!CAS 不敢在不知道 正负时乱化——这也是它「谨慎」的一面。你可以用假设告诉它 x 为正,它就放心化了(假设系统是符号计算的重要一环)。
小结与下一站
自动化简只做廉价且永远正确的规范化(、约分、同底幂);不做有方向的变换( 不自动展开)和需要搜索恒等式的化简( 不自动等于 1)。深层原因是 Richardson 定理:判断表达式是否恒为零不可判定,所以完美的 simplify 不存在,它只能是启发式。实践中用 expand/factor/trigsimp 等有方向的工具。
地基就位:表达式是树、数是精确的、化简有其边界。接下来进入内核——符号计算到底怎么「算」?下一章你会发现,无论求导、化简还是解方程,底层都是同一个动作:在树里找到一个模式,把它替换掉。👉 替换与模式匹配。
参考文献
- [1] Daniel Richardson.「Some Undecidable Problems Involving Elementary Functions of a Real Variable.」J. Symbolic Logic, 1968. —— 零判定不可判定性。
- [2] Joel S. Cohen.《Computer Algebra and Symbolic Computation》—— 自动化简与规范形式。
- [3] SymPy 文档.「Simplify.」https://docs.sympy.org/latest/tutorials/intro-tutorial/simplification.html