4.3 多項式求根#
概念說明#
多項式 p(x) = aₙxⁿ + … + a₁x + a₀ 是最特殊的一類函數:它由有限個係數完全決定,求值、微分、積分都有封閉公式,而且代數基本定理保證它恰有 n 個(計入重數的)複數根。這些結構讓多項式求根可以比一般函數求根更「系統化」:
- 利用合成除法(秦九韶/Horner 法):以 O(n) 時間求 p(x₀) 的值,順便得到商式,可用於「降階」——找到一個根後除掉對應因式,繼續找下一個根。
- 實根的定位與隔離:斯圖姆定理(Sturm's theorem)或 Descartes 符號法則可以確定某區間內實根的個數,把根一個個「關進」互不重疊的區間,再交給牛頓法或二分法精化。
- 全部根一次解:Durand-Kerner 法(又稱 Weierstrass 法)同時迭代 n 個近似根,每一步是簡單的代數公式;或直接呼叫
numpy.roots,其內部使用伴隨矩陣的特徵值分解。
與一般求根相比,多項式求根要特別小心兩件事:重根(收斂變慢且數值敏感)與病態性(係數的微小擾動可能讓根大幅移動,Wilkinson 多項式是經典例子)。
演算法步驟#
以「Horner 求值 + 降階 + 牛頓法」找多項式全部實根為例:
- 用 Horner 法實作 p(x) 與 p′(x) 的求值。
- 用符號法則估計正實根個數;對實係數多項式,實根都落在 [−R, R],其中 R = 1 + max|aᵢ/aₙ|(柯西界)。
- 在 [−R, R] 上掃描取樣找變號區間,每個變號區間用牛頓法(或二分法)精化出一個根。
- 找到根 r 後做降階:p ← p/(x − r)(用合成除法),回到第 3 步直到次數為 1。
Python 程式碼範例#
import numpy as np
def horner(coeffs, x):
"""coeffs = [a_n, ..., a_1, a_0],回傳 p(x)"""
y = 0.0
for c in coeffs:
y = y * x + c
return y
def deflate(coeffs, r):
"""以合成除法除以 (x - r),回傳新係數"""
out, carry = [], 0.0
for c in coeffs[:-1]:
carry = c + carry * r
out.append(carry)
return out
def poly_real_roots(coeffs, tol=1e-10):
roots = []
cs = list(map(float, coeffs))
while len(cs) > 2:
# 在 [-R, R] 掃描找變號區間
R = 1 + max(abs(c) for c in cs[:-1]) / abs(cs[0])
xs = np.linspace(-R, R, 2001)
vals = [horner(cs, x) for x in xs]
r = None
for i in range(len(xs) - 1):
if vals[i] == 0:
r = xs[i]; break
if vals[i] * vals[i+1] < 0:
# 牛頓法精化(導數係數由冪法則生成)
dc = [cs[j]*(len(cs)-1-j) for j in range(len(cs)-1)]
x = 0.5*(xs[i] + xs[i+1])
for _ in range(60):
fx, dfx = horner(cs, x), horner(dc, x)
if abs(dfx) < 1e-14: break
x -= fx/dfx
r = x; break
if r is None:
break
roots.append(r)
cs = deflate(cs, r)
return sorted(roots)
cs = [1, -5, 5, -1] # p(x) = x^3 - 5x^2 + 5x - 1
print("自寫法:", poly_real_roots(cs))
print("numpy :", np.roots(cs))
print("sympy :", __import__("sympy").solve(__import__("sympy").
Symbol("x")**3 - 5*__import__("sympy").Symbol("x")**2
+ 5*__import__("sympy").Symbol("x") - 1))
三種方法都應給出根 {1, (5±√13)/2}。注意 numpy.roots 會連複數根一起回傳,而符號解(sympy)才是數學上的精確答案——這正是第 1 章「精確 vs 近似」光譜的具體展現。
複雜度與誤差分析#
Horner 求值需要 n 次乘法與 n 次加法,時間 O(n),這是漸近最優的。掃描加牛頓的總成本約為 O(n²)(每個根一次降階,降階本身 O(n),根最多 n 個)。Durand-Kerner 每步 O(n²),通常數十步收斂。誤差方面,多項式求根的條件數可能非常大:對 n 次多項式,係數擾動 δ 導致的根擾動可放大到 O(δ · 10ⁿ)(Wilkinson 現象),因此高次多項式不要自己冪基底展開,改用切比雪夫基底或對根直接參數化。降階法還有誤差累積問題:每除一次,後面根的精度就下降一級,所以精密計算應改用同時迭代法或在最後對原多項式做一步牛頓「拋光」。
練習#
- 用 Horner 法分別在 x = 1.5 與 x = 10 處求 p(x) = x⁵ − 3x⁴ + 2x − 1 的值,並與
np.polyval比對;順便統計你的程式做了幾次乘法。 - 對 Wilkinson 多項式的 n=8 版本(根為 1, 2, …, 8):先用
np.poly由根重建係數,再用np.roots求回來,觀察根的誤差有多大,並解釋病因。 - 實作 Durand-Kerner 法:x_i ← x_i − p(x_i) / ∏_{j≠i} (x_i − x_j),對 p(x) = x⁴ − 1 測試,驗證四個根 {±1, ±i} 都被找出來。