问题描述
01揹包是一个可以用动态规划解决的经典问题:给定 n 种物品和一揹包。物品 i 的重量是 wi ,其价值为 vi ,揹包的容量为 c 。问应如何选择装入揹包中的物品,使得装入揹包的物品的总价值最大?
在选择装入揹包的物品时,对每种物品 i 只有两种选择,即装入揹包或不装入揹包。不能将物品 i 装入揹包多次,也不能只装入部分的物品 i 。因此,称为01揹包问题。
形式化描述
给定 c>0,wi>0,vi>0,1≤i≤n ,要求找出一个 n 元0-1向量 (x1,x2,⋯,xn),xi∈{0,1},1≤i≤n , 使得 ∑i=1nwixi≤c , 而且 ∑i=1nvixi 达到最大。因此,0-1揹包是一个特殊的整数规划问题:
max∑i=1nvixi
⎧⎩⎨∑i=1nwixi≤cxi∈{0,1},1≤i≤n
递归关系
01揹包具有最优子结构性质,证明略。下面来根据其递归关系得到递推公式。
设所给01揹包问题的子问题
max∑k=invkxk
⎧⎩⎨∑k=inwkxk≤jxk∈{0,1},i≤k≤n
的最优值为
m(i,j) ,即
m(i,j) 是揹包容量为 j ,可选择物品为 i,i+1,⋯,n 时0-1揹包问题的最优值。由0-1揹包问题的最优子结构性质,
分别考虑拿上或者不拿物品 i , 以及揹包剩余容量是否足够装下物品 i ,可以建立计算
m(i,j) 的递归式如下:
m(i,j)={max{m(i+1,j),m(i+1,j−wi)+vi} j≥wim(i+1,j) 0≤j<wi
m(n,j)={vn j≥wn0 0≤j<wn
这样编写的程序会计算得到一张
m(i,j) 的二维表,最后的结果是(代码中下标从0开始):
ans=m(0,c)
计算过程
现在有五件物品,重量分别是2,2,6,5,4,价值分别是6,3,5,4,6,揹包最多拿上重量10个单位的物品,如何让揹包里装入的物品具有最大的价值总和?
下表是按照上述算法的计算结果,表格计算顺序为自下向上,从左到右。
i | weight | value | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 6 | 0 | 0 | 6 | 6 | 9 | 9 | 12 | 12 | 15 | 15 | 15 |
2 | 2 | 3 | 0 | 0 | 3 | 3 | 6 | 6 | 9 | 9 | 9 | 10 | 11 |
3 | 6 | 5 | 0 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 10 | 11 |
4 | 5 | 4 | 0 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 10 | 10 |
5 | 4 | 6 | 0 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
例如,对于 m(2,6) 代表的是揹包容量为6,可选择物品为2,3,4,5时0-1揹包的最优值。那么 m(2,6)=max{m(3,6),m(3,6−2)+3}=max{6,9}=9
代码
有一点需要注意的是:在上面描述问题的时候始终是从1开始计数的,代码的话是从下标0开始的。可以在网上找一些数据测试一下程序。
#include <iostream>
using namespace std;
//设定一个最大的揹包容量+1的值
#define MAXCP1 10000
void Knapsack(int v[], int w[], int c, int n, int m[][MAXCP1]);
int main()
{
int c, n; //揹包容量c 物品个数n
cin >> c >> n;
int w[n], v[n], m[n][MAXCP1]; //n个物品的重量w[n],n个物品的价值v[n],以及二维数组m
for (int i = 0; i < n; i++)
{
cin >> w[i] >> v[i];
}
Knapsack(v, w, c, n, m);
cout << m[0][c] << endl;
return 0;
}
void Knapsack(int v[], int w[], int c, int n, int m[][MAXCP1])
{
/* *初始化 */
for (int j = 0; j < w[n-1]; j++)
m[n-1][j] = 0;
for (int j = w[n-1]; j <= c; j++)
m[n-1][j] = v[n-1];
/* *根据递推关系得到一张二维表 */
for (int i = n - 2; i > 0; i--)
{
for (int j = 0; j < w[i]; j++)
m[i][j] = m[i+1][j];
for (int j = w[i]; j <= c; j++)
m[i][j] = max(m[i+1][j], m[i+1][j-w[i]] + v[i]);
}
/* *得到最优值m[0][c] */
m[0][c] = m[1][c];
if (c >= w[0])
m[0][c] = max(m[1][c], m[1][c-w[0]] + v[0]);
}
事实上,代码可以看起来更简洁,如下:
#include <iostream>
#include <vector>
using namespace std;
void Knapsack(int v[], int w[], int c, int n, vector<int> &m);
int main()
{
int c, n; //揹包容量c 物品个数n
cin >> c >> n;
int w[n], v[n]; //n个物品的重量w[n],n个物品的价值v[n]
for (int i = 0; i < n; i++)
{
cin >> w[i] >> v[i];
}
vector<int> m(c+1, 0); //m[j]表示揹包容量为j时可以拿到的最大价值
Knapsack(v, w, c, n, m);
cout << m[c] << endl;
return 0;
}
void Knapsack(int v[], int w[], int c, int n, vector<int> &m)
{
for (int i = 0; i < n; i++)
{
for (int j = c; j >= w[i]; j--)
m[j] = max(m[j], m[j - w[i]] + v[i]); //表示从拿或者不拿物品i做一个选择
}
}