1. 動態規劃
動態規劃這四個大字在演算法設計這一塊算是鼎鼎有名。它不是什麼高深的東西,就是演算法設計裡面最基礎的法門,少林長拳是也。基本上判斷一個人是不是真的學過演算法,考考動態規劃就知道了。有趣的是,這動態規劃偏偏就是和遞迴較勁的,而這個最簡單的例子就是著名的肥婆拉車 …… 呃,費伯納西(Fibonacci)數列了。
這個數列是這麼定義的(知道的同學請跳過這一段):記 fib(n) 為費伯納西數列的第 n 個元素,則 fib(0) = 0, fib(1) = 1, 對於所有的 n > 1 ,有 fib(n) = fib(n-1) + fib(n-2) 。巧得很,那位 ID 很長的 M 朋友(以後直接用 M 簡稱了,相信 007 不會介意),要計算的函數差不多也是這麼個形式,所以下面的討論,基本上可以一字不差的套用過去。
要計算這個函數,最直截了當的實現就是用遞迴了:
Java代碼
- int fib(int n)
- {
- // 不要和我扯什麼函數參數檢查之類,喜歡摳這些的請去隔壁的對日外包培訓班
-
- if (n == 0) return 0;
- else if (n == 1) return 1;
- else return fib(n-1) + fib(n-2);
- }
這個實現基本上就是照抄數列的定義,任何智商正常的程式員都能寫出來、看得懂。但是如果說起運行效率,那完全可以用一坨有機肥料來形容。嗯嗯,當然了,搞演算法的人不可以信口開河,光說人家像有機肥料是不行的,你得嚴格證明這一點,那麼現在就讓我們來簡單分析一下:記 fib(n) 運行所需的時間為 T(n),顯然 T(0) 和 T(1) 是常數,而對於所有的 n > 1, T(n) = T(n-1) + T(n-2) + 某個常數。Javaeye 不支援 latex,數學公式展不開,我們就取個最粗略的分析。顯然,對於所有的 n > 1,T(n) 是單調增加的,滿足 T(n) > T(n-1) > T(n-2) ... (有興趣的同學可以數學歸納一把確證一下),因此 T(n) = T(n-1) + T(n-2) + 某個常數 > 2*T(n-2) ,所以 T(n) = Ω(2^{n/2})。通俗點說,n 每增大 2,T(n) 至少要翻一倍,如果 n = 256 ,那麼這段程式多半是沒有希望在宇宙毀滅之前運行完了。
這見鬼的肥婆 …… 費伯納西數列真的那麼難算嗎?當然不是,上面那段程式的問題在於包含了太多的重複計算。讓我們看下面這張圖(此圖無恥地盜鏈自 SICP 的公開電子書):
看圖,這是上面那個遞迴程式計算 fib(5) 的過程。一眼可以看出,這裡面冗餘無數,比如fib(5) 那棵計算 fib(3) 的右子樹和 fib(4) 的左子樹完全是相同的。這種冗餘計算耗費了幾乎全部的程式已耗用時間,從而使得這個程式的運行效率散發出有機肥料的氣味。只要消除這些冗餘,就能讓程式運行速度快起來。
那麼具體如何著手呢?看看那個遞迴式子,fib(n) = fib(n-1) + fib(n-2) ,從本質上來講,這個式子真正告訴我們的是這樣一件事:計算 fib(n) 這個任務,可以分解為計算 fib(n-1) 和計算 fib(n-2) 這兩個新任務,而計算 fib(n-1) 和 fib(n-2) 這兩個新任務,也可以繼續通過這個遞迴式子分解下去,直到我們遇到 fib(0) 和 fib(1) 。算一算,我們一共可能遇到幾個不同任務?fib(0) 到 fib(n) 一共 n + 1 個,而且我們有了 fib(0) 和 fib(1) ,就能算出 fib(2),有了 fib(1) 和 fib(2) 就能算出 fib(3) …… 以此類推,我們可以照著這個順序把這區區 n + 1 個計算任務全都給完成了,從而也就得到了 fib(n)。程式如下:
Java代碼
- int fib(int n)
- {
- int[] fibs = new int[max(2, n+1)]; // 儲存 n + 1 個計算任務結果的數組
- // PS:其實沒必要保留這麼多空間,這裡是為了說明起來更清楚
- fibs[0] = 0; // 第 1 個計算任務
- fibs[1] = 1; // 第 2 個計算任務
- for (int i=2; i<=n; ++i) // 第 2 到 n 個計算任務
- {
- fibs[i] = fibs[i-1] + fibs[i-2];
- }
- return fibs[n];
- }
這個程式的時間複雜度是 O(n) ,也就是線性時間(看不出來的回去重修資料結構),傳個 n = 256 進去,瞬間你就能看到結果 —— 我們就這樣把一個本來需要計算到宇宙末日的問題給解決了。
這種演算法設計技巧,就是動態規劃。
我相信有些同學一定覺得上面這個例子實在太簡單了,你們天才的大腦需要更有挑戰性的工作才能滿足,所以下面給個稍微複雜點的問題作為思考題,有興趣的同學可以想一想。這是我面試某公司時遇到的面試題:話說有個魔法字典,其中記錄了一些魔力單詞(字串),如果一個句子(也是字串)可以被完全分解為若干魔力單詞的拼接,那麼這個句子就是一條咒語。假設我們可以用常數時間查詢魔力字典是否包含一個特定的單詞,那麼現在給你一個句子,讓你寫一個程式判斷這個句子是否一條咒語。用動態規劃可以解哦。