01背包是在M件物品取出若干件放在空間為W的背包里,每件物品的體積為C1,C2,…,Cn,與之相對應的價值為W1,W2,…,Wn.求解將那些物品裝入背包可使總價值最大。
動態規划(DP):
1) 子問題定義:F[i][j]表示前i件物品中選取若干件物品放入剩余空間為j的背包中所能得到的最大價值。
2) 根據第i件物品放或不放進行決策
(1-1)
其中F[i-1][j]表示前i-1件物品中選取若干件物品放入剩余空間為j的背包中所能得到的最大價值;
而F[i-1][j-C[i]]+W[i]表示前i-1件物品中選取若干件物品放入剩余空間為j-C[i]的背包中所能取得的最大價值加上第i件物品的價值。
根據第i件物品放或是不放確定遍歷到第i件物品時的狀態F[i][j]。
設物品件數為N,背包容量為V,第i件物品體積為C[i],第i件物品價值為W[i]。
由此寫出偽代碼如下:
- F[0][] ← {0}
- F[][0] ← {0}
- for i←1 to N
- do for k←1 to V
- F[i][k] ← F[i-1][k]
- if(k >= C[i])
- then F[i][k] ← max(F[i][k],F[i-1][k-C[i]]+W[i])
- return F[N][V]
以上偽代碼數組均為基於1索引,及第一件物品索引為1。時間及空間復雜度均為O(VN)
舉例:表1-1為一個背包問題數據表,設背包容量為10根據上述解決方法可得到對應的F[i][j]如表1-2所示,最大價值即為F[6][10].
表1-1背包問題數據表
| 物品號i | 1 | 2 | 3 | 4 | 5 | 6 |
| 體積C | 2 | 3 | 1 | 4 | 6 | 5 |
| 價值W | 5 | 6 | 5 | 1 | 19 | 7 |
表1-2前i件物品選若干件放入空間為j的背包中得到的最大價值表
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
| 2 | 0 | 5 | 6 | 6 | 11 | 11 | 11 | 11 | 11 | 11 | 11 |
| 3 | 0 | 5 | 5 | 10 | 11 | 11 | 16 | 16 | 16 | 16 | 16 |
| 4 | 0 | 5 | 5 | 10 | 11 | 11 | 16 | 16 | 16 | 16 | 17 |
| 5 | 0 | 5 | 5 | 10 | 11 | 11 | 19 | 24 | 24 | 29 | 30 |
| 6 | 0 | 5 | 5 | 10 | 11 | 11 | 19 | 24 | 24 | 29 | 30 |
很多文章講背包問題時只是把最大價值求出來了,並沒有把所選的是哪些物品找出來。本人在學習背包問題之前遇到過很多的類似問題,當時也是只求得了最大價值或最大和,對具體哪些物品或路徑等細節也束手無策。再次和大家一起分享細節的求法。
根據算法求出的最大價值表本身其實含有位置信息,從F[N][V]逆着走向F[0][0],設i=N,j=V,如果F[i][j]==F[i-1][j-C[i]]+W[i]說明包里面有第i件物品,同時j -= C[i],不管F[i][j]與F[i-1][j-C[i]]+W[i]相不相等i都要減1,因為01背包的第i件物品要么放要么不放,不管放還是不放其已經遍歷過了,需要繼續往下遍歷。
打印背包內物品的偽代碼如下:
- i←N
- j←V
- while(i>0 && j>0)
- do if(F[i][j]=F[i-1][j-C[i]]+W[i])
- then Print W[i]
- j←j-C[i]
- i←i-1
當然也可以定義一個二維數組Path[N][V]來存放背包內物品信息,開始時Path[N][V]初始化為0,當 F[i][j]==F[i-1][j-C[i]]+W[i]時Path[i][j]置1。最后通過從Path[N+1][V+1]逆着走向Path[0][0]來獲取背包內物品。其中Path[0][]與Path[][0]為邊界。
加入路徑信息的偽代碼如下:
- F[0][] ← {0}
- F[][0] ← {0}
- Path[][] ← 0
- for i←1 to N
- do for k←1 to V
- F[i][k] ← F[i-1][k]
- if(k >= C[i] && F[i][k] < F[i-1][k-C[i]]+W[i])
- then F[i][k] ← F[i-1][k-C[i]]+W[i]
- Path[i][k] ← 1
- return F[N][V] and Path[][]
打印背包內物品的偽代碼如下:
- i←N
- j←V
- while(i>0 && j>0)
- do if(Path[i][j] = 1)
- then Print W[i]
- j←j-C[i]
- i←i-1
在時間及空間復雜度均為O(NV)的情況下,利用Path[][]的方法明顯比直接通過F[i][j]==F[i-1][j-C[i]]+W[i]來打印物品耗費空間,Path[][]需要額外的空間O(NV)但總空間復雜度不變仍為O(NV)。但下面要講到的O(V)的空間復雜度的方法卻不能利用關系式F [j]==F [j-C[i]]+W[i]而只能利用Path[][]進行標記.
接下來考慮如何壓縮空間,以降低空間復雜度。
時間復雜度為O(VN),空間復雜度將為O(V)
觀察偽代碼可也發現,F[i][j]只與F[i-1][j]和F[i-1][j-C[i]]有關,即只和i-1時刻狀態有關,所以我們只需要用一維數組F[]來保存i-1時的狀態F[]。假設i-1時刻的F[]為{a0,a1,a2,…,av},難么i時刻的F[]中第k個應該為max(ak,ak-C[i]+W[i])即max(F[k],F[k-C[i]]+W[i]),這就需要我們遍歷V時逆序遍歷,這樣才能保證求i時刻F[k]時F[k-C[i]]是i-1時刻的值。如果正序遍歷則當求F[k]時其前面的F[0],F[1],…,F[K-1]都已經改變過,里面存的都不是i-1時刻的值,這樣求F[k]時利用F[K-C[i]]必定是錯的值。最后F[V]即為最大價值。
求F[j]的狀態方程如下:
(1-2)
偽代碼如下:
- F[] ← {0}
- for i ← 1 to N
- do for k ← V to C[i]
- F[k] ← max(F[k],F[k-C[i]]+W[i])
- return F[V]
同樣,怎么求路徑?
利用前面講到的Path[][]標記,需空間消耗O(NV)。這里不能用F [j]==F [j-C[i]]+W[i]來判斷是因為一維數組並不能提供足夠的信息來尋找二維路徑。
加入路徑信息的偽代碼如下:
- F[] ← {0}
- Path[][]←0
- for i←1 to N
- do for k←V to C[i]
- if(F[k] < F[k-C[i]]+W[i])
- then F[k] ← F[k-C[i]]+W[i]
- Path[i][k] ← 1
- return F[V] and Path[][]
打印路徑的偽代碼和前面未壓縮空間復雜度時的偽代碼一樣,這里不再重寫
這里給出第二種思路
時間復雜度O(VN),不考慮路徑空間復雜度為O(V),考慮路徑空間復雜度為O(VN)
#include<iostream>
using namespace std;
#define Size 1111
int dp[Size];
int Path[Size][Size];
int Max(int x,int y)
{
return x>y?x:y;
}
int Package01_Compress(int Weight[], int Value[], int goodsN, int maxWeight){
int i,j;
memset(dp,0,sizeof(dp));
memset(Path,0,sizeof(Path));
for(i=1;i<=goodsN;i++)
for(j=maxWeight;j>=Weight[i];j--){
/* -----只求最大值----- */
/* dp[j]=Max( dp[j] , dp[j-Weight[i]] +Value[i] );*/
/*------求最大值和路徑------*/
if(dp[j] < dp[j-Weight[i]]+Value[i])
{
dp[j] = dp[j-Weight[i]]+Value[i];
Path[i][j] = 1;
}
/*--------------------------*/
}
return dp[maxWeight];
}
int main()
{
int va[Size],vm[Size];
int t,n,m;
int i;
cin>>t;
while(t--)
{
cin>>n>>m; //n為個數,m為最大載重量
for(i=1;i<=n;i++)
cin>>va[i];
for(i=1;i<=n;i++)
cin>>vm[i];
int myWhats=Package01_Compress(vm,va, n, m);
printf("%d\n",myWhats);
printf("-----取的重量如下(不超過%d):\n",m);
int i2 = n, j2 = m;
while(i2 > 0 && j2 > 0)
{
if(Path[i2][j2] == 1)
{
cout << vm[i2] << " ";
j2 -= vm[i2];
}
i2--;
}
cout << endl;
}
return 0;
}
本文部分內容參考“背包九講”
和 這里
版權聲明:本文為博主原創文章,未經博主允許不得轉載。
