貪婪演算法
零錢換整問題
問題: 要把 n 個 1 元硬幣兌換成 50 元, 10 元, 5 元, 1 元硬幣, 如何兌換可以讓最終的硬幣數最少?
愚公移山的想法 (exhaustive search): 把各種硬幣的各種組合都試過一次, 逐一檢查其和是否為 n, 並記錄符合條件的組合各用了多少個硬幣, 再找出最佳答案。
動態規劃的想法: 用愚公移山法解的過程中, 不同的子問題是否用到共同的孫問題答案?
Greedy algorithm -- 短視/近利/偷懶/貪婪的想法: 每一步都不管大局, 只求這一步換掉越多 1 元硬幣越好。
- x0 = floor(n / 50); n = n % 50;
- x1 = floor(n / 10); n = n % 10;
- x2 = floor(n / 5) ; x3 =n % 5 ;
則 "x0 個 50 元, x1 個 10 元, x2 個 5 元, x3 個 1 元" 即為答案。
同樣是零錢換整問題, 如果可換硬幣的面額改成 25 元, 18 元, 5 元, 1 元, 則 greedy algorithm 所求出的就不是最佳解了。 例: n = 41, 用 greedy algorithm 解得 1 個 25 元, 0 個 18 元, 3 個 5 元, 1 個 1 元; 但最佳解其實是 0 個 25 元, 2 個 18 元, 1 個 5 元, 0 個 1 元。 (真的有學者 做類似的建議。) Q: 有沒有什麼規則可以看出 "零錢換整問題" 何時可以用 greedy algorithm?
Huffman Encoding
問題: 如何壓縮檔案?
想法: 用 ASCII 碼儲存檔案很浪費空間, 因為有些字母 (例如 e, s, t) 出現頻率很高, 有些字母出現頻率很低, 但卻使用相同的 bit 數。 如果能對所有字母重新編碼, 用比較少的 bit 表示出現次數高的字母, 用比較多的 bit 表示出現次數低的字母, 應該可以用比較小的檔案儲存相同豐富的資訊。
但這會遇到一個問題: 如果每個字母的編碼數不固定, 到時解碼如何知道應斷在何處? 如果採用的是 prefix code 就不會有困擾了。 所謂 prefix code, 指編碼必須滿足下列特性: 任何兩字元的編碼, 必然不會出現其中一者是另一者前段的狀況。 例如有 "0110" 就不會有 "011" 也不會有 "011010" (有 "洪朝貴" 就不會有 "洪朝" 也不會有 "洪朝貴仔" -- 比方啦。) 這類編碼其實應該稱為 prefix-free code 這裡的 free, 既不是 「免費」, 也不是 「自由」, 而是 「沒有」 的意思。 但一般書籍大多稱之為 prefix code。
演算法: 把所有字母視為一棵 binary tree 的 leaves (external nodes), 根據下列步驟建出完整的樹。
- 每次取出出現頻率最低的兩個 nodes, 為它們加上一個共同的 parent node。 這個 parent node 的頻率就以兩個 children 的頻率之和來表示。
- 把這個 parent node 放回去, 重複以上步驟。 因為每次取走兩個 nodes, 放回一個 node, 所以執行 n-1 次 (n 為字母個數) 後, 只剩一棵大樹, 包含所有的 nodes。
- 每個分支的左右各以 0 與 1 表示其編碼; 每個字母 x (皆為 external nodes) 的編碼可從 root 到 x 的路徑上所遇到的樹枝編碼讀出。
「取出頻率最低的兩個 nodes」這個動作, 最適合用 heap 來實作。 請見 程式 與 執行結果。
正確性:
- Greedy Choice Property: 確實存在 "以最少見兩碼為兄弟" 的最佳編碼。 注意此處的邏輯。 同一個問題, 可能可以有很多組不同的最佳編碼。 我們只需要證明 Huffman Encoding 編出來的是其中之一即可。
- Optimal Substructure Property: 從 "小心挑選的小一號問題的最佳編碼" 適當修改來的編碼, 必為原問題的最佳編碼。 注意此處的邏輯。 這樣的方向, 等一下才能用數學規納法。 有些講義寫顛倒了...
GCP 的證明, 請見: Rashid Bin Muhammad 與 Michael Mascagni; OSP 的證明, 請見: Leila Kalantari 或參考書目當中的 C.L.R.S. 。
結論
Greedy algorithm 貪婪演算法 的精神: "今朝有酒今朝醉" 每一步面臨選擇時, 都做眼前最有利的選擇, 不考慮對將來是否有不良的影響。
當然很多 combinatorial optimization problems 都可以試著用 greedy algorithm 去解; 但最大的問題在於: 這樣子找出來的解是否真的是最佳解? 大部分時候, 眼前的利益與長遠的利益並不相等; 但有少數問題可以經由證明顯示 「每一步都選眼前最有利的部分解答, 則最後所得的結果正是全盤考慮下最有利的解答」。
我的心得: 有很多問題都可以用蠻力解決。 特別是計算量大的問題, 通常用遞迴的方式寫, 程式看起來會很簡單。 (雖然執行起來可能要很久) 如果你發現遞迴過程當中, 有很多子問題根本在重複計算相同的東西, 浪費時間, 那麼這個問題可能可以用動態規劃來解。 這樣的程式, 速度會快很多, 但稍微難想。 如果你運氣好, 遇到的問題具有某些特性, 不必把所有可能性嘗試一遍, 只需要在每一步都做當下最有利的選擇, 那麼就可以用貪婪演算法解決。 這樣的程式, 速度最快, 寫起來也很簡單; 但是 「每一步都短視近利, 最後會得到整體的最佳解」 這件事是需要證明的, 通常不見得很好想。 所以本講義的編排, 先談遞迴, 次談動態規劃, 最後才談貪婪演算法; 可能和一般演算法書籍的順序不太一樣。
作業
- 上題可否用 dynamic programming 解?
- 試用 greedy algorithm 解上個單元的 fractional knapsack problem。
更多參考資料
- 本頁最新版網址: https://frdm.cyut.edu.tw/~ckhung/b/al/greedy.php; 您所看到的版本: February 14 2012 10:32:24.
- 作者: 朝陽科技大學 資訊管理系 洪朝貴
- 寶貝你我的地球, 請 減少列印, 多用背面, 丟棄時做垃圾分類。
- 本文件以 Creative Commons Attribution-ShareAlike License 或以 Free Document License 方式公開授權大眾自由複製/修改/散佈。