最近看揹包九講,對於我們這種小白來說需要仔細研讀,由於裡面有些思維跳躍,故在原文基礎上加上自己的理解。
有n件物品和乙個容量為v的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使價值總和最大。
這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。
用子問題定義狀態:即f[i][v]表示前i件物品恰放入乙個容量為v的揹包可以獲得的最大價值。則其狀態轉移方程便是:
f[i][v]=max
這個方程非常重要,基本上所有跟揹包相關的問題的方程都是由它衍生出來的。所以有必要將它詳細解釋一下:「將前i件物品放入容量為v的揹包中」這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化為乙個只牽扯前i-1件物品的問題。如果不放第i件物品,那麼問題就轉化為「前i-1件物 品放入容量為v的揹包中」,價值為f[i-1][v];如果放第i件物品,那麼問題就轉化為「前i-1件物品放入剩下的容量為v-c[i]的揹包中」,此時能獲得的最大價值就是f[i-1][v-c[i]]再加上通過放入第i件物品獲得的價值w[i]。
以上方法的時間和空間複雜度均為o(vn),其中時間複雜度應該已經不能再優化了,但空間複雜度卻可以優化到o。
先考慮上面講的基本思路如何實現,肯定是有乙個主迴圈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]=max一句恰就相當於我們的轉移方程
f[i][v]=max
,因為現在的f[v-c[i]]就相當於原來的f[i-1][v-c[i]]。如果將v的迴圈順序從上面的逆序改成順序的話,那麼則成了f[i][v]由f[i][v-c[i]]推知,與本題意不符,但它卻是另乙個重要的揹包問題p02最簡捷的解決方案,故學習只用一維陣列解01揹包問題是十分必要的。(為什麼要逆序呢?當逆序時,從i=1開始舉例:在外迴圈計算完i=1後,對應於i=1時的所有f[0...v]都已經有乙個值,那麼在外迴圈進入i=2時,最大的f[v]=max,這個時候用到的f[v-c[2]]不就是在上乙個迴圈i=1時得到的f[v-c[i]]麼,也即為f[i-1][v-c[i]],所以逆序才能對應上二維時候公式。也就是:在執行v時,還沒執行到v-c[i]的,因此,f[v-c[i]]儲存的還是第i-1次迴圈的結果。而如果是順序的話,那麼在v從0到v的過程中,會把f[0...v]逐漸的更新,便是f[i][v-c[i]]了,而不是f[i-1][v-c[i]],所以這裡要逆序。)
事實上,使用一維陣列解01揹包的程式在後面會被多次用到,所以這裡抽象出乙個處理一件01揹包中的物品過程,以後的**中直接呼叫不加說明。
過程zeroonepack,表示處理一件01揹包中的物品,兩個引數cost、weight分別表明這件物品的費用和價值。
procedure zeroonepack(cost,weight)
for v=v..cost
f[v]=max注意這個過程裡的處理與前面給出的偽**有所不同。前面的示例程式寫成v=v..0是為了在程式中體現每個狀態都按照方程求解了,避免不必要的思維複雜度。而這裡既然已經抽象成看作黑箱的過程了,就可以加入優化。費用為cost的物品不會影響狀態f[0..cost-1],這是顯然的。
有了這個過程以後,01揹包問題的偽**就可以這樣寫:
for i=1..n
zeroonepack(c[i],w[i]);我們看到的求最優解的揹包問題題目中,事實上有兩種不太相同的問法。有的題目要求「恰好裝滿揹包」時的最優解,有的題目則並沒有要求必須把揹包裝滿。一種區別這兩種問法的實現方法是在初始化的時候有所不同。
如果是第一種問法,要求恰好裝滿揹包,那麼在初始化時除了f[0]為0其它f[1..v]均設為-∞,這樣就可以保證最終得到的f[n]是一種恰好裝滿揹包的最優解。
如果並沒有要求必須把揹包裝滿,而是只希望**盡量大,初始化時應該將f[0..v]全部設為0。
為什麼呢?可以這樣理解:初始化的f陣列事實上就是在沒有任何物品可以放入揹包時的合法狀態。如果要求揹包恰好裝滿,那麼此時只有容量為0的揹包可能被價值為0的nothing「恰好裝滿」,其它容量的揹包均沒有合法的解,屬於未定義的狀態,它們的值就都應該是-∞了。如果揹包並非必須被裝滿,那麼 任何容量的揹包都有乙個合法解「什麼都不裝」,這個解的價值為0,所以初始時狀態的值也就全部為0了。
這個小技巧完全可以推廣到其它型別的揹包問題,後面也就不再對進行狀態轉移之前的初始化進行講解。
前面的偽**中有for v=v..1,可以將這個迴圈的下限進行改進。
由於只需要最後n對應的f[n]的值,所以在前乙個物品n-1的f[0...v]中,其實只要知道f[v-c[n]]即可;對應的求n-1的f[v-c[n]]時,又只需要知道n-2時的f[v-c[n]-c[n-1]]即可。以此類推,對以第j個揹包,其實只需要知道到f[v-sum]即可,即**中的
for i=1..n
for v=v..0可以改成
for i=1..n
bound=max,c[i]}
for v=v..bound這對於v比較大時是有用的。
另附hdu 2602題bone collector,是乙個純正的01揹包問題。**如下:
1 #include 2 #include 3 #include 45int value[1005];6
int volume[1005];7
int dp[1005];8
9int max(int a,int
b)10
1314
intmain()
1528
for(i=1; i<=n; i++)
2932
33for(i=1; i<=n; i++)
3439
}40 printf("
%d\n
",dp[v]);41}
42return0;
43 }
此**沒有進行深入優化,顯然根據揹包九講中的優化方法還可以進一步的優化迴圈步數。優化**主要是對逆序時候的下限進行運算。
改進後的**如下方所示。揹包九講中說在v很大的時候這種優化比較有用,不過經過親身在hdu上面測試,發現題目ac的執行時間及記憶體占用都比優化前要多。所以這種優化還是得視具體情況而定。
1for(i=1; i<=n; i++)
29 bound = max(volume[i],v-temp);
10for(j=v; j>=bound; j--)
1114 }
揹包九講之 01揹包
01揹包是最基礎的揹包問題,其中01代表的就是第i個物品的選或不選,在此先設v i 為體積,w i 為價值。很顯然,我們可以使用二位陣列dp i j 來表示前i個物品在揹包容量為j的時候可存放的最大價值。首先dp 0 0 0是很顯然的。而計算dp i j 時,存在01兩種情況 選或不選第i件物品。1...
DP 揹包九講之01揹包
有n件物品和乙個容量為v 的揹包。放入第i件物品耗費的空間是ci,得到 的價值是wi。求解將哪些物品裝入揹包可使價值總和最大。這是最基礎的揹包問題,特點是 每種物品僅有一件,可以選擇放或不 放。用子問題定義狀態 即f i,v 表示前i件物品恰放入乙個容量為v的揹包可以 獲得的最大價值。則其狀態轉移方...
DP 揹包九講之01揹包
有 n 件物品和乙個容量是 v 的揹包。每件物品只能使用一次。第 i 件物品的體積是 vi,價值是 wi。求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大。輸出最大價值。輸入格式 第一行兩個整數,n,v,用空格隔開,分別表示物品數量和揹包容積。接下來有 n 行,每行兩個整數 ...