同一個問題的兩種答案
這座階梯先前各級裡的每一個方法,都共享一個悄悄的假設:答案會是一個數字,而那個數字是近似的。我們解 A x = b 得到的是 2.9999999998 而不是 3;我們跑牛頓法,看著位數一位位堆上去,卻永遠不會變成精確;我們一直在浮點運算裡工作,而在那裡 0.1 根本沒有精確的二進位形式。那是數值計算,是兩大風格中的一種。本級講的是另一種。
在符號計算裡,電腦處理的與其說是數字,不如說是式子。要它求 x^3 的導數,它回傳的是精確的式子 3 x^2,而不是一張斜率表。要它求 1/x 的積分,它一字不差地回傳 log(x)。要它把 1/3 加上 1/6,它精確地回傳 1/2——絕不是 0.5,絕不是 0.4999999。它承載的物件是一棵符號的樹:一個有兩個子節點的加法節點、一個次方節點、一個叫 x 的變數。這兩種風格之間的對比,是整個本級賴以建立的核心想法;詞彙表稱它為符號計算與數值計算之分。
「精確」到底換來了什麼
精確性的根基,在於機器拒絕捨入。浮點數只保留約 16 位十進位數字、其餘全丟掉;CAS 則把數字存成不損失任何東西的形式。最簡單的是精確有理數運算:一個分數不過是一對整數——分子與分母——所以 1/3 永遠是那一對 (1, 3),絕不會衰變成 0.3333。把兩個分數相加,你得到的還是一個精確分數,並透過約掉它們的公因數而化簡。這裡完全沒有捨入的地板——那個曾限制數值準確度的東西,根本就不存在。
但整數會長得遠遠超過機器暫存器能裝的 64 個位元。把足夠多的整數乘在一起——比方說算 50 階乘——結果就是一個 65 位數,沒有任何單一暫存器能存下。答案是任意精度運算,常被叫做 bignum:一個整數被存成一個「位數塊」的陣列,系統需要多少塊就帶多少塊,並在塊與塊之間做小學課本式的進位。它比硬體運算慢——bignum 乘法會隨數字變長而變貴——但它對任意大小的整數都精確,而精確有理數,不過就是兩個 bignum 並肩同行。
在有理數之外,CAS 把無理數與超越數量保持成它們自己。根號 2 被存成符號 sqrt(2),而不是 1.41421356;pi 被存成 pi。於是 sqrt(2) 乘以 sqrt(2) 精確地塌縮成 2,而一百萬次這樣的運算引入的誤差是零,因為從頭到尾沒做過任何近似。這正是符號風格最大的獎賞:一個 pi/2 的答案就是真相,而不是它的 16 位影子,而且不管計算跑多久,它都保持為真相。
暗藏的難關:判定兩個東西何時相等
符號計算在這裡顯露出一種在數值世界裡毫無回響的困難。如果兩個數字不一樣,你會立刻看出來——3.0 不是 3.1。但式子 (x + 1)^2 與 x^2 + 2 x + 1 相等嗎?它們當然相等,可是寫在紙上的樣子完全不同。一個分辨不出來的 CAS 會給出笨拙、冗餘的答案,甚至可能什麼都化簡不了。所以每個系統都需要一個辦法,把式子帶到一個大家都同意的單一形狀,也就是標準形式:一條規則說「不管你原本怎麼寫,這裡是寫它的那個標準寫法。」把兩個式子都化到標準形式,若它們一字不差地相符,它們就相等。
對多項式來說這辦法漂亮極了——把所有東西展開、合併同類項,上面兩邊都會變成 x^2 + 2 x + 1。但只要踏出多項式一步,腳下的地面就塌了。式子 sin(x)^2 + cos(x)^2 - 1 等於零嗎?它等於,但前提是系統知道畢氏恆等式。exp(log(x)) - x 是零嗎?只有在 x 大於零時才是。判定一個任意式子是否恆等於零,就是零等價問題,而誠實卻令人不安的真相是:對於夠豐富的式子類別,它已被證明是不可判定的——沒有任何演算法能永遠回答它。這不是某個更聰明的工程師日後會填上的缺口;它是烤進數學本身的一道硬性極限。
兩種各在何時勝出?
於是你手上有兩套工具。你該伸手拿哪一套?這個決定取決於你真正需要從答案裡得到什麼,而兩種風格的甜蜜點幾乎相反。當你需要的是一條公式而非一個數值時——一個你能讀懂、能手動微分、或之後能代入許多輸入的結果——符號勝出。當你需要的是當下的一個數字、當問題根本沒有封閉解(這是大多數真實問題的情況)、或當符號答案會龐大到無法使用時,數值勝出。
- 伸手拿符號:當你想要一條能檢視的精確公式時——比方說某個函數的反導數、一個小型方程組的通解、或一個你會用許多不同參數值反覆代入的封閉形式式子。
- 伸手拿數值:當你想要一個具體數值時、當問題沒有封閉解時(一道湍流、一個市場、一個星系)、或當速度與龐大的規模比看見代數更重要時。
- 兩者並用,這才是專家實際在做的事:先用符號推出一條乾淨的公式以確定它正確無誤,再把那條公式交給快速的數值程序,去大規模地求值。每種風格都補上另一種的盲點。
一個具體的畫面:假設你必須對一個函數積分。CAS 也許幾秒內就交給你一個精確的反導數——太好了,你現在有一條對每個輸入都成立的公式。但許多再尋常不過的函數,例如 exp(-x^2),根本沒有能用初等函數表示的反導數;符號引擎會正確地這麼告訴你。對那些函數,唯一的路是數值積分,也就是數值積分那一級的近似面積法,它即使在沒有公式時也照樣回傳一個數字。兩種風格沒有誰更高明;它們回答的是不同的問題。
籠罩這一切的那個陷阱
如果精確的符號計算這麼強大——沒有捨入、是字面上的真相——那為什麼全世界不都用符號?因為精確性掛著一個殘酷的價碼,而它還有自己的名字:表達式膨脹。隨著一個符號計算往前推進,中間的式子可能爆炸性地暴漲,即使輸入與最終答案都既小又整齊。一個不大的符號矩陣的行列式,可能引爆成好幾千項;一個精確分數的分子與分母,可能在計算中途各自長到好幾百位數,最後才又約回來。
這是你在數值世界裡遇過的那些限制的符號鏡像。在數值世界裡,核心的敵人是捨入誤差與病態——一個接近 10^8 的條件數悄悄吃掉你一半的位數。在符號世界裡完全沒有捨入,但儲存與操作那些精確式子的代價可能暴漲得如此之快,以至於計算早在完成之前就耗光了記憶體或時間。精確性無法擴展,而本級的最後一篇指南,整篇就獻給「為什麼」。
在攀爬本級時,把整級的地圖記在心裡。接下來你會仔細看精確性實際上是怎麼建起來的——任意精度,以及讓相等性在良好情形下可判定的標準形式。然後是多項式的代數:用多項式版的歐幾里得演算法求最大公因式,以及為整個多項式方程組而生的葛羅布納基底。然後是符號計算,包括那個了不起的 Risch 演算法,它能判定一個積分究竟有沒有初等形式。最後是表達式膨脹,那道每個符號方法終究會撞上的牆。兩種計算,兩種極限——而一個成熟的計算數學家,清楚知道何時該在兩者之間切換。