【轉載】揹包九講
P01: 01揹包問題
題目
有N件物品和一個容量為V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
基本思路
這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。
用子問題定義狀態:即f[i][v]表示前i件物品恰放入一個容量為v的揹包可以獲得的最大價值。則其狀態轉移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。
這個方程非常重要,基本上所有跟揹包相關的問題的方程都是由它衍生出來的。所以有必要將它詳細解釋一下:“將前i件物品放入容量為v的揹包中”這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化為一個只牽扯前i-1件物品的問題。如果不放第i件物品,那麼問題就轉化為“前i-1件物品放入容量為v的揹包中”;如果放第i件物品,那麼問題就轉化為“前i-1件物品放入剩下的容量為v-c[i]的揹包中”,此時能獲得的最大價值就是f [i-1][v-c[i]]再加上通過放入第i件物品獲得的價值w[i]。
注意f[i][v]有意義當且僅當存在一個前i件物品的子集,其費用總和為v。所以按照這個方程遞推完畢後,最終的答案並不一定是f[N] [V],而是f[N][0..V]的最大值。如果將狀態的定義中的“恰”字去掉,在轉移方程中就要再加入一項f[i][v-1],這樣就可以保證f[N] [V]就是最後的答案。至於為什麼這樣就可以,由你自己來體會了。
優化空間複雜度
以上方法的時間和空間複雜度均為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-c[i]]兩個子問題遞推而來,能否保證在推f[i][v]時(也即在第i次主迴圈中推f[v]時)能夠得到f[i-1][v]和f[i-1][v -c[i]]的值呢?事實上,這要求在每次主迴圈中我們以v=V..0的順序推f[v],這樣才能保證推f[v]時f[v-c[i]]儲存的是狀態f[i -1][v-c[i]]的值。虛擬碼如下:
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-c[i]]+w[i]};
其中的f[v]=max{f[v],f[v-c[i]]}一句恰就相當於我們的轉移方程f[i][v]=max{f[i-1][v],f[i- 1][v-c[i]]},因為現在的f[v-c[i]]就相當於原來的f[i-1][v-c[i]]。如果將v的迴圈順序從上面的逆序改成順序的話,那麼則成了f[i][v]由f[i][v-c[i]]推知,與本題意不符,但它卻是另一個重要的揹包問題P02最簡捷的解決方案,故學習只用一維陣列解01揹包問題是十分必要的。
總結
01揹包問題是最基本的揹包問題,它包含了揹包問題中設計狀態、方程的最基本思想,另外,別的型別的揹包問題往往也可以轉換成01揹包問題求解。故一定要仔細體會上面基本思路的得出方法,狀態轉移方程的意義,以及最後怎樣優化的空間複雜度。
P02: 完全揹包問題
題目
有N種物品和一個容量為V的揹包,每種物品都有無限件可用。第i種物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
基本思路
這個問題非常類似於01揹包問題,所不同的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已並非取或不取兩種,而是有取0件、取1件、取2件……等很多種。如果仍然按照解01揹包時的思路,令f[i][v]表示前i種物品恰放入一個容量為v的揹包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程,像這樣:f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<= v}。這跟01揹包問題一樣有O(N*V)個狀態需要求解,但求解每個狀態的時間則不是常數了,求解狀態f[i][v]的時間是O(v/c[i]),總的複雜度是超過O(VN)的。
將01揹包問題的基本思路加以改進,得到了這樣一個清晰的方法。這說明01揹包問題的方程的確是很重要,可以推及其它型別的揹包問題。但我們還是試圖改進這個複雜度。
一個簡單有效的優化
完全揹包問題有一個很簡單有效的優化,是這樣的:若兩件物品i、j滿足c[i]<=c[j]且w[i]>=w[j],則將物品j去掉,不用考慮。這個優化的正確性顯然:任何情況下都可將價值小費用高得j換成物美價廉的i,得到至少不會更差的方案。對於隨機生成的資料,這個方法往往會大大減少物品的件數,從而加快速度。然而這個並不能改善最壞情況的複雜度,因為有可能特別設計的資料可以一件物品也去不掉。
轉化為01揹包問題求解
既然01揹包問題是最基本的揹包問題,那麼我們可以考慮把完全揹包問題轉化為01揹包問題來解。最簡單的想法是,考慮到第i種物品最多選V/c [i]件,於是可以把第i種物品轉化為V/c[i]件費用及價值均不變的物品,然後求解這個01揹包問題。這樣完全沒有改進基本思路的時間複雜度,但這畢竟給了我們將完全揹包問題轉化為01揹包問題的思路:將一種物品拆成多件物品。
更高效的轉化方法是:把第i種物品拆成費用為c[i]*2^k、價值為w[i]*2^k的若干件物品,其中k滿足c[i]*2^k<V。這是二進位制的思想,因為不管最優策略選幾件第i種物品,總可以表示成若干個2^k件物品的和。這樣把每種物品拆成O(log(V/c[i]))件物品,是一個很大的改進。但我們有更優的O(VN)的演算法。 * O(VN)的演算法這個演算法使用一維陣列,先看虛擬碼: <pre class"example"> for i=1..N for v=0..V f[v]=max{f[v],f[v-c[i]]+w[i]};
你會發現,這個虛擬碼與P01的虛擬碼只有v的迴圈次序不同而已。為什麼這樣一改就可行呢?首先想想為什麼P01中要按照v=V..0的逆序來迴圈。這是因為要保證第i次迴圈中的狀態f[i][v]是由狀態f[i-1][v-c[i]]遞推而來。換句話說,這正是為了保證每件物品只選一次,保證在考慮“選入第i件物品”這件策略時,依據的是一個絕無已經選入第i件物品的子結果f[i-1][v-c[i]]。而現在完全揹包的特點恰是每種物品可選無限件,所以在考慮“加選一件第i種物品”這種策略時,卻正需要一個可能已選入第i種物品的子結果f[i][v-c[i]],所以就可以並且必須採用v= 0..V的順序迴圈。這就是這個簡單的程式為何成立的道理。
這個演算法也可以以另外的思路得出。例如,基本思路中的狀態轉移方程可以等價地變形成這種形式:f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]},將這個方程用一維陣列實現,便得到了上面的虛擬碼。
總結
完全揹包問題也是一個相當基礎的揹包問題,它有兩個狀態轉移方程,分別在“基本思路”以及“O(VN)的演算法“的小節中給出。希望你能夠對這兩個狀態轉移方程都仔細地體會,不僅記住,也要弄明白它們是怎麼得出來的,最好能夠自己想一種得到這些方程的方法。事實上,對每一道動態規劃題目都思考其方程的意義以及如何得來,是加深對動態規劃的理解、提高動態規劃功力的好方法。
P03: 多重揹包問題
題目
有N種物品和一個容量為V的揹包。第i種物品最多有n[i]件可用,每件費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
基本演算法
這題目和完全揹包問題很類似。基本的方程只需將完全揹包問題的方程略微一改即可,因為對於第i種物品有n[i]+1種策略:取0件,取1件……取 n[i]件。令f[i][v]表示前i種物品恰放入一個容量為v的揹包的最大權值,則:f[i][v]=max{f[i-1][v-k*c[i]]+ k*w[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的最大整數。例如,如果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(log n[i])種物品,將原問題轉化為了複雜度為O(V*∑log n[i])的01揹包問題,是很大的改進。
O(VN)的演算法
多重揹包問題同樣有O(VN)的演算法。這個演算法基於基本演算法的狀態轉移方程,但應用單調佇列的方法使每個狀態的值可以以均攤O(1)的時間求解。由於用單調佇列優化的DP已超出了NOIP的範圍,故本文不再展開講解。我最初瞭解到這個方法是在樓天成的“男人八題”幻燈片上。
小結
這裡我們看到了將一個演算法的複雜度由O(V*∑n[i])改進到O(V*∑log n[i])的過程,還知道了存在應用超出NOIP範圍的知識的O(VN)演算法。希望你特別注意“拆分物品”的思想和方法,自己證明一下它的正確性,並用儘量簡潔的程式來實現。
P04: 混合三種揹包問題