背包問題泛指以下這一種問題:
給定一組有固定價值和固定重量的物品,以及一個已知最大承重量的背包,求在不超過背包最大承重量的前提下,能放進背包里面的物品的最大總價值。
這一類問題是典型的使用動態規划解決的問題,我們可以把背包問題分成3種不同的子問題:0-1背包問題、完全背包和多重背包問題。下面對這三種問題分別進行討論。
1.0-1背包問題
0-1背包問題是指每一種物品都只有一件,可以選擇放或者不放。現在假設有n件物品,背包承重為m。
對於這種問題,我們可以采用一個二維數組去解決:f[i][j],其中i代表加入背包的是前i件物品,j表示背包的承重,f[i][j]表示當前狀態下能放進背包里面的物品的最大總價值。那么,f[n][m]就是我們的最終結果了。
采用動態規划,必須要知道初始狀態和狀態轉移方程。初始狀態很容易就能知道,那么狀態轉移方程如何求呢?對於一件物品,我們有放進或者不放進背包兩種選擇:
(1)假如我們放進背包,f[i][j] = f[i - 1][j - weight[i]] + value[i],這里的f[i - 1][j - weight[i]] + value[i]應該這么理解:在沒放這件物品之前的狀態值加上要放進去這件物品的價值。而對於f[i - 1][j - weight[i]]這部分,i - 1很容易理解,關鍵是 j - weight[i]這里,我們要明白:要把這件物品放進背包,就得在背包里面預留這一部分空間。
(2)假如我們不放進背包,f[i][j] = f[i - 1][j],這個很容易理解。
因此,我們的狀態轉移方程就是:f[i][j] = max(f[i][j] = f[i - 1][j] , f[i - 1][j - weight[i]] + value[i])
當然,還有一種特殊的情況,就是背包放不下當前這一件物品,這種情況下f[i][j] = f[i - 1][j]。
下面是實現的代碼:
#include <iostream> #define V 500 using namespace std; int weight[20 + 1]; int value[20 + 1]; int f[20 + 1][V + 1]; int main() { int n, m; cout << "請輸入物品個數:"; cin >> n; cout << "請分別輸入" << n << "個物品的重量和價值:" << endl; for (int i = 1; i <= n; i++) { cin >> weight[i] >> value[i]; } cout << "請輸入背包容量:"; cin >> m; for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (weight[i] > j) { f[i][j] = f[i - 1][j]; } else { f[i][j] = f[i - 1][j] > f[i - 1][j - weight[i]] + value[i] ? f[i - 1][j] : f[i - 1][j - weight[i]] + value[i]; } } } cout << "背包能放的最大價值為:" << f[n][m] << endl; }
特別的是,0-1背包問題還有一種更加節省空間的方法,那就是采用一維數組去解決,下面是代碼:
#include <iostream> #define V 500 using namespace std; int weight[20 + 1]; int value[20 + 1]; int f[V + 1]; int main() { int n, m; cout << "請輸入物品個數:"; cin >> n; cout << "請分別輸入" << n << "個物品的重量和價值:" << endl; for (int i = 1; i <= n; i++) { cin >> weight[i] >> value[i]; } cout << "請輸入背包容量:"; cin >> m; for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { if (weight[i] <= j) { f[j] = f[j] > f[j - weight[i]] + value[i] ? f[j] : f[j - weight[i]] + value[i]; } } } cout << "背包能放的最大價值為:" << f[m] << endl; }
我看過很多博客的描述,講得都不太清楚:為什么要把第二層循環顛倒過來呢?我認為要理解這種方法,用圖是最合適不過了,我在另外一個博客(http://blog.csdn.net/mu399/article/details/7722810)找到了這樣一個圖:

這個表格對於理解0-1背包問題很有用,我們利用它來理解一下為什么要把第二層循環顛倒這個問題。考慮d9這一項,要求出這個狀態,我們有可能利用到的就是e1到e8這8個狀態,當我們把第二層循環顛倒過來時,當我們要求f[j]時,f[j -1]到f[1]還保存着下面一行的狀態,因此可以采用一維數組解決。我們可以考慮一下假如不把第二層循環顛倒,當要求f[j]時,f[j - 1]到f[1]已經是同一行的狀態了,根本沒法求。
我在LeetCode上也曾做過類似的題目——120 Triangle(https://leetcode.com/problems/triangle/description/),也是把二維數組簡化為一維數組去解決問題。
更新:
for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { if (weight[i] <= j) { f[j] = f[j] > f[j - weight[i]] + value[i] ? f[j] : f[j - weight[i]] + value[i]; } } }
這是0-1背包最重要的部分,可以把它改為下面的更簡潔的版本:
for (int i = 1; i <= n; i++) { for (int j = m; j >= weight[i]; j--) { f[j] = f[j] > f[j - weight[i]] + value[i] ? f[j] : f[j - weight[i]] + value[i]; } }
2.完全背包問題
完全背包問題是指每種物品都有無限件,具體的解法我也解釋不清楚,只能先放代碼,談談我的理解了:
#include <iostream> #define V 500 using namespace std; int weight[20 + 1]; int value[20 + 1]; int f[V + 1]; int max(int a, int b) { return a > b ? a : b; } int main() { int n, m; cout << "請輸入物品個數:"; cin >> n; cout << "請分別輸入" << n << "個物品的重量和價值:" << endl; for (int i = 1; i <= n; i++) { cin >> weight[i] >> value[i]; } cout << "請輸入背包容量:"; cin >> m; for (int i = 1; i <= n; i++) { for (int j = weight[i]; j <= m; j++) { f[j] = max(f[j], f[j - weight[i]] + value[i]); } } cout << "背包能放的最大價值為:" << f[m] << endl; }
i=1時,計算只放第一件物品的最大價值。
i=2時,計算加上第二件物品的最大價值(在只放第一件物品的前提下)
以此類推……
值得注意的是,第二層循環要從j=weight[i]開始,這個稍微理解一下即可。
我在別的博客看到了一段分析0-1背包問題和完全背包問題區別的話,覺得對理解這兩個問題很有幫助,因此截圖如下:
3.多重背包問題
多重背包問題限定了一種物品的個數,解決多重背包問題,只需要把它轉化為0-1背包問題即可。比如,有2件價值為5,重量為2的同一物品,我們就可以分為物品a和物品b,a和b的價值都為5,重量都為2,但我們把它們視作不同的物品。
代碼如下:
#include <iostream> using namespace std; #define V 1000 int weight[50 + 1]; int value[50 + 1]; int num[20 + 1]; int f[V + 1]; int max(int a, int b) { return a > b ? a : b; } int main() { int n, m; cout << "請輸入物品個數:"; cin >> n; cout << "請分別輸入" << n << "個物品的重量、價值和數量:" << endl; for (int i = 1; i <= n; i++) { cin >> weight[i] >> value[i] >> num[i]; } int k = n + 1; for (int i = 1; i <= n; i++) { while (num[i] != 1) { weight[k] = weight[i]; value[k] = value[i]; k++; num[i]--; } } cout << "請輸入背包容量:"; cin >> m; for (int i = 1; i <= k; i++) { for (int j = m; j >= 1; j--) { if (weight[i] <= j) f[j] = max(f[j], f[j - weight[i]] + value[i]); } } cout << "背包能放的最大價值為:" << f[m] << endl; }
