動態規劃入門講稿
目錄
第一講 01背包問題
第二講 完全背包問題
第三講 多重背包問題
第四講 混合三種背包問題
第五講 LIS問題
第一講:01背包
問題描述:有N件物品和一個容量為V的背包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入背包可使價值總和最大。
基本解法:
類似此類問題我們稱之為01背包問題,其基本特點為:每種類型的物品有且僅有一件,對於每一個物品的操作僅有選取和不選取兩種,如果使用深度優先搜尋,檢查N個物品的選取情況的組合,那麽時間復雜度為O(2^n),若是物品費用,以及背包大小均為整數,就可以已動態規劃的方法高效求解。
我們用二維數組dp[i][j],表示前i件物品放入一個容量為j的背包內可獲得的最大價值。我們將:N件物品
01背包問題的狀態轉移方程式為:
dp[i][j] = max(dp[i-1][j],dp[i-1][j-c[i]]+w[i])
這個方程式非常重要,所有背包問題均由這個方程式衍生,所以我們著重解釋下這個方程式:
將前i件物品放入容量為v的背包中”這個子問題,若只考慮第i件物品的策略(放或不放),那麽就可以轉化為一個只牽扯前i-1件物品的問題。
若我們不放入第i件物品,那麽問題就轉化為:“前i-1件物品放入容量為j的背包” ,其最優價值為dp[i-1][j]。
若我們放入第i件物品,那麽問題就轉化為:“將前i-1件物品放入容量為(j-c[i])的背包內(因為放入第i件物品時背包體積為j,所以未放入第i件物品時,前i-1件物品占有的空間為(j-c[i]))”,此時最優價值為dp[i-1][j-c[i]]加上放入第i件物品獲得的價值w[i]。
對於這兩種操作,我們選取最優解,即為dp[i][j]的值。
列出偽代碼如下:
for i=1...N
for j=1...V
dp[i][j] = max(dp[i-1][j],dp[i-1][j-c[i]]+w[i])
空間復雜度的優化:
上述狀態轉移方程式的時間復雜度為:N*V,空間復雜度為亦是N*V。對於時間復雜度的優化是很艱難的,我們著重討論對於空間復雜度的優化。
我們再次分析上述狀態轉移方程式:
dp[i][j] = max(dp[i-1][j],dp[i-1][j-c[i]]+w[i])
我們可以看出對於第i件物品的存取策略僅直接依賴與第(i-1)件物品的存取策略。假設我們將dp[0...n][0...v]壓縮為dp[0...v],在進行第i次循環時,若還未進行更新,那麽此時的dp[j]和dp[j-c[i]]存放的值分別對應:dp[i-1][j]和dp[i-1][j-c[i]]。但需要註意的是,這要求我們在每次主循環的時候已 j = (V->0)的順序來推dp[j],這樣做的目的為:確保無後效性,確保dp[j],dp[j-c[i]]的值均來自上一輪,同時確保每件物品僅能選入一次。
列出偽代碼如下:
for i=1...N
for j=V...c[i] (很明顯 j-c[i]的值應不小於0)
dp[j] = max(dp[j],dp[j-c[i]]+w[i])
問題細分:
01背包問題可以繼續向下細分為2個類型:
1.求恰好裝滿背包時的最優解
2.未要求恰好裝滿背包時的最優解
事實上,這兩個問題在解決時,僅在初始化時有所不同
對於第一類問題,除dp[0]=0外,其他dp[]值均初始化為負無窮。
對於第二類問題,全部初始化為0。
我們這樣理解這兩個問題:對於dp的初始化,我們將其理解為在未放入任何值時,背包的最優解(合法狀態)。對於第一類問題,除容量為0的背包可以被價值為0的物品“恰好裝滿”外,其他容量的背包均無合法解,因此我們賦予其一個極小值,以便未來可以被任何一個合法值所更新。對於第二類問題,因為未要求“恰好裝滿”,所以對於任何體積的背包均由一個合法解即:“不裝入任何物品”,此時背包的價值為0,所以我們將dp初始化為0。幾乎所有背包問題,都可細分出這兩類問題,因此大家因掌握這個技巧。
第二講:完全背包
題目描述:有N種物品和一個容量為V的背包,每種物品都有無限件可用。第i種物品的費用是c[i],價值是w[i]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
基本解法:
這個問題是01問題的衍生問題,與01背包的區別在於:對於每種物品可以無限取。所以對於任意一件物品,它的存取策略包括但不局限與取或不取,而是有取0件,1件,2件……n件……等。
類似的,我們可以模仿01背包的狀態轉移方程式,寫出完全背包的狀態轉移方程式:
dp[i][j] = max(dp[i-1][j-k*c[i]]+k*w[i])
接下來我們來解釋這個方程式:dp[i][j]代表前i種物品放入容量為j的背包時的最優解。k代表第i件物品的選取數量,很明顯 k*w[i]的值應大於等於0且小於等於j,通過枚舉k的值我們可以求解方程式。
復雜度的優化:
時間復雜度的優化:完全背包問題依然有N*V個狀態需要我們逐一求解,但是對於每個狀態的求解已經無法在常數時間內完成。因為要枚舉k值,所以對於每個狀態的求解耗時為:j/c[i]。總的時間復雜度則為 P*N*V(P為非一的系數)。
相較於01背包,完全背包額外的時間消耗發生在枚舉k時,所以我們思考能否在此進行優化?事實上我們可以利用二進制對其枚舉過程進行優化,將(j/c[i])件物品拆分為價值為 w[i]*2^p ,重量為c[i]*2^p的若幹件物品。p值不相同,那麽(j/c[i])件物品將會被分為log2(j/c[i])件,從而大大節省了時間。那麽為什麽可以通過二進制進行優化呢?
我們分析:
2^0 = 1
2^1 = 10
2^2 = 100
2^3 = 1000
……
對於2^p-1內的數字,每一位上都有1的存在,所以我們可以通過組合相加得到2^p-1內的所有整數。
通過二進制可以將P*N*V的P降低至log級別,是非常巨大的優化,但是依然有更好的優化方法,使空間復雜與時間復雜度均降低至N*V級別的,偽代碼如下:
for i=1...N
for j=0...V
dp[j] = max(dp[j],dp[j-c[i]]+w[i])
我們對比01背包問題狀態壓縮後的偽代碼發現,兩者不同的僅為:j的遍歷順序不同,前者逆序,後者正序。為什麽僅改變遍歷順便可以解決完全背包問題呢?我們再次分析01背包逆序枚舉的原因:“確保無後效性,確保dp[j],dp[j-c[i]]的值均來自上一輪,同時確保每件物品僅能選入一次”即:保證在考慮第i件物品的選取策略時,依據的是絕無第i件物品入選的的子結果dp[i-1][j-c[i]]。 然而完全背包對於一件物品可多次入選,所以在考慮“加選一件第i種物品”這種策略時,我們所依賴的正是一個已經入選若幹i物品的子結果dp[i][j-c[i]],所以我們應當正序枚舉j值。
第三講:多重背包
題目描述:有N種物品和一個容量為V的背包。第i種物品最多有n[i]件可用,每件費用是c[i],價值是w[i]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
基本解法:
多重背包和完全背包十分相似,狀態轉移方程式也類似:
Dp[i][j] = max(dp[i][j-k*c[i]]+k*w[i])
不同的是k的取值範圍為:0<=k<=n[i]。
方程式的含義與完全背包相似,不再解釋
復雜度的優化:
與完全背包相似,額外的時間支出發生在枚舉k值上面,所以我們考慮能否在此處進行優化。同樣的,我們考慮二進制的思想:將第i種物品分成若幹件物品,其中每件物品有一個系數,這件物品的費用和價值均是原來的費用和價值乘以這個系數。使這些系數分別為1,2,4,...,2^(k-1),n[i]-2^k+1,且k是滿足n[i]-2^k+1>0的最大整數。例如,如果n[i]為13,就將這種物品分成系數分別為1,2,4,6的四件物品。要註意的是拆分得到的物品的系數和為n[i],確保不可能取多於n[i]件的第i件物品。
附:二進制的優化方法已經可以解決絕大部分的多重背包問題,但對於極個別的難題,還有更優解法:應用單調隊列的方法使每個狀態的值可以以均攤O(1)的時間求解,最終使得時間復雜度壓縮至O(N*V)詳情見樓天成《男人八題》。但因為過於復雜,這裏不再討論。
第四講:混合背包
混合背包問題一般分為以下幾類:
1.01背後和完全背包的混合:部分物品只可選入一次,部分物品可無限次選入。對於此類問題我們再設計狀態轉移方程式是分開考慮即可:
For i 1...n
If(第i件物品僅可以選入一次)
For j V...0
Dp[j] = max(dp[j],dp[j-c[i]]+w[i])
If(第i件物品可無限次選入)
For j 0...V
Dp[j] = max(dp[j],dp[j-c[i]]+w[i])
2.01背包,完全背包,多重背包混合:部分物品只可入選一次,部分物品可入選無限次,部分物品可以入選n[i]次。對於此類問題,我們可以將符合完全背包和多重背包的物品通過二進制拆分成若幹個物品,將拆分而來的物品視為僅可入選一次獨立的物品,從而將混合背包問題轉化為01背包問題。
3.完全背包,多重背包混合。解法與2相同。
第五講:最長上升子序列問題(LIS Longest Increasing Subsequence)
題目描述:給定N個數,求這N個數的最長上升子序列。
基本解法:
長度為N的序列A[],其子序列有2^n個,因而采用窮舉的方法耗費巨大,故而這裏采用動態規劃的方法。
A[i]代表數組中的第i個數字,dp[i]代表以A[i]為結尾的最長上升子序列的長度。那麽在更新dp[i]時,dp[i]僅依賴與dp[1...(j-1)],很容易想出:對於a[j],若a[i]>a[j],那麽dp[i]的值應當從{dp[i],dp[j]+1}中選取最大值。換而言之,dp[i]=max{dp[j]+1}(1<=j<i)。
列出偽代碼:
For i 1...n
For j 1..i-1
If a[i] > a[j]
dp[i] = max(dp[i],dp[j]+1)
初始化問題:數組中的任何一個元素都可以看做長度為1的子序列,所以我們將dp初始化為1.
復雜度的優化:
上述的解法的時間復雜度為為:O(n^2),事實上,我們可以將動態規劃和二分搜索結合起來,從而將時間復雜度進一步壓縮至:O(n*log2(n))。
我們試圖用貪心的思想去尋找最長上升子序列本身,並用數組L[]存儲,那麽length(L)就是最長上升子序列的長度。
例如A = {4,1,6,2,8,5,7,3}
1.L = [4] 將4加入L
2.L = [1] 1小於4,為了將來能加入更多的數,使LIS盡可能長,故而我們用a[i],替換第一個大於a[i]的數
3.L = [1,6] 6大於1,直接將6加入L
4.L = [1,2]
5.L = [1,2,8]
6.L = [1,2,5]
7.L = [1,2,5,7]
8.L = [1,2,3,7]
最終length(L) = 4 即最長上升自序列的長度為4。
因為L內的元素始終為單調不下降,所以我們在查找第一個大於a[i]的數字時,可以用二分查找的方式進行優化。
偽代碼如下:
For i 1...N
If(a[i] > L[length])
L[length+1] = a[i]
Else
pos = lower_abound(L,L+length,a[i])-L
L[pos] = a[i]
問題細分:
LIS可以細分為兩大類:
1.最長嚴格上升子序列
2.最長非嚴格上升自序列。
兩者的區別在於:前者的LIS中 L[i] > L[i-1],後者LIS中L[i] >= L[i-1]。兩個問題的解法一致,但在狀態轉移時要註意。
問題變形:
LIS問題有很多的變形,常見的有已下幾種:
1.最長下降子序列
解法:逆序處理A[]即可!
2.環狀最長上升子序列
解法:將首尾拼接!
3.最長連續上升子序列
解法:dp[i]僅依賴於dp[i-1],而非更早的狀態!
附:
本講稿部分內容借鑒了或引用了以下博客或書籍內容:
崔添翼:https://github.com/tianyicui/pack
《計算機科學及編程導論》
《挑戰程序設計》
作者博客ID及地址:聲聲醉如蘭,http://www.cnblogs.com/alan-W/轉載請註明出處
動態規劃入門講稿