由下而上的製表(bottom-up tabulation)
與其從大問題出發向下遞迴到小問題,你改從已經知道的最小答案出發向上建造,一列一列填表,直到大答案成為你寫下的最後一格。這就像疊積木塔:在它所倚靠的積木就位之前,你無法放上更高的積木,所以你先打地基再往上做。這裡完全沒有遞迴——只有以精心選定的順序填滿陣列的迴圈。
做法是:配置一個以狀態為索引的表格,直接寫入基底情況的項,再以一種能保證「每一項都在它所依賴的全部項算好之後才被計算」的順序遍歷各狀態,套用轉移由先前的格子填出每一格。以費氏數為例:建一個陣列 f,令 f[0]=0、f[1]=1,再讓 i 從 2 到 n,設 f[i]=f[i-1]+f[i-2];答案是 f[n]。讓 i 由小到大的迴圈順序,正是使你需要 f[i-1] 與 f[i-2] 時它們都已備妥的關鍵。執行時間同樣是(狀態個數)乘上(每次轉移的工作),但現在沒有函式呼叫開銷、也沒有堆疊。
製表的回報是速度與可預測的記憶體用量。由於填表順序是明確的,你常能大幅縮減空間:費氏數只往回看兩格,所以用兩個純量變數就夠,不必用整個陣列,得到 O(1) 空間。代價是你必須自己推敲出一個有效的計算順序,而且天真的製表會把表中每個狀態都算出來,連答案其實永遠用不到的也算——反觀由上而下的記憶化只觸及可達的狀態。所以製表用一點點彈性,換來純粹的效率與堆疊安全。
以製表做的省空間費氏數:a=0、b=1;重複 n 次:(a,b)=(b,a+b);答案是 a。兩個變數、O(n) 時間、O(1) 空間——之所以可行,只因明確的填表順序準確告訴你最多往回看多遠。
知道填表順序,就能只保留還會用到的少數格子,從而縮小記憶體。
把迴圈順序搞錯是製表的經典臭蟲:若你在某格被填好之前就去讀它,你會用到過時或為零的值,答案無聲地出錯。務必確認每次轉移只讀取較早填好的格子。