算法之美11 - 思想

思想

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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章