→ fireslayer:DP 09/04 21:21
→ a27417332:DP是指dynamic programming嗎0.0 09/04 21:23
→ Feis:是阿. 09/04 21:24
→ diabloevagto:好像繞口令,看得我都暈了... 09/04 21:24
→ coal511464:其實現在硬體都夠強悍了..... 09/04 21:26
→ coal511464:遇到萬行以上的 想改快點腦袋真的會很頭痛.... 09/04 21:27
→ Feis:即使是硬體夠強悍, 很多不良的遞迴或迴圈用法依然會爆炸 09/04 21:36
→ Feis:晚點我可以舉些例子. 只是可能都太簡單 09/04 21:37
推 LPH66:這篇正好把 DP, memoization 跟原本的遞迴都講到了 09/04 21:58
→ LPH66:memoization 就是中間算完把值記下來的那個方法 09/04 21:59
推 karaokstar:推講解詳細 09/04 22:14
推 littleshan:這不能算是「有些疑惑」吧... 09/04 22:35
推 BlazarArc:最近剛好有用到 memorization 的方法 09/04 23:26
推 LPH66:樓上, memoization 沒有 r 喔 XD 它是從 memo 一字衍伸來的 09/05 00:57
→ LPH66:沒有 r 的這個字是專門用在這裡指稱這種「筆記」法的 09/05 00:57
推 BlazarArc:(嚇) 還真的耶...這太像了XDDD 09/05 04:59
推 rebaudiana:我覺得需要用遞迴的狀況大部分屬於「計算順序不明確」 09/05 05:59
→ rebaudiana:或者是有時候狀態的表示太複雜了所以丟給遞迴做… 09/05 06:01
推 ousapas:同樣是DFS 自己用stack做就硬是比直接遞迴快很多 09/05 08:40
→ Feis:ousapas: 那可以分享一下你的 DFS 是怎麼寫的嗎? 09/05 08:57
推 ousapas:只是之前寫過一個題目 用遞迴會超時 stack就剛好能過 09/05 09:16
→ ousapas:印象頗深刻XD 09/05 09:16
推 crazycat2:受教了! 09/05 09:16
→ Feis:ousapas: 效率會受到寫法、編譯器跟硬體影響. 09/05 10:10
→ Feis:用迴圈寫某層面來說是我們幫編譯器做自以為的最佳化. 09/05 10:11
→ Feis:如果我們比編譯器聰明, 更了解運作平台, 當然可能更有效率 09/05 10:13
→ Feis:但是也可能剛好相反. 09/05 10:13
→ adrianshum:這裡說成iteration要用空間換取效率,可是明明可以不用 09/05 22:32
→ adrianshum:呀,iterative不必存每個已算的f(n)... 09/05 22:33
→ Feis:確實,那就是我在文中提到有更簡單做法的原因 09/06 00:25
→ Feis:不過已經有人回應了~ 09/06 00:28
※ 編輯: Feis 來自: 140.112.217.49 (09/06 12:14)
推 christianSK:推!! 09/06 14:54
> -------------------------------------------------------------------------- <
作者: Feis (永遠睡不著 @@) 看板: C_and_CPP
標題: Re: [問題] 關於遞迴加快速度的迷思?
時間: Fri Sep 6 12:04:55 2013
※ 引述《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
推 s3748679:嗯XD 好多字 要看~ (被毆) 09/06 12:13
推 c2251393:是說可以構造矩陣((1, 1),(1, 0))來加速www 09/06 23:48
推 joshua5201:樓上國手 09/06 23:51
→ Feis:我所知道最快的應該是用矩陣乘法加上 doubling 09/07 00:14
→ s3748679:痾.. 據我所知最快的應該是公式解吧.. (直接代入公式-3-) 09/07 00:26
→ Feis:公式解印象中效率是差不多的 09/07 00:43
推 joshua5201:公式照理說是O(1)? 不過那些根號電腦不好算吧 09/07 02:25
→ Feis:O(1) 要看你的單位是什麼. 實際上還是跟項次有關 09/07 08:35
→ Feis:根號 (無理數) 會有精度問題. 不過應該可以展開避免. 09/07 08:36
→ s3748679:.... 被打臉了~ (臉紅) 原以為會萬無一失說 orz.. 09/07 09:47
→ Feis:我也是道聽塗說. 上次寫快速 Fibonacci 應該十幾年前了. 09/07 09:52
→ Feis:你有興趣可以寫寫看. 再跟大家分享一下~ 09/07 09:52
推 c2251393:公式解的話有個^n,要計算的話還是快不了吧 09/07 23:18
→ c2251393:(是說再深入下去感覺會離題XDD 09/07 23:20
※ 編輯: Feis 來自: 140.112.217.49 (09/07 23:37)