dp笔记:关于DP算法和滚动数组优化的思考
從網上總結了一些dp的套路以及對滾動數組的一些思考,現記錄如下,希望以后回顧此類算法時會有所幫助。
目錄
- 1、DP算法經驗
- 1、DP算法核心:
- 2、DP算法類別以及例題
- 例1:三步問題
- 例2:最小路徑和
- 例3:乘積最大子數組
- 例4:[線性DP]最長上升子序列(LIS)
- 例5:[線性DP]俄羅斯套娃信封(排序降維后視作LIS)
- 例6:[線性DP]最長公共子序列(LCS、最基本的雙串匹配模型)
- 2、滾動數組優化與背包問題
- 1、01背包問題
- 2、完全背包問題
- 3、多重背包問題
- 3、參考
1、DP算法經驗
1、DP算法核心:
1、確定【DP狀態】
2、確定【DP狀態轉移方程】
其中DP狀態又需要考慮到兩點:
1、最優子結構
將原有問題化為一個個子問題,即子結構。對于每一個子問題,其最優值均由【更小規模的子問題的最優值】推導過來
2、無后效性
我們只關心子問題的最優值,不關心子問題的最優值是怎樣的得到的。
2、DP算法類別以及例題
例1:三步問題
問題描述:樓梯有n階,一次可上1階,2階,或者3階。問總共偶多少種方案。同時,由于結果很大,請對結果取模MOD;
分析:
1、目標:得到爬到n階樓梯的總方案數
2、子問題:爬i階樓梯時的總方案數
3、
【1】定義f[i]為爬i階樓梯時的總方案數,一般來說,爬第i階,它可能是由前1階爬1階、前2階爬2階、前3階爬3階得到的。
所以f[i] 的取值取決于:f[i-1]、f[i-2]、f[i-3]。
【2】f[i]取值與f[i-1]、f[i-2]、f[i-3]的數值如何得到無關。
【3】dp狀態轉移方程:
code:
//鏈接:https://zhuanlan.zhihu.com/p/180443034 class Solution {public:vector<int>f;int MOD=10000007;int waysToStep(int n){f.reszie(n+1);f[0]=1; //第0階方案為1for(int i=1;i<=n;i++){if(i==1)f[i]=f[i-1]; else if(i==2)f[i]=(f[i-1]+f[i-2])%MOD; elsef[i]=(f[i-1]+f[i-2]+f[i-3])%MOD;}return f[n];} };例2:最小路徑和
問題描述:給定一個包含非負整數mxn網格,找出一條從左上角到右下角的路徑,只能向右、向下,使路徑上的數字綜合最小
輸入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
輸出: 7
解釋: 因為路徑 1→3→1→1→1 的總和最小。
分析:
1、目標:獲取網格grid[0][0]到grid[m][n]的最小數字和:f[m][n]
2、子問題:獲取網格grid[0][0]到grid[i][j]的最小數字和:f[i][j]
3、由題意可知到達網格grid[i][j]有兩種方法:一種是從它的左邊過來,一種是從它的上邊過來(因為只能向右、向下移動)
所以f[i][j] 的取值取決于:f[i-1][j] (左邊)、f[i][j-1] (上邊)
【2】f[i][j] 的取值取決于:f[i-1][j] (左邊)、f[i][j-1] (上邊)如何得到無關。
注意:若改為可以向上向下向左向右,且不能重復格子,則f[i][j-1]到f[i+1][j]所對應的具體路徑會影響f[i][j]取值,則會不符合無后效性
【3】dp狀態轉移方程:
code:
//鏈接:https://zhuanlan.zhihu.com/p/180443034 class Solution {public:int minPathSum(vector<vector<int>> &grid){for(int i=0;i<grid.size();i++){for(int j=0;j<grid[0].size();j++){if(i==0 && j==0) continue; //grid[0][0]不需要修改int tp=1e9;//從左邊和上邊中選取一個較小的,作為路徑if(i>0) tp = min(tp,grid[i-1][j]);if(j>0) tp = min(tp,grid[i][j-1]); grid[i][j]+=tp;}}return grid[grid.size()-1][grid[0].size()-1];} }; //注意,這里為了節約空間,不新開辟數組,而是在grid數組的基礎上進行修改,這樣就是在grid的基礎上加上左、上鄰值(最小路徑和)的最小值。推導:f[2][2] = f[2][1]+1=f[2][0]+2=f[1][0]+3=f[0][0]+6=1+6=7
例3:乘積最大子數組
問題描述:一個整數數組nums,找出數組中乘積最大的連續子數組(該子數組最少包含一個數字),并返回子數組對應的乘積
輸入:[2,3,-2,4]
輸出: 6
解釋: 因為路徑 2 * 3=6
輸入:[-2,0,-1]
輸出: 0
分析:
1、目標:在左端點為0,右端點為nums.size的連續區間中找到最大乘積
2、子問題:在左端點為0,右端點為i的連續區間中找到最大乘積
3、分析狀態定義是否符合最優子結構
nums=[2,-1,-2]
對應的f[i]=[2,-1,4]
f[3] =4 != nums[3] !=f[2] * nums[3],所以f[i]與f[i-1]無關。
即DP轉臺最優質的無法由更小的規模的DP狀態最優值推出,不符合【最優子結構】原則。
出錯原因:若nums[i]為負,則f[i] * nums[i]只會越來越小,因此需要分正負情況討論。
【1】、nums[i]>0
【2】、nums[i]<=0
f[i] = max(nums[i],以i-1為右端點的連續區間的最小乘積*nums[i])這樣就需要引入新的DP狀態
maxn[i]:表示以右端點的連續區間最大乘積
minn[i]:表示以右端點的連續區間最小乘積
maxn[i]、minn[i]由maxn[i-1]、minn[i-1]推導而出。
code:
例4:[線性DP]最長上升子序列(LIS)
題目描述:給定一個無序的整數數組,找到其中最長上升子序列的長度。
示例:
輸入:[10,9,2,5,3,7,101,18]
輸出:4
解釋:最長的上升子序列是[2,3,7,101],它的長度是4
思路:
1、先從小規模入手:
序列長度為1時=>ans=1
序列長度為2時=>Y[1]>=Y[2]的話,長度為1;Y[1]<Y[2],長度為2
DP狀態:f[i]表示僅考慮前i個數,以第i個數結尾的最長上升子序列的最大長度。
這種方法就是每一次嘗試尋找“可以接下去的”那一個數,換句話說,設原序列為a,則
當aj<ai(j<i)且f[j]+1>f[i]時,f[i]=f[j]+1。
對于每一個數,他都是在“可以接下去”的中,從前面的最優值+1轉移而來。通俗的來說,你肯定就是在所有能找到的里面取最好的一個。
因此,這個算法是可以求出正確答案的。復雜度很明顯,外層i枚舉每個數,內層j枚舉目前i的最優值,即 O(n2)
code:
例5:[線性DP]俄羅斯套娃信封(排序降維后視作LIS)
題目描述:
給定一些標記了寬度和高度的信封,寬度和高度以整數對形式 (w, h) 出現。當另一個信封的寬度和高度都比這個信封大的時候,這個信封就可以放進另一個信封里,如同俄羅斯套娃一樣。請計算最多能有多少個信封能組成一組“俄羅斯套娃”信封(即可以把一個信封放到另一個信封里面)。
示例:(不允許旋轉信封)
輸入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
輸出: 3
解釋: 最多信封的個數為 3, 組合為: [2,3] => [5,4] => [6,7]。
分析:
1、簡要概括題意,求一組二維上升子序列
,同時滿足:
由于裝入信封需要滿足兩個條件,而且信封的順序是無序的并且是可以調整順序的。所以我們可以先限制一維例如w,使信封按照w的大小,從小到大排列好,這樣就可以確定以w為準則的話后面的總是可以將前面的包含。就只需要考慮h維度了。
接下來就和LIS步驟類似了:給定一個無序的整數數組(h維度上無序),找到其中最長上升子序列的長度。
code:
對這個問題有疑惑的,可以轉到我的記錄:
C++中的sort函數對二維數組排序是按照什么準則?
例6:[線性DP]最長公共子序列(LCS、最基本的雙串匹配模型)
題目描述:
給定兩個字符串text1和text2,返回兩個字符串中的最長公共子序列長度。
例:
“ace”為"abcde"的子序列,若兩個字符串無公共子序列,返回0
IN: text1=“abcde”,text2=“ace”
OUT:3
IN:text1=“abc”,text2=“def”
OUT:0
tips:1<=text.length<=1000
1<=tex2.length<=1000
text中均為小寫英文字母
分析:
首先確定線性DP特點:DP狀態沿著各個維度線性增長。
目標:獲取長度為n1的字符串與長度為n2的字符串的最長公共子序列
子問題:獲取長度為i的字符串與長度為j的字符串的最長公共子序列
確定dp狀態:f[i][j]:表示第一個字符串前i個字符與第二個字符串前j個字符的最長公共子序列長度
確定狀態轉移方程:
text1[i]與text2[j]有兩種結果,一個是相同,一個是不相同
1、如果text1[i]與text2[j]不相同 , f[i][j] 從f[i-1][j],f[i][j-1]選出最大的繼承下來,此時并沒有出現增長
f[i][j]=max(f[i-1][j],f[i][j-1]);
2、如果text1[i]與text2[j]相同,說明我們可以增加一種方式,且f[i][j]:表示第一個字符串前i個字符與第二個字符串前j個字符的最長公共子序列長度,所以f[i][j]是在f[i-1][j-1]的基礎上的來的。
f[i][j]=f[i-1][j-1]+1;
這樣的話復雜度為O(nm),n,m分別為text1和text2的長度
2、滾動數組優化與背包問題
1、01背包問題
一共有N件物品,從第i(i從1開始)件物品的重量為w[i],價值為v[i].在總重量不超過背包承載上限W的情況下,求出能夠裝入背包的最大價值是多少?
分析步驟:
1、獲取總目標:將N件物品裝進限重為W的背包可以獲得的最大價值
2、子問題:將前i個物品裝進限重為j的背包可以獲得的最大價值,0<=i<=N,0<=j<=W;
3、dp狀態定義以及檢查無后效性:dp[i][j]:表示將將前i個物品裝進限重為j的背包可以獲得的最大價值,0<=i<=N,0<=j<=W;
再次分析:dp[i][j]由兩個部分組成:不裝入第i個物品、裝入第i個物品(假如能夠裝入的話)
即dp[i][j]只與dp[i-1][j] (不裝入)以及dp[i-1][j-w[i]]+v [i] (裝入第i個物品)有關,并且與子狀態的具體怎么得來無關,符合無后效性
4、dp狀態轉移方程定義:
已知dp[i][j]只與dp[i-1]j以及dp[i-1][j-w[i]] (裝入第i個物品)有關,那么具體是怎樣的關系:
dp[i][j]指的是將前i個物品裝進限重為j的背包可以獲得的最大價值
dp[i-1][j]指的是將前i-1個物品裝進限重為j的背包可以獲得的最大價值
dp[i-1][j-w[i]+v[i]指的是將前i-1個物品裝進限重為j的背包可以獲得的最大價值+將第i個物品裝入的總價值.
dp[i][j]是在上述兩種可能的情況中較優的一種,也就是價值更大的一種,所以可以這樣描述:
dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])
接下來介紹一種在DP算法比較常用的手法:滾動數組優化
在上述的dp狀態方程中我們可以發現:dp[i][j] 只與dp[i-1][0,…,j-1]有關,也就是說dp狀態的二維數組的第一維在此時是浪費的,我們用到的只有第二維。所以可以采用滾動數組優化,去掉dp中的第一維,變為如下形式:
dp[j] = max(dp[j],dp[j-w[i]+v[i])
滾動數組是DP中的一種編程思想。簡單的理解就是讓數組滾動起來,每次都使用固定的幾個存儲空間,來達到壓縮,節省存儲空間的作用。起到優化空間,主要應用在遞推或動態規劃中(如01背包問題)。因為DP題目是一個自底向上的擴展過程,我們常常需要用到的是連續的解,前面的解往往可以舍去。所以用滾動數組優化是很有效的。利用滾動數組的話在N很大的情況下可以達到壓縮存儲的作用。
參考:《滾動數組》—滾動數組思想,運用在動態規劃當中
此時滾動的方向尤其重要。
例如在遞推dp[j]時應按W到0的順序,這樣才能保證推dp[j]時dp[j-w[i]]保存的是狀態dp[i-1,j-w[i]]的值
因為我們進行操作的時候,是用一維數組dp同時存儲這一狀態和上一狀態的值的,從W到0遞推也就是說從后往前將i-1狀態更新為i狀態。
當推導到j時,j往后的是i狀態。而j之前的是i-1狀態。要符合優化之前的狀態方程,所以01背包問題的優化數組滾動方向必然是從后往前的。
(不知道解釋的清不清楚。。。)
優化后的核心代碼如下:需要注意的是滾動數組更新的結束條件是j>=w[i],意思就是是背包限重必須大于w[i],也就是限重必須大于第i個物品的質量(這是我們之前在談論狀態方程說過的,dp[i][j]由兩個部分決定,一個是不裝入第i件物品,一個是裝入第i件物品(前提是裝的下),這里就是為了滿足裝得下)
2、完全背包問題
問題描述:
每種物品有無限多個:一共有N種物品,每種物品有無限多個,從第i(i從1開始)種物品的重量為w[i],價值為v[i].在總重量不超過背包承載上限W的情況下,能夠裝入背包的最大價值為多少?
1、分析總目標:將N種物品裝進限重為W的背包可以獲得的最大價值(注意這里是種而不是件)
2、分析子狀態:將前i種物品裝進限重為j的背包可以獲得的最大價值,0<=i<=N,0<=j<=W;
3、dp狀態定義以及檢查無后效性:dp[i][j]:表示將將前i種物品裝進限重為j的背包可以獲得的最大價值,0<=i<=N,0<=j<=W;
再次分析:dp[i][j]由兩個部分組成:不裝入第i種物品、裝入第i種物品(假如能夠裝入的話)
即dp[i][j]只與dp[i-1][j] (不裝入)以及dp[i][j-w[i]]+v[i] (裝入第i種物品)有關,并且與子狀態的具體怎么得來無關,符合無后效性
注意:這里使用的是dp[i][j-w[i]]+v[i],而不是dp[i-1][j-w[i]]+v[i],兩者有什么區別呢?
首先確定一點:裝入第i種商品后還可以再繼續裝入第i種商品。
dp[i][j]指的是將前i種物品裝進限重為j的背包可以獲得的最大價值
dp[i-1][j]指的是將前i-1種物品裝進限重為j的背包可以獲得的最大價值
dp[i-1][j-w[i]+v[i]指的是將前i-1種物品裝進限重為j的背包可以獲得的最大價值+將一個第i種物品裝入的總價值.
dp[i][j-w[i]+v[i]指的是將前i種物品裝進限重為j的背包可以獲得的最大價值+將一個第i種物品裝入的總價值
也就是說,當我們裝入第1個第i種物品后,假設讓dp[i][j]=dp[i][j-w[i]+v[i],接下來再次將第2個第i中國物品裝入,此時i不改變,改變的是j,也就是背包容量。
得到的dp狀態方程為:
現在我們對完全背包進行滾動數組優化
與01背包類似,同樣二維狀態數組可以優化成1維狀態數組,不同的是這里的數組滾動方向只能是正向,因為這里的max的第二項是dp[i],而不是01背包的dp[i-1],這里就需要覆蓋,而01背包是要避免覆蓋的。
分析一下:
例如在遞推dp[j]時應按w[i]到W的順序,這樣才能保證推dp[j]時dp[j-w[i]]保存的是狀態dp[i,j-w[i]]的值
因為我們進行操作的時候,是用一維數組dp同時存儲這一狀態和上一狀態的值的,從w[i]到W遞推也就是說從前往后將i-1狀態更新為i狀態。
當推導到j時,j往后的是i-1狀態。而j之前的是i狀態。要符合優化之前的狀態方程,所以01背包問題的優化數組滾動方向必然是從后往前的,注意沒對第j項進行賦值時,第j項也是i-1狀態,所以這樣顯然就符合了狀態方程,優化如下:
核心代碼如下;
int[] dp = new int[W + 1]; for (int i = 0; i < N; i++) {// 滾動數組優化 正序枚舉jfor (int j = w[i]; j <= W; j--) {dp[j] = Integer.max(dp[j], dp[j - w[i]] + v[i]);} }3、多重背包問題
問題描述:
一共有N種物品,從第i(i從1開始)種物品的數量為n[i],重量為w[i],價值為v[i].在總重量不超過背包承載上限W的情況下,能夠裝入背包的最大價值為多少?
分析:
從裝第i種物品出發,裝入第i種物品0件、1件、…n[i]件(并且要滿足不超過限重)
dp狀態方程為:
接下來進行滾動數組優化,將狀態數組的第1維度消除,同時注意應該逆序操作,同樣的解釋思路,參見01背包。
核心代碼:
關于背包問題以及其他的優化思路,這里不做擴展。
3、參考
這些文章對我理解背包問題以及動態規劃有比較大的幫助:
1、滾動數組優化
2、《滾動數組》—滾動數組思想,運用在動態規劃當中
3、動態規劃之背包問題系列
4、九章算法 | 背包問題
5、怎樣學好動態規劃?
6、動態規劃算法3——最長上升子序列
7、我的知乎收藏
總結
以上是生活随笔為你收集整理的dp笔记:关于DP算法和滚动数组优化的思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++中的sort函数对二维数组排序是按
- 下一篇: 为什么最近都进不到拍卖行啊 有些人怎么就