01背包问题
有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
1 <= n <= 100
1 <= wi, vi <= 100
1 <= w <= 10000
int n, W;
int w[MAX_n], v[MAX_n];
//从第i个物品开始挑选总重小于j的部分
int rec(int i, int j)
{
int res;
if (i == n) {
//已经没有剩余物品了
res = 0;
}
else if (j < w[i]) {
//无法挑选这个物品
res = rec(i + 1, j);
}
else {
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
}
return res;
}
这种方法的搜索深度为n,且每一层搜索都需要两次分支,最坏需要O(2^n)的时间。
该算法中许多参数被重复计算,我们可以将第一次的时间结果记录下来,省略重复计算。
int dp[MAX_N+1][MAX_N+1];
memset(dp, -1, sizeof(dp));
int rec(int i, int j)
{
if (dp[i][j] >= 0) {
//已经计算过的话直接使用之间的结果
return dp[i][j];
}
int res;
if (i == n) {
res = 0;
}
else if (j < w[i]) {
rec(i + 1, j);
}
else {
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
}
//将结果记录在数组中
return dp[i][j] = res;
}
对于同样的参数,只会在第一次被调用时执行递归部分,第二次之后都会直接返回。
参数组合不过nW种,而函数内只调用2次递归,所以只需要O(nW)的复杂度就能解决问题。
记dp[i][j]为根据rec的定义,从第i个物品开始挑选总重小于j时,总价值的最大值,于是有如下递推式:
dp[n][j] = 0。
当j < w[i]时,dp[i][j] = dp[i+1][j]。
其他情况下,dp[i][j] = max(dp[i+1][j], dp[i+1][j-w[i]]+v[i])。
int dp[MAX_N+1][MAX_N+1];
for (int i = n - 1; i >= 0; i--) {
for (int j = 0; j <= W; j++) {
if (j < w[i]) {
dp[i][j] = dp[i + 1][j];
}
else {
dp[i][j] = max(dp[i+1][j], dp[i+1][j-w[i]] + v[i]);
}
}
}
printf("%d\n", dp[0][W]);
dp[i+1][j]=从0到i这i+1个物品中选出总重量不超过j的物品时总价值的最大值。
dp[0][j] = 0。
当j < w[i]时,dp[i+1][j] = dp[i][j]。
其他情况下,dp[i+1][j] = max(dp[i][j], dp[i][j-w[i]] + v[i])。
for (int i = 0; i < n; i++) {
for (int j = 0; j <= W; j++) {
if (j < w[i]) {
dp[i+1][j] = dp[i][j];
}
else {
dp[i+1][j] = max(dp[i][j], dp[i][j-w[i]] + v[i]);
}
}
}
printf("%d\n", dp[n][W]);
此外,除了运用递推方式逐项求解之外,还可以把状态转移想象成从“前i个物品中选取总重不超过j时的状态”向“前i+1个物品中选取总重不超过j”和“前i+1个物品中选取总重不超过j+w[i]时的状态”的转移。
for (int i = 0; i < n; i++) {
for (int j = 0; j <= w; j++) {
dp[i+1][j] = max(dp[i+1][j], dp[i][j]);
if (j + w[i] <= W) {
dp[i+1][j+w[i]] = max(dp[i+1][j+w[i]], dp[i][j] + v[i]);
}
}
}
printf("%d\n", dp[n][W]);
当背包问题的限制条件很大时,还可以改变DP的对象。之前的方法中,我们用DP针对不同的重量限制计算最大的价值。
这次不妨用DP针对不同的价值计算最小的重量。
dp[i+1][j]=前i个物品中挑选出价值总和为j时总重量的最小值(不存在时就是一个充分大的数值INF)。
由于前0个物品中什么都挑选不了,所以初始值为:
dp[0][0] =0。
dp[0][j] = INF。
此外,前i个物品中挑选出价值总和为j时,一定有:
前i个物品中挑选价值总和为j的部分。
前i-1个物品中挑选出价值总和为j-v[i]的部分,然后再选中第i个物品。
所以得到dp[i+1][j] = min(dp[i][j], dp[i][j-v[i]]+w[i])。
最终的答案就对应于令dp[n][j] <= W的最大的j。
int dp[MAX_N+1][MAX_N*MAX_V+1];
for (int i = 1; i < MAX_N * MAX_V + 1; i++)
dp[0][i] = INF;
dp[0][0] = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= MAX_N * MAX_V; j++) {
if (j < v[i]) {
dp[i+1][j] = dp[i][j];
}
else {
dp[i+1][j] = min(dp[i][j], dp[i][j-v[i]] + w[i]);
}
}
}
int res = 0;
for (int i = 0; i <= MAX_N*MAX_V; i++) {
if (dp[n][i] <= W)
res = i;
}
printf("%d\n", res);