4大經典算法問題
如果我們將這四種算法思想分一下類,那貪心、回溯、動態規划可以歸為一類,而分治單獨可以作為一類,因為它跟其他三個都不大一樣。為什么這么說呢?前三個算法解決問題的模型,都可以抽象成我們今天講的那個多階段決策最優解模型,而分治算法解決的問題盡管大部分也是最優解問題,但是,大部分都不能抽象成多階段決策模型
貪心
就是每次取最優,最大解。局部最優,得出全局最優。它是動態規划的一種特例。
實際上,用貪心算法解決問題的思路,並不總能給出最優解,目前使用的比較少,需要針對非常特殊的數據場景。
分治算法(divide and conquer)
分治算法用四個字概括就是“分而治之”,將原問題划分成 n 個規模較小而結構與原問題相似的子問題,遞歸地解決這些子問題,然后再合並其結果,就得到原問題的解
分治算法是一種處理問題的思想,遞歸是一種編程技巧
要解決這種數據量大到內存裝不下的問題,我們就可以利用分治的思想。我們可以將海量的數據集合根據某種方法,划分為幾個小的數據集合,每個小的數據集合單獨加載到內存來解決,然后再將小數據集合合並成大數據集合。實際上,利用這種分治的處理思路,不僅僅能克服內存的限制,還能利用多線程或者多機處理,加快處理的速度。
for example
在統計方面比較多,比如統計我國人口,要知道我國人口就要先知道每個省人口,要知道省人口就要知道每個市人口,要知道市人口就要知道每個區縣人口,直到村社區,然后匯總求的總人數。
回溯
回溯的處理思想,有點類似枚舉搜索。我們枚舉所有的解,找到滿足期望的解。為了有規律地枚舉所有可能的解,避免遺漏和重復,我們把問題求解的過程分為多個階段。每個階段,我們都會面對一個岔路口,我們先隨意選一條路走,當發現這條路走不通的時候(不符合期望的解),就回退到上一個岔路口,另選一種走法繼續走。
常見問題,八皇后和背包的問題。
回溯的問題就是,時間復雜度為O(n^2), 指數級別,復雜度很高。一般使用遞歸來實現,我們會使用備忘錄去記錄已經計算過的情況,避免重復計算,有時候我們也可以根據實際情況進行剪枝,加快查詢。
動態規划 dynamic programing 又叫 DP
我們把問題分解為多個階段,每個階段對應一個決策。我們記錄每一個階段可達的狀態集合(去掉重復的),然后通過當前階段的狀態集合,來推導下一個階段的狀態集合,動態地往前推進。
大部分動態規划能解決的問題,都可以通過回溯算法來解決,只不過回溯算法解決起來效率比較低,時間復雜度是指數級的。動態規划算法,在執行效率方面,要高很多。盡管執行效率提高了,但是動態規划的空間復雜度也提高了,所以,很多時候,我們會說,動態規划是一種空間換時間的算法思想。
什么樣的問題適合用動態規划來解決呢?換句話說,動態規划能解決的問題有什么規律可循呢?
一般是用動態規划來解決最優問題,而解決問題的過程,需要經歷多個決策階段。每個決策階段都對應着一組狀態。然后我們尋找一組決策序列,經過這組決策序列,能夠產生最終期望求解的最優值。
三個特征
1. 最優子結構
最優子結構指的是,問題的最優解包含子問題的最優解。反過來說就是,我們可以通過子問題的最優解,推導出問題的最優解。
我們也可以理解為,后面階段的狀態可以通過前面階段的狀態推導出來。
2. 無后效性
后效性有兩層含義,含義是,在推導后面階段的狀態的時候,我們只關心前面階段的狀態值,不關心這個狀態是怎么一步一步推導出來的,且當前階段的狀態不受后面階段的狀態影響。
3. 重復子問題
不同的決策序列,到達某個相同的階段時,可能會產生重復的狀態。所以一般可以緩存一起來,避免重復計算。
動態規划DP解法思路
先通過回溯分析出是否存在重復子問題,以及是怎么發生的,然后嘗試在紙上畫一下,遞歸樹,看是否能用回溯+備忘錄的方式解決重復子問題。這樣得到的結果與DP最后得到的結果效率差不多。
1. 狀態轉移表法/ 狀態轉移方程法
我們先畫出一個狀態表。狀態表一般都是二維的(可能是三維/四維復雜場景),所以你可以把它想象成二維數組。其中,每個狀態包含三個變量,行、列、數組值。我們根據決策的先后過程,從前往后,根據遞推關系,分階段填充狀態表中的每個狀態。最后,我們將這個遞推填表的過程,翻譯成代碼,就是動態規划代碼了。
比如: matrix[i][j] + Math.min(states[i][j-1], states[i-1][j]);
第一個,第二個默認值先找出來。
狀態轉移方程法有點類似遞歸的解題思路。我們需要分析,某個問題如何通過子問題來遞歸求解,也就是所謂的最優子結構。根據最優子結構,寫出遞歸公式,也就是所謂的狀態轉移方程。有了狀態轉移方程,代碼實現就非常簡單了。一般情況下,我們有兩種代碼實現方法,一種是遞歸加“備忘錄”,另一種是迭代遞推。
min_dist(i, j) = w[i][j] + min(min_dist(i, j-1), min_dist(i-1, j))
狀態轉移方程是解決動態規划的關鍵
LeetCode
1. 最小路徑和
https://leetcode-cn.com/problems/minimum-path-sum/ 采用dp方程
class Solution { public int minPathSum(int[][] grid) { if (grid == null || grid.length == 0 || grid[0].length == 0) { return 0; } int rows = grid.length, columns = grid[0].length; int[][] dp = new int[rows][columns]; dp[0][0] = grid[0][0]; for (int i = 1; i < rows; i++) { dp[i][0] = dp[i - 1][0] + grid[i][0]; } for (int j = 1; j < columns; j++) { dp[0][j] = dp[0][j - 1] + grid[0][j]; } for (int i = 1; i < rows; i++) { for (int j = 1; j < columns; j++) { dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; } } return dp[rows - 1][columns - 1]; } }
2. 硬幣兌換
https://leetcode-cn.com/problems/coin-change/solution/322-ling-qian-dui-huan-by-leetcode-solution/
coins = [1, 2, 5], amount = 11
F(11)=min(F(11−1),F(11−2),F(11−5))+1
3. 乘積最大子數組
4. 三角形最小路徑和
https://leetcode-cn.com/problems/triangle/
[ [2], [3,4], [6,5,7], [4,1,8,3] ]
定義二維 dp 數組「自底向上的遞推」。
a. 狀態定義:
dp[i][j]表示從點 (i, j) 到底邊的最小路徑和。
b. 狀態轉移:
dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j]dp[i][j]=min(dp[i+1][j],dp[i+1][j+1])+triangle[i][j]
class Solution { public int minimumTotal(List<List<Integer>> triangle) { int n = triangle.size(); // dp[i][j] 表示從點 (i, j) 到底邊的最小路徑和。 int[][] dp = new int[n + 1][n + 1]; // 從三角形的最后一行開始遞推。 for (int i = n - 1; i >= 0; i--) { for (int j = 0; j <= i; j++) { dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle.get(i).get(j); } } return dp[0][0]; } } 作者:sweetiee 鏈接:https://leetcode-cn.com/problems/triangle/solution/di-gui-ji-yi-hua-dp-bi-xu-miao-dong-by-sweetiee/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。