思想
greed、dc、dp、backtracking、enum
37 | 貪心算法:如何用貪心算法實現Huffman壓縮編碼?
思想
局部最優
例子
- 揹包中所裝物品的總價值最大
- 分糖果
- 錢幣找零
- 區間覆蓋
- 霍夫曼編碼
問題
假設我有一個包含 1000 個字符的文件,每個字符佔 1 個 byte(1byte=8bits),存儲這 1000 個字符就一共需要 8000bits,那有沒有更加節省空間的存儲方式呢?
- 不同字符
- 霍夫曼編碼:是一種十分有效的編碼方法,廣泛用於數據壓縮中,其壓縮率通常在 20%~90% 之間。
步驟
針對一組數據,我們定義了限制值和期望值,希望從中選出幾個數據,在滿足限制值的情況下,期望值最大。
實際上,用貪心算法解決問題的思路,並不總能給出最優解。
課後思考
- 在一個非負整數 a 中,我們希望從中移除 k 個數字,讓剩下的數字值最小,如何選擇移除哪 k 個數字呢?
由最高位開始,比較低一位數字,如高位大,移除,若高位小,則向右移一位繼續比較兩個數字,直到高位大於低位則移除,循環k次。
- 假設有 n 個人等待被服務,但是服務窗口只有一個,每個人需要被服務的時間長度是不同的,如何安排被服務的先後順序,才能讓這 n 個人總的等待時間最短?
由等待時間最短的開始服務
38 | 分治算法:談一談大規模計算框架MapReduce中的分治思想
思想
分而治之
分治算法用四個字概括就是“分而治之”,將原問題劃分成 n 個規模較小而結構與原問題相似的子問題,遞歸地解決這些子問題,然後再合併其結果,就得到原問題的解。
分治和遞歸的區別
分治算法是一種處理問題的思想,遞歸是一種編程技巧。實際上,分治算法一般都比較適合用遞歸來實現。
分治算法的遞歸實現中,每一層遞歸都會涉及這樣三個操作:
- 分解:將原問題分解成一系列子問題;
- 解決:遞歸地求解各個子問題,若子問題足夠小,則直接求解;
- 合併:將子問題的結果合併成原問題。
條件
- 原問題與分解成的小問題具有相同的模式
- 原問題分解成的子問題可以獨立求解,子問題之間沒有相關性,這一點是分治算法跟動態規劃的明顯區別,等我們講到動態規劃的時候,會詳細對比這兩種算法;
- 具有分解終止條件,也就是說,當問題足夠小時,可以直接求解;
- 可以將子問題合併成原問題,而這個合併操作的複雜度不能太高,否則就起不到減小算法總體複雜度的效果了。
舉例
如何編程求出一組數據的有序對個數或者逆序對個數呢?
藉助歸併排序算法
歸併排序中有一個非常關鍵的操作,就是將兩個有序的小數組,合併成一個有序的數組。實際上,在這個合併的過程中,我們就可以計算這兩個小數組的逆序對個數了。每次合併操作,我們都計算逆序對個數,把這些計算出來的逆序對個數求和,就是這個數組的逆序對個數了。
private int num = 0; // 全局變量或者成員變量
public int count(int[] a, int n) {
num = 0;
mergeSortCounting(a, 0, n-1);
return num;
}
private void mergeSortCounting(int[] a, int p, int r) {
if (p >= r) return;
int q = (p+r)/2;
mergeSortCounting(a, p, q);
mergeSortCounting(a, q+1, r);
merge(a, p, q, r);
}
private void merge(int[] a, int p, int q, int r) {
int i = p, j = q+1, k = 0;
int[] tmp = new int[r-p+1];
while (i<=q && j<=r) {
if (a[i] <= a[j]) {
tmp[k++] = a[i++];
} else {
num += (q-i+1); // 統計 p-q 之間,比 a[j] 大的元素個數
tmp[k++] = a[j++];
}
}
while (i <= q) { // 處理剩下的
tmp[k++] = a[i++];
}
while (j <= r) { // 處理剩下的
tmp[k++] = a[j++];
}
for (i = 0; i <= r-p; ++i) { // 從 tmp 拷貝回 a
a[p+i] = tmp[i];
}
}
# todo
問題
二維平面上有 n 個點,如何快速計算出兩個距離最近的點對?
有兩個 nn 的矩陣 A,B,如何快速求解兩個矩陣的乘積 C=AB?
分治思想在海量數據處理中的應用
給 10GB 的訂單文件按照金額排序?
- 劃分
- 合併
利用這種分治的處理思路,不僅僅能克服內存的限制,還能利用多線程或者多機處理,加快處理的速度。
- 訂單數據存儲在類似 GFS 這樣的分佈式系統上
解答開篇
MapReduce 本質上就是利用了分治思想
課後思考
我們前面講過的數據結構、算法、解決思路,以及舉的例子中,有哪些採用了分治算法的思想呢?除此之外,生活、工作中,還有沒有其他用到分治算法的地方呢?你可以自己回憶、總結一下,這對你將零散的知識提煉成體系非常有幫助。
- 統計我國人口
39 | 回溯算法:從電影《蝴蝶效應》中學習回溯算法的核心思想
思想
枚舉搜索
回溯的處理思想,有點類似枚舉搜索。我們枚舉所有的解,找到滿足期望的解。爲了有規律地枚舉所有可能的解,避免遺漏和重複,我們把問題求解的過程分爲多個階段。每個階段,我們都會面對一個岔路口,我們先隨意選一條路走,當發現這條路走不通的時候(不符合期望的解),就回退到上一個岔路口,另選一種走法繼續走。
選一條路走,走不通就退回再走
例子
- 8皇后
- 0-1 揹包
- 正則表達式
40 | 初識動態規劃:如何巧妙解決“雙十一”購物時的湊單問題?
41 | 動態規劃理論:一篇文章帶你徹底搞懂最優子結構、無後效性和重複子問題
一個模型三個特徵
多階段決策最優解模型
最優子結構、無後效性和重複子問題
- 例子
假設我們有一個 n 乘以 n 的矩陣 w[n][n]。矩陣存儲的都是正整數。棋子起始位置在左上角,終止位置在右下角。我們將棋子從左上角移動到右下角。每次只能向右或者向下移動一位。從左上角到右下角,會有很多不同的路徑可以走。我們把每條路徑經過的數字加起來看作路徑的長度。那從左上角移動到右下角的最短路徑長度是多少呢?
兩種動態規劃的解題思路
狀態轉移表法解題思路大致可以概括爲,回溯算法實現 - 定義狀態 - 畫遞歸樹 - 找重複子問題 - 畫狀態轉移表 - 根據遞推關係填表 - 將填表過程翻譯成代碼。
狀態轉移方程法的大致思路可以概括爲,找最優子結構 - 寫狀態轉移方程 - 將狀態轉移方程翻譯成代碼。
四種算法思想比較分析
貪心、分治、回溯和動態規劃
貪心、回溯、動態規劃可以歸爲一類,而分治單獨可以作爲一類
回溯算法是個“萬金油”。基本上能用的動態規劃、貪心解決的問題,我們都可以用回溯算法解決。窮舉所有的情況,然後對比得到最優解。不過,回溯算法的時間複雜度非常高,是指數級別的,只能用來解決小規模數據的問題。對於大規模數據的問題,用回溯算法解決的執行效率就很低了。
能用動態規劃解決的問題,需要滿足三個特徵,最優子結構、無後效性和重複子問題。
在重複子問題這一點上,動態規劃和分治算法的區分非常明顯。分治算法要求分割成的子問題,不能有重複子問題,而動態規劃正好相反,動態規劃之所以高效,就是因爲回溯算法實現中存在大量的重複子問題。
貪心算法實際上是動態規劃算法的一種特殊情況。它解決問題起來更加高效,代碼實現也更加簡潔。不過,它可以解決的問題也更加有限。它能解決的問題需要滿足三個條件,最優子結構、無後效性和貪心選擇性(這裏我們不怎麼強調重複子問題)。“貪心選擇性”的意思是,通過局部最優的選擇,能產生全局的最優選擇。每一個階段,我們都選擇當前看起來最優的決策,所有階段的決策完成之後,最終由這些局部最優解構成全局最優解。
課後思考
硬幣找零問題,我們在貪心算法那一節中講過一次。我們今天來看一個新的硬幣找零問題。假設我們有幾種不同幣值的硬幣 v1,v2,……,vn(單位是元)。如果我們要支付 w 元,求最少需要多少個硬幣。比如,我們有 3 種不同的硬幣,1 元、3 元、5 元,我們要支付 9 元,最少需要 3 個硬幣(3 個 3 元的硬幣)。
42 | 動態規劃實戰:如何實現搜索引擎中的拼寫糾錯功能?
如何編程計算萊文斯坦距離?
編輯距離指的就是,將一個字符串轉化成另一個字符串,需要的最少編輯操作次數(比如增加一個字符、刪除一個字符、替換一個字符)。編輯距離越大,說明兩個字符串的相似程度越小;相反,編輯距離就越小,說明兩個字符串的相似程度越大。對於兩個完全相同的字符串來說,編輯距離就是 0。
步驟:
- 是否符合多階段決策最優解模型
- 使用最簡單的回溯算法
- 根據回溯算法的代碼實現,我們可以畫出遞歸樹,看是否存在重複子問題。如果存在重複子問題,那我們就可以考慮能否用動態規劃來解決;如果不存在重複子問題,那回溯就是最好的解決方法。
- 狀態轉移方程
- 填充狀態表
- 編碼
如何編程計算最長公共子串長度?
最長公共子串長度(Longest common substring length)。
步驟:
- 定義狀態
- 回溯的處理思路
比較萊文斯坦距離與最長公共子串長度
其中,萊文斯坦距離允許增加、刪除、替換字符這三個編輯操作,最長公共子串長度只允許增加、刪除字符這兩個編輯操作。
而且,萊文斯坦距離和最長公共子串長度,從兩個截然相反的角度,分析字符串的相似程度。萊文斯坦距離的大小,表示兩個字符串差異的大小;而最長公共子串的大小,表示兩個字符串相似程度的大小。
解答開篇
將編輯距離最小的單詞,作爲糾正之後的單詞,提示給用戶。
課後思考
我們有一個數字序列包含 n 個不同的數字,如何求出這個序列中的最長遞增子序列長度?比如 2, 9, 3, 6, 5, 1, 7 這樣一組數字序列,它的最長遞增子序列就是 2, 3, 5, 7,所以最長遞增子序列的長度是 4。
幾個動態規劃問題
問題1:0-1 揹包問題(0-1 揹包問題升級版)
問題2:如何巧妙解決“雙十一”購物時的湊單問題?
問題3:楊輝三角問題,求出從最高層移動到最底層的最短路徑長度
問題4:棋盤問題,假設我們有一個 n 乘以 n 的矩陣 w[n][n]。矩陣存儲的都是正整數。棋子起始位置在左上角,終止位置在右下角。我們將棋子從左上角移動到右下角。每次只能向右或者向下移動一位。從左上角到右下角,會有很多不同的路徑可以走。我們把每條路徑經過的數字加起來看作路徑的長度。那從左上角移動到右下角的最短路徑長度是多少呢?
問題5:硬幣找零問題,我們在貪心算法那一節中講過一次。我們今天來看一個新的硬幣找零問題。假設我們有幾種不同幣值的硬幣 v1,v2,……,vn(單位是元)。如果我們要支付 w 元,求最少需要多少個硬幣。比如,我們有 3 種不同的硬幣,1 元、3 元、5 元,我們要支付 9 元,最少需要 3 個硬幣(3 個 3 元的硬幣)。
問題6:如何編程計算萊文斯坦距離?
問題7:如何編程計算最長公共子串長度?
問題8:如何實現搜索引擎中的拼寫糾錯功能?
問題9:我們有一個數字序列包含 n 個不同的數字,如何求出這個序列中的最長遞增子序列長度?比如 2, 9, 3, 6, 5, 1, 7 這樣一組數字序列,它的最長遞增子序列就是 2, 3, 5, 7,所以最長遞增子序列的長度是 4。