不妨先用最朴素的方法,针对每个物品是否放入背包进行搜索:
// 输入
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;
}
void solve() {
printf("%d\n", rec(0, W));
}
只不过,这种方法的搜索深度是 ,而且每一层的搜索都需要两次分支,最坏的就需要 的时间,当 比较大时就没办法解了。由于 在递归调用时存在重复调用的情况,所以我们可以把第一次计算时的结果记录下来,省略掉第二次以后的重复计算试试看。
int dp[MAX_N + 1][MAX_W + 1]; // 记忆化数组
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]) {
res = rec(i + 1, j);
} else {
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
}
// 将结果储存在数组中
return dp[i][j] = res;
}
void solve() {
// 用 -1 表示尚未计算过,初始化整个数组
memset(dp, -1, sizeof(dp));
printf("%d\n", rec(0, W));
}
对于同样的参数,只会在第一次被调用到时执行递归部分,第二次之后都会直接返回。参数的组合不过 种,而函数内只调用两次递归,所以只需要 的复杂度就能解决问题。这种方法一般被称为记忆化搜索。
如果对记忆化搜索还不是很熟练的话,可以写成穷竭搜索的写法:
// 目前选择的物品价值总和是 sum,从第 i 个物品之后的物品中挑选重量总和小于 j 的物品
int rec(int i, int j, int sum) {
int res;
if (i == n) {
// 已经没有剩余物品了
res = sum;
} else if (j < w[i]) {
// 无法挑选这个物品
res = rec(i + 1, j, sum);
} else {
// 挑选和不挑选的两种情况都尝试一下
res = max(rec(i + 1, j, sum), rec(i + 1, j - w[i], sum + v[i]));
}
return res;
}
在需要剪枝的情况下,可能会像这样把各种参数都写在函数上,但在这种下会让记忆化搜索难以实现,需要注意。
接下来,我们来仔细研究一下前面的算法利用到的这个记忆化数组。记 为根据 的定义,从第 个物品开始挑选总重小于 时,总价值的最大值。于是就有如下递推式:
这样不用写递归函数,直接利用递推式将各项的值计算出来,简单的二重循环也能解决这一问题。
int dp[MAX_N + 1][MAX_W + 1]; // DP 数组
void solve() {
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]] + w[i]);
}
}
}
printf("%d\n", dp[0][W]);
}
这个算法的复杂度与前面的相同,也是 ,但是简洁了很多。以这种方式一步步按顺序求出问题的解的方法被称作动态规划法,也就是常说的 。