如何不用 SymPy 高效计算 arctan 高阶导数并生成 Taylor 级数?
可以不使用 SymPy。对 arctan 求导时,保留分母 (1+x**2)**n,只递推分子,就能避免当前实现中分母次数的指数级增长。如果最终只需要 Taylor 系数,也可以直接进行幂级数运算,无需构造完整的高阶导数表达式。
为什么当前算法越来越慢?
frac_poly_dif 每次都会将整个分母平方,却没有消去分子和分母的公共因子。从 arctan 的一阶导数开始,未经约分的分母依次是:
(1+x**2)**1
(1+x**2)**2
(1+x**2)**4
(1+x**2)**8
...
因此,计算 arctan 的第 n 阶导数时,当前表示中的分母为 (1+x**2)**(2**(n-1)),展开后的次数是 2**n。
这些导数可以统一写成:
D**n arctan(x) = P_n(x)/(1+x**2)**n, n >= 1
其中 P_n 是次数为 n-1 的多项式。你给出的第六阶导数,分母只需保留为 (1+x**2)**6。
问题出在未经约分的表达式不断膨胀。仅删除零系数或优化 Counter 的遍历,无法解决这个增长问题。
需要完整导数时:递推分子,保留分母的幂
设:
q(x) = 1+x**2
D**n arctan(x) = P_n(x)/q(x)**n
对这个形式求导并消去公共因子:
D(P_n/q**n)
= (q*P_n' - n*q'*P_n)/q**(n+1)
由于 q'(x)=2*x,递推式为:
P_1(x) = 1
P_(n+1)(x) = (1+x**2)*P_n'(x) - 2*n*x*P_n(x)
分子中的一项 c*x**k 在下一步的贡献为:
k*c*x**(k-1) + (k-2*n)*c*x**(k+1)
因此不需要进行一般的多项式乘法,直接移动指数、累加系数即可:
from collections import Counter
def arctan_derivative_numerator(n):
"""返回 arctan 的第 n 阶导数的分子,分母为 (1+x**2)**n。"""
if n < 1:
raise ValueError("n must be >= 1")
p = Counter({0: 1})
for order in range(1, n):
result = Counter()
for degree, coefficient in p.items():
if degree > 0:
result[degree - 1] += degree * coefficient
result[degree + 1] += (degree - 2 * order) * coefficient
p = Counter({
degree: coefficient
for degree, coefficient in result.items()
if coefficient != 0
})
return p
numerator = arctan_derivative_numerator(16)
print(sorted(numerator.items()))
# 完整导数为该分子除以 (1+x**2)**16。
前三个分子分别为:
P_1(x) = 1
P_2(x) = -2*x
P_3(x) = 6*x**2 - 2
这个递推只使用整数运算,不需要计算多项式最大公因式。计算到第 N 阶时,累计需要 O(N**2) 次量级的系数运算。这个估算假定每次整数运算的成本固定,实际成本还受大整数位数的影响。
只需要 Taylor 系数时:直接递推级数
在零点展开 arctan,可以先展开它的一阶导数。设:
1/(1+x**2) = b_0 + b_1*x + b_2*x**2 + ...
两边乘以 1+x**2,比较相同次数的系数,得到:
b_0 = 1
b_1 = 0
b_k = -b_(k-2), k >= 2
再逐项积分,利用 arctan(0)=0,得到:
arctan(x) = x - x**3/3 + x**5/5 - x**7/7 + ...
这个过程也可以写成一般的有理函数级数除法。设分子、分母和商的系数分别为 p_n、q_n 和 b_n,且 q_0 != 0,则:
b_n = (p_n - sum(q_k*b_(n-k), k=1..n))/q_0
超出多项式次数的系数按零处理。下面的实现只依赖 Python 标准库,用 Fraction 保留精确的有理数结果:
from fractions import Fraction
def rational_series(p, q, order):
"""返回 p(x)/q(x) 在 x=0 处到 order 次的幂级数系数。
p、q 均按常数项、一次项、二次项……的顺序传入。
"""
if order < 0:
raise ValueError("order must be >= 0")
p = [Fraction(value) for value in p]
q = [Fraction(value) for value in q]
if not q or q[0] == 0:
raise ValueError("the denominator must have a nonzero constant term")
coefficients = []
for n in range(order + 1):
value = p[n] if n < len(p) else Fraction(0)
for k in range(1, min(n, len(q) - 1) + 1):
value -= q[k] * coefficients[n - k]
coefficients.append(value / q[0])
return coefficients
# arctan'(x) = 1/(1+x**2),先求到 x**15 的系数。
derivative_coefficients = rational_series([1], [1, 0, 1], 15)
# 逐项积分,得到 arctan 到 x**16 的 Taylor 系数。
taylor_coefficients = [Fraction(0)] + [
coefficient / (degree + 1)
for degree, coefficient in enumerate(derivative_coefficients)
]
print([
(degree, str(coefficient))
for degree, coefficient in enumerate(taylor_coefficients)
if coefficient != 0
])
输出为:
[(1, '1'), (3, '-1/3'), (5, '1/5'), (7, '-1/7'),
(9, '1/9'), (11, '-1/11'), (13, '1/13'), (15, '-1/15')]
对于这里固定的二次分母,每个系数最多依赖前两个系数。生成 N 个系数需要 O(N) 次量级的有理数运算。同样,这个估算没有将大整数运算的位数成本视为变量。
如果需要在其他点 a 展开,令 t=x-a,则:
arctan'(a+t) = 1/((1+a**2) + 2*a*t + t**2)
把分母系数换成 [1+a**2, 2*a, 1],即可生成关于 t 的导数级数,再逐项积分。积分常数 arctan(a) 需要另外提供,上述递推不会自动求出这个常数。若要继续使用 Fraction 进行精确计算,a 应用整数或 Fraction 表示。
实现时需要注意什么?
- 注意区分导数阶数。初始的
1/(1+x**2)已经是 arctan 的一阶导数,再调用 16 次求导,得到的是 arctan 的第 17 阶导数。 - 第 n 阶 Taylor 系数是
f^(n)(a)/n!。上面的级数除法和积分直接生成系数,不需要额外再除一次阶乘。 - 保留负系数。使用
Counter时,应继续逐项累加;它的二元+、-运算会丢弃非正计数,不适合直接用于有符号多项式的加减法。 - arctan 的 Taylor 系数包含
1/3等有理数。Fraction使用整数分子、分母表示它们,可以避免浮点舍入。 - 计算系数后,还要判断级数的收敛范围。零点处的 arctan 级数在
|x|<1内收敛到 arctan,在实数端点x=±1也收敛,但较慢;不能直接用于|x|>1的求值。对于其他函数,也需要单独判断解析性和收敛范围。
如果要通用地生成更多函数的 Taylor 级数,可以进一步实现截断幂级数的加、减、乘、除、求导和积分,并为支持的函数增加相应的系数递推规则。当前的多项式商法则实现只覆盖有理函数,不能直接处理所有超越函数。
备注:内容仅供参考。
