背包問題泛指以下這一種問題:
給定一組有固定價值和固定重量的物品,以及一個已知最大承重量的背包,求在不超過背包最大承重量的前提下,能放進背包里面的物品的最大總價值。
這一類問題是典型的使用動態規划解決的問題,我們可以把背包問題分成3種不同的子問題:0-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 - 1][j] , f[i - 1][j - weight[i]] + value[i])
當然,還有一種特殊的情況,就是背包放不下當前這一件物品,這種情況下f[i][j] = f[i - 1][j]。這種場景可以用來初始化f[i][j]。
下面是實現的代碼:
#include <iostream> using namespace std; #define V 500 #define N 20 int weight[N + 1]; int value[N + 1]; int f[N + 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] = max(f[i - 1][j], f[i - 1][j - weight[i]] + value[i]); } } } cout << "背包能放的最大價值為:" << f[n][m] << endl; return 0; }
應用例子:采葯
山洞里有三株不同的草葯,采第一株需要71分鍾,采第二株需要69分鍾,采第三株需要1分鍾。第一株的價值為100,第二株的價值為1,第三侏的價值為2。給你70分鍾的時間,你可以讓采到的草葯的最大的總價值是多少?
分析:
這就是一個0-1背包問題,總時間70分鍾相當於背包的承重能力,采每株草葯的時間相當於每個物品的重量。
直接運行上面的程序,可得到結果:
請輸入物品個數:3 請分別輸入3個物品的重量和價值: 71 100 69 1 1 2 請輸入背包容量:70 背包能放的最大價值為:3
代碼分析:
用i表示當前采了幾種草葯,j表示用了多少時間
(1)i = 1表示要采第一株草葯。
第一株草葯weight[1] = 71,價值value[1] = 100。
j = 1時,f[1][1] = f[0][1] = 0
j = 2時,f[1][2] = f[0][2] = 0
j = 3時,f[1][3] = f[0][3] = 0
……
j = 70時,f[1][70] = f[0][70] = 0
(2)i = 2表示要采前兩株草葯。
第二株草葯weight[2] = 69,value[2] = 1。
j = 1時,f[2][1] = f[1][1] = 0
j = 2時,f[2][2] = f[1][2] = 0
j = 3時,f[2][3] = f[1][3] = 0
……
j = 68時,f[2][68] = f[1][68] = 0
j = 69時,j >= weight[i], f[2][69] = max(f[1][69], f[1][0] + value[2]) = max(0, 0 + 1) = 1。max函數中的第一個參數f[1][69]表示采第一株草葯用掉全部69單位的時間能獲取到的價值,因為第一株草葯需要71單位的時間,所以f[1][69]得到的價值為0;第二個參數f[1][0] + value[2]表示采第一株草葯用了0單位時間,價值為0,把剩余的69的時間全用在采第二株草葯上,得到的價值為1。
j = 70時,j >= weight[i], f[2][70] = max(f[1][70], f[1][1] + value[2] = max(0, 0 + 1) = 1。max函數中的第一個參數f[1][70]表示采第一株草葯用掉全部70單位的時間能獲取到的價值,因為第一株草葯需要71單位的時間,所以f[1][70]得到的價值為0;第二個參數f[1][1] + value[2]表示采第一株草葯用了1單位時間,因為采第一草葯需要71單位的時間,所以1單位時間不夠采第一株草葯,得到的價值為0,把剩余的69的時間全用在采第二株草葯上,得到的價值為1。
(3)i = 3表示要采前三株草葯。
第三株草葯weight[3] = 1,value[3] = 2。
j = 1時,j >= weight[i], f[3][1] = max(f[2][1], f[2][0] + value[3]) = max(0, 0 + 2) = 2,max中的第一個參數表示把這1單位的時間用來采前兩株草葯,第二個參數表示用前0秒的時間采前兩株草葯,用剩余1單位的時間采第三株草葯。
j = 2時,j >= weight[i], f[3][2] = max(f[2][2], f[2][1] + value[3]) = max(0, 0 + 2) = 2
j = 3時,j >= weight[i], f[3][3] = max(f[2][3], f[2][2] + value[3]) = max(0, 0 + 2) = 2
……
j = 68時,j >= weight[i], f[3][68] = max(f[2][68], f[2][67] + value[3]) = max(0, 0 + 2) = 2
j = 69時,j >= weight[i], f[3][69] = max(f[2][69], f[2][68] + value[3]) = max(1, 0 + 2) = 2。max函數的第一個參數f[2][69]表示把69單位的時間用在采前兩株草葯,事實上只能采到第二株草葯,價值為1。第二個參數中的f[2][68]表示前68單位的時間用來采前兩株草葯,無法采到草葯,價值為0;value[3]表示把最后一秒的時間用來采第三株草葯,得到的價值為2。
j = 70時,j >= weight[i], f[3][70] = max(f[2][70], f[2][69] + value[3]) = max(1, 1 + 2) = 2 =3。max函數的第一個參數f[2][70]表示把70單位的時間用在采前兩株草葯,事實上只能采到第二株草葯,價值為1。第二個參數中的f[2][69]表示前69單位的時間用來采前兩株草葯,可以采到第二株草葯,價值為1;value[3]表示把最后一秒的時間用來采第三株草葯,得到的價值為2,二者加起來即總價值為3。
0-1背包問題還有一種更加節省空間的方法,那就是采用一維數組去解決,下面是代碼:
#include <iostream> #include <algorithm> using namespace std; #define V 500 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] = max(f[j], f[j - weight[i]] + value[i]); } } } cout << "背包能放的最大價值為:" << f[m] << endl; return 0; }
代碼分析:
1 仍以采草葯的例子為例,總時間m = 70,草葯數量n = 3。
(1)i = 1, weight[1] = 71
j = 70, weight[1] <= j為假。
j = 69, weight[1] <= j為假。
……
j = 2, weight[1] <= j為假。
j = 1, weight[1] <= j為假。
(2)i = 2, weight[2] = 69
j = 70, weight[2] <= j為真,f[70] = max(f[70], f[1] + value[2]) = max(0, 0 + 1) = 1。max函數中的第一個參數f[70]表示70分鍾的時間用來采第一株草葯。第二個參數f[1] + value[2]表示把前1分鍾的時間用來采第一株草葯,把剩下的69分鍾時間用來采第二株草葯。
j = 69, weight[2] <= j為真,f[69] = max(f[69], f[0] + value[2]) = max(0, 0 + 1) = 1。f[0] + value[2]表示把前0分鍾的時間用來采第一株草葯,把剩下的69分鍾用來采第二株草葯。
j = 68, weight[2] <= j為假。
……
j = 1, weight[2] <= j為假。
(3)i = 3, weight[3] = 1
j = 70, weight[3] <= j為真,f[70] = max(f[70], f[69] + value[3]) = max(1, 1 + 2) = 3。max函數中的第一個參數f[70],根據i=2中的f[70]可知是表示前1分鍾的時間用來采第一株草葯后69分鍾的時間用來采第二株草葯。第二個參數f[69] + value[3]表示前69分鍾的時間用來采前兩株草葯(具體是前0分鍾的時間采第一株后69分鍾的時間采第二株),最后一秒用來采第三株。
j = 69, weight[3] <= j為真,f[69] = max(f[69], f[68] + value[3]) = max(1, 0 + 2) = 2。max函數中的第一個參數f[69]表示這69分鍾的時間用來采前兩株草葯的最大價值,根據i=2中的f[69]可知具體是前0分鍾的時間用來采第一株草葯后69分鍾的時間用來采第二株草葯。第二個參數f[68] + value[3]表示前68分鍾的時間用來采前兩株草葯,最后一秒用來采第三株。
……
j = 2, weight[3] <= j為真,f[2] = max(f[2], f[1] + value[3]) = max(0, 0 + 2) = 2。max函數中的第一個參數f[2]表示前個分鍾的時間用來采前兩株草葯。第二個參數f[1] + value[3]表示前1分鍾的時間用來采前兩株草葯,最后一秒用來采第三株。
j = 1, weight[3] <= j為真,f[1] = max(f[1], f[0] + value[3]) = max(0, 0 + 2) = 2。max函數中的第一個參數f[1]表示前一分鍾的時間用來采前兩株。第二個參數f[0] + value[3]表示前0分鍾的時間用來采前兩株,剩余1分鍾用來采第三株。
(4)最后,f[m] = f[70]即是所求的答案。
2 第二個for循環里面,j為什么要從大到小枚舉,而不是從小到大枚舉?
假如j是從小到大枚舉,則代碼為:
for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (weight[i] <= j) { f[j] = max(f[j], f[j - weight[i]] + value[i]); } } }
i = 2,j = 69時,f[69] = max(f[69], f[0] + value[2]) = max(0, 0 + 1) = 1。
i = 2,j = 70時,f[70] = max(f[70], f[1] + value[2]) = max(0, 0 + 1) = 1。
i = 3,j = 1時,f[1] = max(f[1], f[0] + value[3]) = max(0, 0 + 2) = 2。max函數中的第一個參數f[1]表示這1分鍾的時間用來采前兩株草葯,f[0] + value[3]表示前0分鍾的時間用來采前兩株草葯,剩余1分鍾的時間用來采第三株草葯。
i = 3,j = 2時,f[2] = max(f[2], f[1] + value[3]) = max(0, 2 + 2) = 4。max函數中的第一個參數f[2]表示這2分鍾的時間都用來采前兩株草葯。第二個參數f[1] + value[3],f[1]根據上一步i = 3, j = 1的情景可知 是前0分鍾采前兩株,剩余1分鍾采第三株,value[3]表示第2分鍾采第三株。注意這里第三株草葯在第1分鍾的時間里采了一次,第2分鍾又采了一次,采重復了,所以出錯。
可以把下一段代碼
for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { if (weight[i] <= j) { f[j] = max(f[j], f[j - weight[i]] + value[i]); } } }
進一步簡化為:
for (int i = 1; i <= n; i++) { for (int j = m; j >= weight[i]; j--) { f[j] = max(f[j], f[j - weight[i]] + value[i]); } }
二、完全背包
完全背包和01背包十分相像, 區別就是完全背包中的每種物品有無限件。由之前的選或者不選轉變成了選或者不選、選的話要選幾件。下面給出實現代碼:
#include <iostream> #include <algorithm> using namespace std; #define V 500 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 = weight[i]; j <= m; j++) { f[j] = max(f[j], f[j - weight[i]] + value[i]); } } cout << "背包能放的最大價值為:" << f[m] << endl; return 0; }
仍以上面的采草葯為例,運行結果為:
請輸入物品個數:3 請分別輸入3個物品的重量和價值: 71 100 69 1 1 2 請輸入背包容量:70 背包能放的最大價值為:140
分析:
1 完全背包的代碼與0-1背包的代碼只有一行區別。完全背包中的j是從小到大按順序枚舉的,而0-1背包中的j是從大到小逆序枚舉的。
2 程序執行過程
(1)i = 1時,j = weight[i] = 71, j <= m為假,循環不執行。
(2)i = 2時,weight[2] = 69,value[2] = 1
j = 69, f[69] = max(f[69], f[0] + value[2]) = max(0, 0 + 1) = 1。max中的第一個參數f[69]表示把69分鍾的時間用於第一株草葯,價值是0。第二個參數中的f[0]表示前0分鍾采到到草葯的總價值,value[2]表示剩下的69分鍾用於采第2株草葯。
j = 70, f[70] = max(f[70], f[1] + value[2]) = max(0, 0 + 1) = 1。max中的第一個參數f[70]表示把70分鍾的時間用於第一株草葯,價值是0。第二個參數中的f[1]表示前1分鍾采到到草葯的總價值,value[2]表示剩下的69分鍾用於采第2株草葯。
(3)i = 3時,weight[3] = 1, value[3] = 2
j = 1, f[1] = max(f[1], f[0] + value[3]) = max(0, 0 + 2) = 2。max中的第一個參數f[1]表示把1分鍾的時間用於采前兩株草葯,價值是0。第二個參數中的f[0]表示前0分鍾采到草葯的總價值,value[3]表示剩下的1分鍾用於采第3株草葯。
j = 2, f[2] = max(f[2], f[1] + value[3]) = max(0, 2 + 2) = 4。max中的第一個參數f[2]表示把2分鍾的時間用於采前兩株草葯,價值是0。第二個參數中的f[1]表示前1分鍾采到草葯的總價值,value[3]表示剩下的1分鍾用於采第3株草葯。
j = 3, f[3] = max(f[3], f[2] + value[3]) = max(0, 4 + 2) = 6。max中的第一個參數f[3]表示把3分鍾的時間用於采前兩株草葯,價值是0。第二個參數中的f[2]表示前2分鍾采到草葯的總價值,value[3]表示剩下的1分鍾用於采第3株草葯。
……
j = 68, f[68] = max(f[68], f[67] + value[3]) = max(0, 134 + 2) = 136。max中的第一個參數f[68]表示把68分鍾的時間用於采前兩株草葯,價值是0。第二個參數中的f[67]表示前67分鍾采到到草葯的總價值,value[3]表示剩下的1分鍾用於采第3株草葯。
j = 69, f[69] = max(f[69], f[68] + value[3]) = max(1, 136 + 2) = 138。max中的第一個參數f[69]表示把69分鍾的時間用於采前兩株草葯,價值是1。第二個參數中的f[68]表示前68分鍾采到到草葯的總價值,value[3]表示剩下的1分鍾用於采第3株草葯。
j = 70, f[70] = max(f[70], f[69] + value[3]) = max(1, 138 + 2) = 140。max中的第一個參數f[70]表示把70分鍾的時間用於采前兩株草葯,價值是1。第二個參數中的f[69]表示前69分鍾采到草葯的總價值,value[3]表示剩下的1分鍾用於采第3株草葯。
三、多重背包
多重背包問題限定了一種物品的個數,解決多重背包問題,只需要把它轉化為0-1背包問題即可。比如,有2件價值為5,重量為2的同一物品,我們就可以分為物品a和物品b,a和b的價值都為5,重量都為2,但我們把它們視作不同的物品。
實現代碼:
#include <iostream> #include <algorithm> using namespace std; #define V 1000 int weight[50 + 1]; int value[50 + 1]; int num[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] >> 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]--; } } k--; cout << "請輸入背包容量:"; cin >> m; for (int i = 1; i <= k; i++) { for (int j = m; j >= weight[i]; j--) { f[j] = max(f[j], f[j - weight[i]] + value[i]); } } cout << "背包能放的最大價值為:" << f[m] << endl; return 0; }
仍舊以采草葯為例,運行結果:
請輸入物品個數:3 請分別輸入3個物品的重量、價值和數量: 71 100 2 69 1 2 1 2 2 請輸入背包容量:70 背包能放的最大價值為:4
分析:
(1)第二個for的作用是將同樣的物品進行拆分。
weight[4] = weight[1]; value[4] = value[1];
weight[5] = weight[2]; value[5] = value[2];
weight[6] = weight[3]; value[6] = value[3];
(2)最終拆分后的物品總數量k = 6。