背包問題
一、01背包問題
【問題】:
有N件物品和一個容量為V的背包。第i件物品的費用(即體積,下同)是w[i],價值是c[i]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。 基本思路:
這是最基礎的背包問題,特點是:每種物品僅有一件,可以選擇放或不放。
用子問題定義狀態:即f[i][v]表示前i件物品(部分或全部)恰放入一個容量為v的背包可以獲得的最大價值。則其狀態轉移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-w[i]]+c[i]}。
這個方程非常重要,基本上所有跟背包相關的問題的方程都是由它衍生出來的。所以有必要將它詳細解釋一下:“將前i件物品放入容量為v的背包中”這個子問題,若只考慮第i件物品的策略(放或不放),那么就可以轉化為一個只牽扯前i-1件物品的問題。如果不放第i件物品,那么問題就轉化為“前i-1件物品放入容量為v的背包中”;如果放第i件物品,那么問題就轉化為“前i-1件物品放入剩下的容量為v-w[i]的背包中”,此時能獲得的最大價值就是f [i-1][v-w[i]]再加上通過放入第i件物品獲得的價值c[i]。
注意f[i][v]有意義當且僅當存在一個前i件物品的子集,其費用總和為v。所以按照這個方程遞推完畢后,最終的答案並不一定是f[N][V],而是f[N][0..V]的最大值。如果將狀態的定義中的“恰”字去掉,在轉移方程中就要再加入一項f[i-1][v],這樣就可以保證f[N][V]就是最后的答案。但是若將所有f[i][j]的初始值都賦為0,你會發現f[n][v]也會是最后的答案。為什么呢?因為這樣你默認了最開始f[i][j]是有意義的,只是價值為0,就看作是無物品放的背包價值都為0,所以對最終價值無影響,這樣初始化后的狀態表示就可以把“恰”字去掉。
優化空間復雜度
以上方法的時間和空間復雜度均為O(N*V),其中時間復雜度基本已經不能再優化了,但空間復雜度卻可以優化到O(V)。
先考慮上面講的基本思路如何實現,肯定是有一個主循環i=1..N,每次算出來二維數組f[i][0..V]的所有值。那么,如果只用一個數組f [0..V],能不能保證第i次循環結束后f[v]中表示的就是我們定義的狀態f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-w[i]]兩個子問題遞推而來,能否保證在推f[i][v]時(也即在第i次主循環中推f[v]時)能夠得到f[i-1][v]和f[i-1][v-w[i]]的值呢?事實上,這要求在每次主循環中我們以v=V..0的逆序推f[v],這樣才能保證推f[v]時f[v-w[i]]保存的是狀態f[i-1][v-w[i]]的值。
【偽代碼】:
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-w[i]]+c[i]};
其中f[v]=max{f[v],f[v-w[i]]+c[i]}相當於轉移方程f[i][v]=max{f[i-1][v],f[i-1][v-w[i]]+c[i]},因為現在的f[v-w[i]]就相當於原來的f[i-1][v-w[i]]。如果將v的循環順序從上面的逆序改成順序的話,那么則成了f[i][v]由f[i][v-w[i]]推知,與本題意不符,但它卻是另一個重要的完全背包問題最簡捷的解決方案,故學習只用一維數組解01背包問題是十分必要的。
二、完全背包問題
【問題】:
有N種物品和一個容量為V的背包,每種物品都有無限件可用。第i種物品的費用是w[i],價值是c[i]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
【基本思路】:
這個問題非常類似於01背包問題,所不同的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已並非取或不取兩種,而是有取0件、取1件、取2件……等很多種。如果仍然按照解01背包時的思路,令f[i][v]表示前i種物品恰放入一個容量為v的背包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程,像這樣:f[i][v]=max{f[i-1][v-k*w[i]]+k*c[i]|0<=k*w[i]<= v}。
將01背包問題的基本思路加以改進,得到了這樣一個清晰的方法。這說明01背包問題的方程的確是很重要,可以推及其它類型的背包問題。
這個算法使用一維數組
【偽代碼】:
for i=1..N
for v=0..V
f[v]=max{f[v],f[v-w[i]]+c[i]};
你會發現,這個偽代碼與01背包問題的偽代碼只有v的循環次序不同而已。為什么這樣一改就可行呢?首先想想為什么01背包問題中要按照v=V..0的逆序來循環。這是因為要保證第i次循環中的狀態f[i][v]是由狀態f[i-1][v-w[i]]遞推而來。換句話說,這正是為了保證每件物品只選一次,保證在考慮“選入第i件物品”這件策略時,依據的是一個絕無已經選入第i件物品的子結果f[i-1][v-w[i]]。而現在完全背包的特點恰是每種物品可選無限件,所以在考慮“加選一件第i種物品”這種策略時,卻正需要一個可能已選入第i種物品的子結果f[i][v-w[i]],所以就可以並且必須采用v= 0..V的順序循環。這就是這個簡單的程序為何成立的道理。
這個算法也可以以另外的思路得出。例如,基本思路中的狀態轉移方程可以等價地變形成這種形式:f[i][v]=max{f[i-1][v],f[i][v-w[i]]+c[i]},將這個方程用一維數組實現,便得到了上面的偽代碼。
一個簡單有效的優化
完全背包問題有一個很簡單有效的優化,是這樣的:若兩件物品i、j滿足w[i]<=w[j]且c[i]>=c[j],則將物品j去掉,不用考慮。這個優化的正確性顯然:任何情況下都可將價值小費用高的j換成物美價廉的i,得到至少不會更差的方案。對於隨機生成的數據,這個方法往往會大大減少物品的件數,從而加快速度。然而這個並不能改善最壞情況的復雜度,因為有可能特別設計的數據可以一件物品也去不掉。
轉化為01背包問題求解
既然01背包問題是最基本的背包問題,那么我們可以考慮把完全背包問題轉化為01背包問題來解。最簡單的想法是,考慮到第i種物品最多選V/w[i]件,於是可以把第i種物品轉化為V/w[i]件費用及價值均不變的物品,然后求解這個01背包問題。這樣完全沒有改進基本思路的時間復雜度,但這畢竟給了我們將完全背包問題轉化為01背包問題的思路:將一種物品拆成多件物品。
更高效的轉化方法是:把第i種物品拆成費用為w[i]*2^k、價值為c[i]*2^k的若干件物品,其中k滿足w[i]*2^k<V。這是二進制的思想,因為不管最優策略選幾件第i種物品,總可以表示成若干個2^k件物品的和。這樣把每種物品拆成O(log(V/w[i])+1)件物品,是一個很大的改進。
【總結】
完全背包問題也是一個相當基礎的背包問題,它有兩個狀態轉移方程,分別在“基本思路”以及“O(VN)的算法“的小節中給出。希望你能夠對這兩個狀態轉移方程都仔細地體會,不僅記住,也要弄明白它們是怎么得出來的,最好能夠自己想一種得到這些方程的方法。事實上,對每一道動態規划題目都思考其方程的意義以及如何得來,是加深對動態規划的理解、提高動態規划功力的好方法。
三、多重背包問題
有N種物品和一個容量為V的背包。第i種物品最多有n[i]件可用,每件費用是w[i],價值是c[i]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
【基本算法】:
這題目和完全背包問題很類似。基本的方程只需將完全背包問題的方程略微一改即可,因為對於第i種物品有n[i]+1種策略:取0件,取1件……取n[i]件。令f[i][v]表示前i種物品恰放入一個容量為v的背包的最大權值,則:f[i][v]=max{f[i-1][v-k*w[i]]+ k*c[i]|0<=k<=n[i]}。復雜度是O(V*∑n[i])。
轉化為01背包問題
另一種好想好寫的基本方法是轉化為01背包求解:把第i種物品換成n[i]件01背包中的物品,則得到了物品數為∑n[i]的01背包問題,直接求解,復雜度仍然是O(V*∑n[i])。
但是我們期望將它轉化為01背包問題之后能夠像完全背包一樣降低復雜度。仍然考慮二進制的思想,我們考慮把第i種物品換成若干件物品,使得原問題中第i種物品可取的每種策略——取0..n[i]件——均能等價於取若干件代換以后的物品。另外,取超過n[i]件的策略必不能出現。
方法是:將第i種物品分成若干件物品,其中每件物品有一個系數,這件物品的費用和價值均是原來的費用和價值乘以這個系數。使這些系數分別為 1,2,4,...,2^(k-1),n[i]-2^k+1,且k是滿足n[i]-2^k+1>0的最大整數(注意:這些系數已經可以組合出1~n[i]內的所有數字)。例如,如果n[i]為13,就將這種物品分成系數分別為1,2,4,6的四件物品。
分成的這幾件物品的系數和為n[i],表明不可能取多於n[i]件的第i種物品。另外這種方法也能保證對於0..n[i]間的每一個整數,均可以用若干個系數的和表示,這個證明可以分0..2^k-1和2^k..n[i]兩段來分別討論得出,並不難,希望你自己思考嘗試一下。
這樣就將第i種物品分成了O(logn[i])種物品,將原問題轉化為了復雜度為O(V*∑logn[i])的01背包問題,是很大的改進。
四、混合三種背包問題
【問題】
如果將01背包、完全背包、多重背包混合起來。也就是說,有的物品只可以取一次(01背包),有的物品可以取無限次(完全背包),有的物品可以取的次數有一個上限(多重背包)。應該怎么求解呢?
01背包與完全背包的混合
考慮到在01背包和完全背包中最后給出的偽代碼只有一處不同,故如果只有兩類物品:一類物品只能取一次,另一類物品可以取無限次,那么只需在對每個物品應用轉移方程時,根據物品的類別選用順序或逆序的循環即可,復雜度是O(VN)。
【偽代碼】:
for i=1..N
if 第i件物品是01背包
for v=V..0
f[v]=max{f[v],f[v-w[i]]+c[i]};
else if 第i件物品是完全背包
for v=0..V
f[v]=max{f[v],f[v-w[i]]+c[i]};
再加上多重背包
如果再加上有的物品最多可以取有限次,那么原則上也可以給出O(VN)的解法:遇到多重背包類型的物品用單調隊列解即可。但如果不考慮超過NOIP范圍的算法的話,用多重背包中將每個這類物品分成O(log n[i])個01背包的物品的方法也已經很優了。
五、二維費用的背包問題
【問題】
二維費用的背包問題是指:對於每件物品,具有兩種不同的費用;選擇這件物品必須同時付出這兩種代價;對於每種代價都有一個可付出的最大值(背包容量)。問怎樣選擇物品可以得到最大的價值。設這兩種代價分別為代價1和代價2,第i件物品所需的兩種代價分別為a[i]和b[i]。兩種代價可付出的最大值(兩種背包容量)分別為V和U。物品的價值為c[i]。
【算法】
費用加了一維,只需狀態也加一維即可。設f[i][v][u]表示前i件物品付出兩種代價分別為v和u時可獲得的最大價值。
狀態轉移方程就是:f [i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+c[i]}。如前述方法,可以只使用二維的數組:當每件物品只可以取一次時變量v和u采用逆序的循環,當物品有如完全背包問題時采用順序的循環。當物品有如多重背包問題時拆分物品。
物品總個數的限制
有時,“二維費用”的條件是以這樣一種隱含的方式給出的:最多只能取M件物品。這事實上相當於每件物品多了一種“件數”的費用,每個物品的件數費用均為1,可以付出的最大件數費用為M。換句話說,設f[v][m]表示付出費用v、最多選m件時可得到的最大價值,則根據物品的類型(01、完全、多重)用不同的方法循環更新,最后在f[0..V][0..M]范圍內尋找答案。
另外,如果要求“恰取M件物品”,則在f[0..V][M]范圍內尋找答案。
六、分組的背包問題
【問題】
有N件物品和一個容量為V的背包。第i件物品的費用是w[i],價值是c[i]。這些物品被划分為若干組,每組中的物品互相沖突,最多選一件。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
【算法】
這個問題變成了每組物品有若干種策略:是選擇本組的某一件,還是一件都不選。也就是說設f[k][v]表示前k組物品花費費用v能取得的最大權值,則有f[k][v]=max{f[k-1][v],f[k-1][v-w[i]]+c[i]|物品i屬於第k組}。
【偽代碼】:
for 所有的組k
for v=V..0
for 所有的i屬於組k
f[v]=max{f[v],f[v-w[i]]+c[i]}
注意這里的三層循環的順序,“for v=V..0”這一層循環必須在“for 所有的i屬於組k”之外。這樣才能保證每一組內的物品最多只有一個會被添加到背包中。
另外,顯然可以對每組中的物品應用完全背包中“一個簡單有效的優化”。
【總結】
1、 最長不降子序列(LIS)
f[i]=max{f[j]}+1 (a[j]<=a[i],j<i) 可以用樹狀數組優化到 nlogn
2、 最長公共子序列(LCS)
If (a[i]==b[j]) f[i][j]=f[i-1][j-1]+1;
Else f[i][j]=max(f[i-1][j],f[i][j-1]);
如果 AB 兩串的元素個數相同且各個元素也相同(比如 AB 均由 n 個 1~n 的數字組成)則可以將 A 排序然后計算 B 的最長上升子序列。
3、 區間 DP
枚舉起始點,枚舉區間長度,再枚舉區間分界點。復雜度 n³。
如果是個環(比如環形的石子合並)則把環變成長度為 2n-1 鏈,然后 DP。只是枚舉區間長度最多是 n,然后取最大值即可。
4、 坐標 DP
其實這種東西我小學就知道了… f[i][j]=max(f[i-1][j],f[i][j-1])+a[i][j];遞推即可。
5、 01 背包
n 個物品,取或不取(不可拆分),求最大價值。
f[i]=max{f[i],f[i-w[j]]+v[j]}
注意:枚舉 i 是要從 V 到 w[j]遞減循環
6、 完全背包
n 個物品,物品數量無限,求最大價值。
朴素:和 01 背包的很像,只是多加了枚舉物品的數量:
f[i]=max{f[i-k*w[j]]+k*v[j]} (i-k*w[j]>0)
優化:省去了枚舉物品的數量,每次計算 i-k*w[j]時就已經包含了 i-(k-1)*w[j] 的情況
f[i]=max{f[i-w[j]]+v[j]}(w[j]<=i<=V)
注意:枚舉 i 是從 w[j]到 V 遞增循環
7、 多重背包
n 個背包,物品數量為 Mi,求最大價值。
朴素:f[i]=max{f[i-k*w[j]]+k*v[j]} (0<=k<=Mi)
優化:將物品的系數 k 由自然數改成 1,2,2^2,…,2^k,Mj-2^k+1,,然后再做 01 背包。
8、 混合背包
在選取物品的時候特判一下即可。
for (int i=1;i<=n;i++)
if 第 i 件物品是 01 背包 then 01pack(Wi,Vi);
else if 第 i 件物品是完全背包 then Completepack(Wi,Vi); else if 第 i 件物品是多重背包 then Multiplepack(Wi,Vi,Ni);
9、 二維費用背包
只要能理解 01 背包就能很好的理解此類問題。
f[u][v]=max(f[u][v],f[u-Wi][v-Pi]+V[i]);
10、 分組背包
n 個物品被分成 k 組,每組中的物品相互沖突,求最大價值。
大致有一些像 01 背包。
for (int p=1;p<=k;p++)
for (int j=v;j>=0;j--)
for i∈第 p 組的物品
f[j]=max(f[j],f[j-Wi]+Vi);
小優化:當 Wi>Wj && Vi<Vj 時可以直接刪掉 i 號物品
11、 有依賴的背包
選這個物品就一定要選擇它的兒子物品。
按照拓撲序進行轉移。
12、 背包求方案
在每次轉移時記錄 G[i,v]表示是否選了物品 i。
i=n;s=v;
While (i>0)
{
If G[i,j]==0 print 沒選 i
Else print 選了 i
s-=C[i];
i--;
}
字典序最小方案:倒着做背包,每次記錄,輸出即可。(具體請畫圖理解)
第 k 小方案:更新答案是進行歸並排序。
方案總數:把求 max 改成求 sum。
感謝各位與信奧一本通的鼎力相助!