01揹包的空間優化方法

摘自Tianyi Cui童鞋的《揹包問題九講》,稍作修改,方便理解。

01揹包問題描述

已知:有一個容量爲V的揹包和N件物品,第i件物品的重量是weight[i],收益是cost[i]。

限制:每種物品只有一件,可以選擇放或者不放

問題:在不超過揹包容量的情況下,最多能獲得多少價值或收益

相似問題:在恰好裝滿揹包的情況下,最多能獲得多少價值或收益

這裏,我們先討論在不超過揹包容量的情況下,最多能獲得多少價值或收益

基本思路

01揹包的特點:每種物品只有一件,可以選擇放或者不放

子問題定義狀態

f[i][v] : 前i件物品放到一個容量爲v的揹包中可以獲得最大價值

狀態轉移方程

f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + cost[i])

分析

考慮我們的子問題,將前i件物品放到容量爲v的揹包中,若我們只考慮第i件物品時,它有兩種選擇,放或者不放。

1) 如果第i件物品不放入揹包中,那麼問題就轉換爲:將前i - 1件物品放到容量爲v的揹包中,帶來的收益f[i - 1][v]

2) 如果第i件物品能放入揹包中,那麼問題就轉換爲:將前i - 1件物品放到容量爲v - weight[i]的揹包中,帶來的收益f[i - 1][v - weight[i]] + cost[i]

代碼

  1. #include <iostream>
  2. using namespace std;
  3. const int N = 3;//物品個數
  4. const int V = 5;//揹包最大容量
  5. int weight[N + 1] = {0,3,2,2};//物品重量
  6. int value[N + 1] = {0,5,10,20};//物品價值
  7. int f[N + 1][V + 1] = {{0}};
  8. int Max(int x,int y)
  9. {
  10. return x > y ? x : y;
  11. }
  12. /*
  13. 目標:在不超過揹包容量的情況下,最多能獲得多少價值
  14. 子問題狀態:f[i][j]:表示前i件物品放入容量爲j的揹包得到的最大價值
  15. 狀態轉移方程:f[i][j] = max{f[i - 1][j],f[i - 1][j - weight[i]] + value[i]}
  16. 初始化:f數組全設置爲0
  17. */
  18. int Knapsack()
  19. {
  20. //初始化
  21. memset(f,0,sizeof(f));
  22. //遞推
  23. for (int i = 1;i <= N;i++) //枚舉物品
  24. {
  25. for (int j = 0;j <= V;j++) //枚舉揹包容量
  26. {
  27. f[i][j] = f[i - 1][j];
  28. if (j >= weight[i])
  29. {
  30. f[i][j] = Max(f[i - 1][j],f[i - 1][j - weight[i]] + value[i]);
  31. }
  32. }
  33. }
  34. return f[N][V];
  35. }
  36. int main()
  37. {
  38. cout<<Knapsack()<<endl;
  39. system("pause");
  40. return 1;
  41. }

效率分析:

此算法的時間複雜度爲O(N*V),空間複雜度也爲O(N*V)。其中,N 表示物品個數,V 表示揹包容量這裏,時間複雜度不可以在優化了,但是空間複雜度可以繼續優化到O(V)

優化空間複雜度

上述的方法,我們使用二維數組 f[i][v] 保存中間狀態,這裏我們可以使用一維數組f[v]保存中間狀態就能得到結果

分析

我們現在使用f[v]保存中間狀態,我們想要達到的效果是,第i次循環後,f[v]中存儲的是前i個物體放到容量v時的最大價值

在回顧下之前講過的狀態轉移方程:

f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + cost[i])

我們可以看到,要想得到 f[i][v],我們需要知道 f[i - 1][v] 和 f[i - 1][v - weight[i]],由於我們使用二維數組保存中間狀態,所以可以直接取出這兩個狀態。

當我們使用一維數組存儲狀態時,f[v]表示,在執行i次循環後(此時已經處理i個物品),前i個物體放到容量v時的最大價值,即之前的f[i][v]。與二維相比較,它把第一維隱去了,但是二者表達的含義還是相同的,只不過針對不同的i,f[v]一直在重複使用,所以,也會出現第i次循環可能會覆蓋第i - 1次循環的結果。

爲了求f[v],我們需要知道,前i - 1個物品放到容量v的揹包中帶來的收益,即之前的f[i - 1][v]  和 前i - 1件物品放到容量爲v - weight[i]的揹包中帶來的收益,即之前的f[i - 1][v - weight[i]] + cost[i]。

難點:由於我們只使用一維數組存儲,則在求這兩個子問題時就沒有直接取出那麼方便了,因爲,第i次循環可能會覆蓋第i - 1次循環的結果。

現在我們來求這兩個值

1)前i - 1個物品放到容量v的揹包中帶來的收益,即之前的f[i - 1][v] :

由於,在執行在i次循環時,f[v]存儲的是前i個物體放到容量v時的最大價值,在求前i個物體放到容量v時的最大價值(即之前的f[i][v])時,我們是正在執行第 i 次循環,f[ v ]的值還是在第 i - 1  次循環時存下的值,在此時取出的 f[ v ]就是前i - 1個物體放到容量v時的最大價值,即f[i - 1][v]。

2)前i - 1件物品放到容量爲v - weight[i]的揹包中帶來的收益,即之前的f[i - 1][v - weight[i]] + cost[i]

由於,在執行第i次循環前,f[0 ~ V]中保存的是第i - 1次循環的結果,即是前i - 1個物體分別放到容量0 ~ V時的最大價值,即f[i - 1][0 ~ V]。

則,在執行第i次循環前,f 數組中v - weight[i]的位置存儲就是我們要找的 前i - 1件物品放到容量爲v - weight[i]的揹包中帶來的收益 (即之前的f[i - 1][v - weight[i]]),這裏假設物品是從數組下標1開始存儲的。

僞代碼

  1. for i=1..N //枚舉物品
  2. for v=V..0 //枚舉容量,從大到小
  3. f[v]=max{f[v],f[v-weight[i]] + cost[i]};

由上面僞代碼可知,在執行第 i 次循環時,需要把揹包容量由V..0都要遍歷一遍,檢測第 i 件物品是否能放。

逆序枚舉容量的原因:

注意一點,我們是由第 i - 1 次循環的兩個狀態推出 第 i 個狀態的,而且 v  > v - weight[i],則對於第i次循環,揹包容量只有當V..0循環時,纔會先處理揹包容量爲v的狀況,後處理揹包容量爲 v-weight[i] 的情況。

具體來說,由於,在執行v時,還沒執行到v - weight[i]的,因此,f[v - weight[i]]保存的還是第i - 1次循環的結果。即在執行第i次循環 且 揹包容量爲v時,此時的f[v]存儲的是 f[i - 1][v] ,此時f[v-weight[i]]存儲的是f[i - 1][v-weight[i]]。

相反,如果在執行第 i 次循環時,揹包容量按照0..V的順序遍歷一遍,來檢測第 i 件物品是否能放。此時在執行第i次循環 且 揹包容量爲v時,此時的f[v]存儲的是 f[i - 1][v] ,但是,此時f[v-weight[i]]存儲的是f[i][v-weight[i]]。

因爲,v  > v - weight[i],第i次循環中,執行揹包容量爲v時,容量爲v - weight[i]的揹包已經計算過,即f[v - weight[i]]中存儲的是f[i][v - weight[i]]。即,對於01揹包,按照增序枚舉揹包容量是不對的。

代碼

  1. #include <iostream>
  2. using namespace std;
  3. const int N = 3;//物品個數
  4. const int V = 5;//揹包最大容量
  5. int weight[N + 1] = {0,3,2,2};//物品重量
  6. int value[N + 1] = {0,5,10,20};//物品價值
  7. int f[V + 1] = {0};
  8. int Max(int x,int y)
  9. {
  10. return x > y ? x : y;
  11. }
  12. /*
  13. 目標:在不超過揹包容量的情況下,最多能獲得多少價值
  14. 子問題狀態:f[j]:表示前i件物品放入容量爲j的揹包得到的最大價值
  15. 狀態轉移方程:f[j] = max{f[j],f[j - weight[i]] + value[i]}
  16. 初始化:f數組全設置爲0
  17. */
  18. int Knapsack()
  19. {
  20. //初始化
  21. memset(f,0,sizeof(f));
  22. //遞推
  23. for (int i = 1;i <= N;i++) //枚舉物品
  24. {
  25. for (int j = V;j >= weight[i];j--) //枚舉揹包容量,防越界,j下限爲 weight[i]
  26. {
  27. f[j] = Max(f[j],f[j - weight[i]] + value[i]);
  28. }
  29. }
  30. return f[V];
  31. }
  32. int main()
  33. {
  34. cout<<Knapsack()<<endl;
  35. system("pause");
  36. return 1;
  37. }

但是,增序枚舉揹包容量會達到什麼效果:它會重複的裝入某個物品,而且儘可能多的,使價值最大,當然不會不超過揹包容量

而逆序枚舉揹包容量:揹包中的物品至多裝一次使價值最大,當然不會不超過揹包容量

我們首先舉例說明:

逆序枚舉物品

當i = 2,我們要求 f [5]:表示檢測物品2放入容量爲5的揹包的最大收益

上圖表示,當i = 2,求f[5]時f數組的狀況,

橙色爲數組現在存儲的值,這些值是i = 1時(上一次循環)存入數組 f 的。相當於f[i - 1][v]

而黃色使我們要求的值,在求f[5]之前,f[5]= 5,即f[i - 1][5] = 5

現在要求 i = 2 時的f[5] = f[5 - 2] + 10 = 5 + 10 = 15  >  f[i - 1][5] = 5

故,f[5] = 15;

注意一點,在求f[v]時,它引用的 f[v - weight[i]] 和 f[v]都是上一次循環的結果

順序枚舉物品

當i = 2,我們要求 f [5]:表示檢測物品2放入容量爲5的揹包的最大收益

上圖表示,當i = 2,求f[5]時f數組的狀況,

橙色爲數組現在存儲的值,這些值是i = 2時(本次循環)存入數組 f 的。相當於f[i][v]

這是由於,我們是增序遍歷數組f的,在求f[v]時,v之前的值(0 ~ v - 1)都已經在第i次循環中求出。

而黃色使我們要求的值,在求f[5]之前,f[5]= 5,即f[i - 1][5] = 5

現在要求 i = 2 時的f[5] = f[5 - 2] + 10 =10+ 10 = 20 >  f[i - 1][5] = 5

故,f[5] = 20;

其中引用的f[3]是相當於f[i][3] 而不是正常的f[i - 1][3]

注意一點,在求f[v]時,它引用的 f[v - weight[i]]是本次循環的結果 而f[v]是上一次循環的結果

換個角度說,

在檢測 揹包容量爲5時,看物品2是否加入

由狀態轉移方程可知,我們f[5]需要引用自己本身和f[3]

由於揹包容量爲3時,可以裝入物品2,且收益比之前的大,所以放入揹包了。

在檢測f[5]時,肯定要加上物品2的收益,而f[5]在引用f[3]時,f[3]時已經加過一次物品2,

因此,在枚舉揹包容量時,物品2會加入多次。

進一步說:

我們觀察一維狀態轉移方程:

f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + cost[i])

首先我們明確三個問題

1) v - weight[i] < v

2) 狀態f[i][v] 是由 f[i - 1][v] 和 f[i - 1][v - weight[i]] 兩個狀態決定

3) 對於物品i,我們在枚舉揹包容量時,只要揹包容量能裝下物品i 且 收益比原來的大,就會成功放入物品i。

具體來說,枚舉揹包容量時,是以遞增的順序的話,由於 v - weight[i] < v,則會先計算 v - weight[i]。在揹包容量爲v - weight[i]時,一旦裝入了物品i,由於求f[v]需要使用f[i - 1][v - weight[i]],而若求f[v]時也可以裝入物品i的話,那麼在揹包容量爲v時,容量爲v的揹包就裝入可兩次物品。又若v - weight[i]是由之前的狀態推出,它們也成功裝入物品i的話,那麼容量爲v的揹包就裝入了多次物品i了。

注意,此時,在計算f[v]時,已經把物品i能裝入的全裝入容量爲v的揹包了,此時裝入物品i的次數爲最大啊

其實,順序枚舉容量是完全揹包問題最簡捷的解決方案。

 

初始化的細節問題

求最優解的揹包問題時,有兩種問法:

1)在不超過揹包容量的情況下,最多能獲得多少價值

2)在恰好裝滿揹包的情況下,最多能獲得多少價值

主要的區別爲是否要求恰好裝滿揹包。但這兩種問法的實現方法是在初始化的時候有所不同。

1)恰好裝滿揹包的情況:使用二維數組f[i][v]存儲中間狀態,其中第一維表示物品,第二維表示揹包容量

初始化時,除了f[i][0] = 0(第一列)外,其他全爲負無窮。

原因:初始化 f 數組就是表示:在沒有任何物品可以放入揹包時的合法狀態。對於恰好裝滿揹包,只有揹包容量爲 0(第一列),可以什麼物品都不裝就能裝滿,這種情況是合法情況,此時價值爲0。其他f[0][v](第一列)是都不能裝滿的,此時有容量沒物品。而其他位置(除去第一行和第一列的位置),我們爲了在計算中比較最大值,也要初始化爲負無窮。我們從程序的角度上看,我們只允許裝入揹包物品的序列的起始位置是從第一列開始,這些起始位置都是合法位置,且能恰好裝滿的情況收益均爲正值,到f[N][V]終止。

注意,我們雖然是求恰好裝滿,還是需要枚舉所有可以裝入揹包的物品,只要能裝入,還需裝入,收益有增加。只不過,由於恰好裝滿的物品的序列肯定是從第一列某行開始的,且之後的收益肯定是正值。對於非恰好裝滿的物品序列,其實位置肯定是從第一行某位置開始的,由於此時被初始化爲負無窮,在和那些恰好裝滿物品序列帶來的價值時,肯定是小的。所以,我們最後能獲得最大值。

代碼:

  1. #include <iostream>
  2. using namespace std;
  3. const int MinNum = 0x80000000;
  4. const int N = 3;//物品個數
  5. const int V = 5;//揹包最大容量
  6. int weight[N + 1] = {0,3,2,2};//物品重量
  7. int value[N + 1] = {0,5,10,20};//物品價值
  8. int f[N + 1][V + 1] = {{0}};
  9. int Max(int x,int y)
  10. {
  11. return x > y ? x : y;
  12. }
  13. /*
  14. 目標:在恰好裝滿揹包的情況下,最多能獲得多少價值
  15. 子問題狀態:f[i][j]:表示前i件物品放入容量爲j的揹包得到的最大價值
  16. 狀態轉移方程:f[i][j] = max{f[i - 1][j],f[i - 1][j - weight[i]] + value[i]}
  17. 初始化:f數組全設置爲0
  18. */
  19. int Knapsack()
  20. {
  21. //初始化
  22. for (int i = 0;i <= N;i++) //枚舉物品
  23. {
  24. for (int j = 0;j <= V;j++) //枚舉揹包容量
  25. {
  26. f[i][j] = MinNum;
  27. }
  28. }
  29. for (int i = 0;i <= N;i++)
  30. {
  31. f[i][0] = 0;//揹包容量爲0時爲合法狀態
  32. }
  33. //遞推
  34. for (int i = 1;i <= N;i++) //枚舉物品
  35. {
  36. for (int j = 1;j <= V;j++) //枚舉揹包容量
  37. {
  38. f[i][j] = f[i - 1][j];
  39. if (j >= weight[i])
  40. {
  41. f[i][j] = Max(f[i - 1][j],f[i - 1][j - weight[i]] + value[i]);
  42. }
  43. }
  44. }
  45. return f[N][V];
  46. }
  47. int main()
  48. {
  49. cout<<Knapsack()<<endl;//輸出25
  50. system("pause");
  51. return 1;
  52. }

使用一維數組f[v]存儲中間狀態,維表示揹包容量

初始化時,除了f[0] = 0,其他全爲負無窮。

原因:只有容量爲0 的揹包可以什麼物品都不裝就能裝滿,此時價值爲0。其它容量的揹包均沒有合法的解,屬於未定義的狀態,應該被賦值爲負無窮了

代碼

  1. #include <iostream>
  2. using namespace std;
  3. const int MinNum = 0x80000000;//int最小的數
  4. const int N = 3;//物品個數
  5. const int V = 5;//揹包最大容量
  6. int weight[N + 1] = {0,3,2,2};//物品重量
  7. int value[N + 1] = {0,5,10,20};//物品價值
  8. int f[V + 1] = {0};
  9. int Max(int x,int y)
  10. {
  11. return x > y ? x : y;
  12. }
  13. /*
  14. 目標:在恰好裝滿揹包容量的情況下,最多能獲得多少價值
  15. 子問題狀態:f[j]:表示前i件物品放入容量爲j的揹包得到的最大價值
  16. 狀態轉移方程:f[j] = max{f[j],f[j - weight[i]] + value[i]}
  17. 初始化:f數組全設置爲0
  18. */
  19. int Knapsack()
  20. {
  21. //初始化
  22. for (int i = 0;i <= V;i++)
  23. {
  24. f[i] = MinNum;
  25. }
  26. f[0] = 0;//只有揹包容量爲0時纔是合法狀態,由合法狀態組成的結果纔是合法的
  27. //遞推
  28. for (int i = 1;i <= N;i++) //枚舉物品
  29. {
  30. for (int j = V;j >= weight[i];j--) //枚舉揹包容量,防越界,j下限爲 weight[i]
  31. {
  32. f[j] = Max(f[j],f[j - weight[i]] + value[i]);
  33. }
  34. }
  35. return f[V];
  36. }
  37. int main()
  38. {
  39. cout<<Knapsack()<<endl;//輸出25
  40. system("pause");
  41. return 1;
  42. }

2)不需要把揹包裝滿,只需要收益最大

使用二維數組f[i][v]存儲中間狀態,其中第一維表示物品,第二維表示揹包容量

初始化時,除了f[i][0] = 0(第一列)外,其他全爲負無窮。

使用一維數組f[v]存儲中間狀態,維表示揹包容量

原因:如果揹包並非必須被裝滿,那麼任何容量的揹包都有一個合法解“什麼都不裝”,這個解的價值爲0,所以初始時狀態的值也就全部爲0了。

代碼在最前面已貼,不在此上傳。

一個常數優化

一維數組描述狀態時的僞代碼:

  1. for i=1..N //枚舉物品
  2. for v=V..0 //枚舉容量,從大到小
  3. f[v]=max{f[v],f[v-weight[i]] + cost[i]};

觀察可知,對於第i個物品,枚舉揹包容量下限時,可以到weight[i]爲止。

原因:

1)對於第i物品,在求f[v]時,需要使用的狀態是 v ~ v -  weight[i] 這麼多,這是由於v取最大容量V時,使用的狀態纔是v - weight[i],當v不取最大狀態時,使用的狀態肯定是在v ~ v -  weight[i]之間的。可以到weight[i]爲止。

2)在到weight[i]爲止時,還可以不進行if判斷,擔心v -  weight[i]是否越界

此時,僞代碼爲

  1. for i=1..N //枚舉物品
  2. for v=V..weight[i] //枚舉容量,從大到小
  3. f[v]=max{f[v],f[v-weight[i]] + cost[i]};

注意,對 f 數組,如果是檢測第i個物品是否能放入,0 ~ weight[i] - 1的這些位置是不會遍歷到的,則此時他們仍表示第i - 1次的狀態,即二維的f[i - 1][v]。

還可以繼續優化下界爲

  1. for i=1..N //枚舉物品
  2. bound=max{V-sum{weight[i..n]},weight[i]}//確定需要枚舉容量的下界
  3. for v=V..bound
  4. f[v]=max{f[v],f[v-weight[i]] + cost[i]};

原因:

1)網上的說法,不太懂,各位大牛可以指導下下。

對於第i次循環(指外循環),對於揹包容量v = V(最大)時,對於f[v]的值,其實只要知道f[v-weight[i]]即可。以此類推,對於揹包容量爲 j 時,我們只需要知道到f[v-sum{weight[j..n]}]即可

2)還有人說

如果比v-sum{weight[j..n]}這個小,那麼即使後面物品的全要也裝不滿揹包。

所以對於物品i,小於v-sum{weight[j..n]}的v值,無意義。

總之是不懂。智商啊

作者說,當V很大是,效果好。

代碼

  1. #include <iostream>
  2. using namespace std;
  3. const int N = 3;//物品個數
  4. const int V = 5;//揹包最大容量
  5. int weight[N + 1] = {0,3,2,2};//物品重量
  6. int value[N + 1] = {0,5,10,20};//物品價值
  7. int f[V + 1] = {0};
  8. int Max(int x,int y)
  9. {
  10. return x > y ? x : y;
  11. }
  12. /*
  13. 目標:在不超過揹包容量的情況下,最多能獲得多少價值
  14. 子問題狀態:f[j]:表示前i件物品放入容量爲j的揹包得到的最大價值
  15. 狀態轉移方程:f[j] = max{f[j],f[j - weight[i]] + value[i]}
  16. 初始化:f數組全設置爲0
  17. */
  18. int Knapsack()
  19. {
  20. int sum = 0;//存儲還未處理物品的總容量
  21. int bound = 0;
  22. //初始化
  23. memset(f,0,sizeof(f));
  24. for (int i = 1;i <= N;i++)
  25. {
  26. sum += weight[i];
  27. }
  28. //遞推
  29. for (int i = 1;i <= N;i++) //枚舉物品
  30. {
  31. //設置下界
  32. if (i != 1)
  33. {
  34. sum -= weight[i - 1];
  35. }
  36. bound = Max(V - sum,weight[i]);
  37. for (int j = V;j >= bound;j--) //枚舉揹包容量
  38. {
  39. if (f[j] < f[j - weight[i]] + value[i])
  40. {
  41. f[j] = f[j - weight[i]] + value[i];
  42. }
  43. }
  44. }
  45. return f[V];
  46. }
  47. int main()
  48. {
  49. cout<<Knapsack()<<endl;
  50. system("pause");
  51. return 1;
  52. }

 輸出方案

一般而言,揹包問題是要求一個最優值,如果要求輸出這個最優值的方案,可以參照一般動態規劃問題輸出方案的方法:記錄下每個狀態的最優值是由狀態轉移方程的哪一項推出來的,換句話說,記錄下它是由哪一個策略推出來的。便可根據這條策略找到上一個狀態,從上一個狀態接着向前推即可。

這裏我們首先給出01揹包的二維狀態轉移方程

f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + cost[i])

對於狀態f[i][v],它來自兩種策略,可以是f[i - 1][v],也可以是f[i - 1][v - weight[i]] + cost[i]

其中,對於第二種情況,就是把物品i放入揹包了,這裏也是我們要找的情況

根據狀態轉移方程,我們可以給出兩種實現方法

1) 藉助存儲狀態的數組,直接根據狀態轉移方程倒着推,檢測是否滿足

f[i][v] == f[i - 1][v - weight[i]] + value[i]

如果滿足,則把第i件物品放入了,此時我們要檢測第i - 1件物品,揹包容量爲v - weight[i]

不滿足則表示沒有把第i件物品放入,直接檢測第i - 1件物品,此時揹包容量還是v

注意,這種方法只適用於存儲狀態數組不壓縮的情況。壓縮數組由於數據有覆蓋,不能使用

代碼

  1. #include <iostream>
  2. using namespace std;
  3. const int N = 3;//物品個數
  4. const int V = 5;//揹包最大容量
  5. int weight[N + 1] = {0,3,2,2};//物品重量
  6. int value[N + 1] = {0,5,10,20};//物品價值
  7. int f[N + 1][V + 1] = {{0}};
  8. int Max(int x,int y)
  9. {
  10. return x > y ? x : y;
  11. }
  12. /*
  13. 目標:在不超過揹包容量的情況下,最多能獲得多少價值
  14. 子問題狀態:f[i][j]:表示前i件物品放入容量爲j的揹包得到的最大價值
  15. 狀態轉移方程:f[i][j] = max{f[i - 1][j],f[i - 1][j - weight[i]] + value[i]}
  16. 初始化:f數組全設置爲0
  17. */
  18. int Knapsack()
  19. {
  20. //初始化
  21. memset(f,0,sizeof(f));
  22. //遞推
  23. for (int i = 1;i <= N;i++) //枚舉物品
  24. {
  25. for (int j = 1;j <= V;j++) //枚舉揹包容量
  26. {
  27. f[i][j] = f[i - 1][j];
  28. if (j >= weight[i])
  29. {
  30. f[i][j] = Max(f[i - 1][j],f[i - 1][j - weight[i]] + value[i]);
  31. }
  32. }
  33. }
  34. return f[N][V];
  35. }
  36. /*
  37. 輸出順序:逆序輸出物品編號
  38. 注意:這裏藉助狀態數組f[i][v]
  39. 使用狀態轉移方程:f[i][j] = max{f[i - 1][j],f[i - 1][j - weight[i]] + value[i]}
  40. */
  41. void PrintKnapsack()
  42. {
  43. int i = N;//枚舉物品
  44. int j = V;//枚舉空間
  45. cout<<"加入揹包的物品編號:"<<endl;
  46. while(i)
  47. {
  48. if (f[i][j] == f[i - 1][j - weight[i]] + value[i])
  49. {
  50. /*if不滿足,表示第i件物品沒裝入揹包,
  51. if條件滿足,表示放入揹包了*/
  52. cout<<i<<" ";
  53. j -= weight[i];//此時容量減少
  54. }
  55. i--;
  56. }
  57. cout<<endl;
  58. }
  59. /*
  60. 輸出順序:順序輸出物品編號
  61. 注意:這裏藉助狀態數組f[i][v]
  62. 使用狀態轉移方程:f[i][j] = max{f[i - 1][j],f[i - 1][j - weight[i]] + value[i]}
  63. */
  64. void PrintKnapsack_recursion(int i,int j)
  65. {
  66. if (i == 0 || j == 0)
  67. {
  68. return;
  69. }
  70. if (f[i][j] == f[i - 1][j - weight[i]] + value[i])
  71. {
  72. PrintKnapsack_recursion(i - 1,j - weight[i]);
  73. cout<<i<<" ";
  74. }
  75. }
  76. int main()
  77. {
  78. cout<<Knapsack()<<endl;
  79. PrintKnapsack();
  80. PrintKnapsack_recursion(N,V);
  81. system("pause");
  82. return 1;
  83. }

2) 另外開闢數組,在求解最大收益時做標記位。求解完最大收益後,根據這個新數組倒着推結果

思想:對於現在這個狀態的位置,它存儲的是該狀態上一位置

注意:這種方法均適用存儲狀態數組不壓縮 和 壓縮兩種情況

代碼:

  1. #include <iostream>
  2. using namespace std;
  3. const int N = 3;//物品個數
  4. const int V = 5;//揹包最大容量
  5. int weight[N + 1] = {0,3,2,2};//物品重量
  6. int value[N + 1] = {0,5,10,20};//物品價值
  7. int f[V + 1] = {0};
  8. int G[N + 1][V + 1] = {{0}};//求揹包序列
  9. int Max(int x,int y)
  10. {
  11. return x > y ? x : y;
  12. }
  13. /*
  14. 目標:在不超過揹包容量的情況下,最多能獲得多少價值
  15. 子問題狀態:f[j]:表示前i件物品放入容量爲j的揹包得到的最大價值
  16. 狀態轉移方程:f[j] = max{f[j],f[j - weight[i]] + value[i]}
  17. 初始化:f數組全設置爲0
  18. */
  19. int Knapsack()
  20. {
  21. //初始化
  22. memset(f,0,sizeof(f));
  23. memset(G,0,sizeof(G));
  24. //遞推
  25. for (int i = 1;i <= N;i++) //枚舉物品
  26. {
  27. for (int j = V;j >= weight[i];j--) //枚舉揹包容量
  28. {
  29. if (f[j] < f[j - weight[i]] + value[i])
  30. {
  31. f[j] = f[j - weight[i]] + value[i];
  32. G[i][j] = 1;
  33. }
  34. }
  35. }
  36. return f[V];
  37. }
  38. /*
  39. 輸出順序:逆序輸出物品編號
  40. 注意:這裏另外開闢數組G[i][v],標記上一個狀態的位置
  41. G[i][v] = 1:表示物品i放入揹包了,上一狀態爲G[i - 1][v - weight[i]]
  42. G[i][v] = 0:表示物品i沒有放入揹包,上一狀態爲G[i - 1][v]
  43. */
  44. void PrintKnapsack()
  45. {
  46. int i = N;//枚舉物品
  47. int j = V;//枚舉空間
  48. cout<<"加入揹包的物品編號:"<<endl;
  49. while(i)
  50. {
  51. if (G[i][j] == 1)
  52. {
  53. /*if不滿足,表示第i件物品沒裝入揹包,
  54. if條件滿足,表示放入揹包了*/
  55. cout<<i<<" ";
  56. j -= weight[i];//此時容量減少
  57. }
  58. i--;
  59. }
  60. cout<<endl;
  61. }
  62. /*
  63. 輸出順序:順序輸出物品編號
  64. 注意:這裏另外開闢數組G[i][v],標記上一個狀態的位置
  65. G[i][v] = 1:表示物品i放入揹包了,上一狀態爲G[i - 1][v - weight[i]]
  66. G[i][v] = 0:表示物品i沒有放入揹包,上一狀態爲G[i - 1][v]
  67. */
  68. void PrintKnapsack_recursion(int i,int j)
  69. {
  70. if (i == 0 || j == 0)
  71. {
  72. return;
  73. }
  74. if (G[i][j] == 1)
  75. {
  76. PrintKnapsack_recursion(i - 1,j - weight[i]);
  77. cout<<i<<" ";
  78. }
  79. }
  80. int main()
  81. {
  82. cout<<Knapsack()<<endl;
  83. PrintKnapsack();
  84. PrintKnapsack_recursion(N,V);
  85. system("pause");
  86. return 1;
  87. }

小結:

01 揹包問題是最基本的揹包問題,它包含了揹包問題中設計狀態、方程的最基本思想。另外,別的類型的揹包問題往往也可以轉換成01 揹包問題求解。故一定要仔細體會上面基本思路的得出方法,狀態轉移方程的意義,以及空間複雜度怎樣被優化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章