Re: [問題] 關於遞迴加快速度的迷思?

看板C_and_CPP作者 (永遠睡不著 @@)時間12年前 (2013/09/06 04:05), 編輯推噓5(509)
留言14則, 4人參與, 最新討論串7/10 (看更多)
※ 引述《LPH66 (f0VMRgEBA)》之銘言: <deleted> : 雖然有點像在模仿 GCD 的 "GCD(a,b) = GCD(b,a%b)" : 不過確實是另一條思路... 確實,通常當我們可以用更簡單的形式來表示時,也意味著有一種 更高階或不同角度的解釋法。 之前我的回應是著重在『遞迴比迴圈效率要差』的論述上,也就是 當問題具有遞迴關係時,在語言實作中使用遞迴函式呼叫是否比使 用迴圈效率還要差。所以我使用一個比較單純的例子跟寫法來說明 其中的關鍵點。不過看來有人對於一些技巧有點興趣,所以我試著 將之前的回應導到這個問題上,看看大家有沒有什麼不同的想法 ( 雖然實際上我在講的可能就是動態規劃 (DP) 常用來減少記憶體使 用的手法)。 這是我在之前回應中寫的最後一個遞迴版本: void f(int n, int *F, int i = 0) { if (n < i) return; if (i <= 1) F[i] = i; else F[i] = F[i-1] + F[i-2]; f(n, F, i+1); } int main() { int F[10+1]; f(10, F); printf("%d\n", F[10]); return 0; } 現在的問題變成是:我們一定需要這麼大的快取空間 (int F[10+1]) 嗎? 其實這件事情會跟計算的順序有很大的關係。 在把遞迴函式呼叫中呼叫者跟被呼叫者的關係描述成樹後,我上一 個回應提到的快取機制 (也就是 memoization) 做的事情是避免具 有相同參數值的遞迴函式重複計算造成多餘的遞迴展開。 因為在這棵樹上只要其中一個具有某組參數值的函式呼叫被遞迴展 開計算完後,其他具有相同參數值的函式呼叫就不需要去做同樣的 遞迴展開。 概念上,我們是用這個快取機制把這棵樹上的一些子樹剪掉而降低 計算量。至於哪些遞迴展開 (子樹) 會被省略掉 (剪掉), 就會跟 我們計算的順序有關。 之前的快取機制是剪完這棵樹後以參數值當鍵值把所有節點都儲存 起來,而這篇要解決的問題是在於是否一定要『同時』記得所有節 點?換句話說,我們最少需要準備多大的快取空間? 在快取機制中,我們清除快取最好的時機是在確保將來都不可能會 再用到的時候。換句話說,在快取空間中,只要該組參數值組合不 會再被存取我們就不用去保留該筆資料。 Fibonacci 數列的例子中,清除快取的手法可以透過對快取空間做 滑動的技巧來達成: // 用不停更新 F[2], F[1], F[0] 去儲存 F[i], F[i-1], F[i-2]; void f(int n, int *F, int i = 0) { if (n < i) return; // 利用滑動來減少快取空間的使用 F[0] = F[1]; F[1] = F[2]; if (i <= 1) F[2] = i; else F[2] = F[1] + F[0]; f(n, F, i+1); } int main() { // 如果計算順序好,我們只需要三個空間 (F[i] = F[i-1] + F[i-2]) int F[3]; f(10, F); // 此時 F[2] 的值就是我們要的 f(10) 的值 printf("%d\n", F[2]); return 0; } 當然,事實上我們可以用更小的 F 來實現滑動, 只是可能會覺得不直覺: void f(int n, int *F, int i = 0) { if (n < i) return; // 原本滑動做的事情: // ( 這裡 F[i] 表示原本快取的資料,F[i] 表示新的值 ) // F[0] = F[1] // F[1] = F[2] // F[2] = F[1] + F[0]; // 這時我們要省略一個,所以改成同時做下面兩個述句: // F[0] = F[1] // F[1] = F[1] + F[0] // 注意這裡 F[i] 與 F[i] 值不同, // 直接依上述述句做的話, 會有 F[i] 值已經被取代不見的問題 // 因此跟實作變數交換一樣,我們需要一個額外的區域變數 old_F1 // 來暫存 int old_F1 = F[1]; if (i <= 1) F[1] = i; else F[1] = F[1] + F[0]; F[0] = old_F1; f(n, F, i+1); } int main() { int F[2]; f(10, F); printf("%d\n", F[1]); return 0; } 因為需要的快取空間夠小且固定大小,可以將他在參數列展開: void f(int n, int &F1, int &F0, int i = 0) { if (n < i) return; int old_F1 = F1; if (i <= 1) F1 = i; else F1 = F1 + F0; F0 = old_F1; f(n, F1, F0, i+1); } int main() { int F[2]; f(10, F[1], F[0]); printf("%d\n", F[1]); return 0; } 其中 F0 的參考又可以被移掉: void f(int n, int &F1, int F0 = 0, int i = 0) { // 這裡 F0 初始值不重要 if (n < i) return; int old_F1 = F1; if (i <= 1) F1 = i; else F1 = F1 + F0; f(n, F1, old_F1, i+1); } int main() { int F1; f(10, F1); printf("%d\n", F1); return 0; } 接下來我們可以用回傳值來替代參考的功能,同時我們可以將邊界條件當做快 取資料的初始值: int f(int n, int F1 = 1 int F0 = 0, int i = 2) { // F0, F1 和 i 初始值很重要 if (n <= 1) return n; if (n < i) return F1; return f(n, F1+F0, F1, i+1); } int main() { printf("%d\n", f(10)); return 0; } 我們最後可以簡化到上面這樣 (但是效率跟泛用性不一定比一開始 的版本好) [編輯] 板友 DJWS 覺得我原本不適當的用英文詞彙強調一些中文名詞可能會 誤導其他板友,所以我將比較不正式或我不確定正確用法的英文名詞 都移除。 -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 140.112.217.49

09/06 12:13, , 1F
嗯XD 好多字 要看~ (被毆)
09/06 12:13, 1F

09/06 23:48, , 2F
是說可以構造矩陣((1, 1),(1, 0))來加速www
09/06 23:48, 2F

09/06 23:51, , 3F
樓上國手
09/06 23:51, 3F

09/07 00:14, , 4F
我所知道最快的應該是用矩陣乘法加上 doubling
09/07 00:14, 4F

09/07 00:26, , 5F
痾.. 據我所知最快的應該是公式解吧.. (直接代入公式-3-)
09/07 00:26, 5F

09/07 00:43, , 6F
公式解印象中效率是差不多的
09/07 00:43, 6F

09/07 02:25, , 7F
公式照理說是O(1)? 不過那些根號電腦不好算吧
09/07 02:25, 7F

09/07 08:35, , 8F
O(1) 要看你的單位是什麼. 實際上還是跟項次有關
09/07 08:35, 8F

09/07 08:36, , 9F
根號 (無理數) 會有精度問題. 不過應該可以展開避免.
09/07 08:36, 9F

09/07 09:47, , 10F
.... 被打臉了~ (臉紅) 原以為會萬無一失說 orz..
09/07 09:47, 10F

09/07 09:52, , 11F
我也是道聽塗說. 上次寫快速 Fibonacci 應該十幾年前了.
09/07 09:52, 11F

09/07 09:52, , 12F
你有興趣可以寫寫看. 再跟大家分享一下~
09/07 09:52, 12F

09/07 23:18, , 13F
公式解的話有個^n,要計算的話還是快不了吧
09/07 23:18, 13F

09/07 23:20, , 14F
(是說再深入下去感覺會離題XDD
09/07 23:20, 14F
※ 編輯: Feis 來自: 140.112.217.49 (09/07 23:37)
文章代碼(AID): #1IALHiHM (C_and_CPP)
討論串 (同標題文章)
本文引述了以下文章的的內容:
以下文章回應了本文
完整討論串 (本文為第 7 之 10 篇):
文章代碼(AID): #1IALHiHM (C_and_CPP)