背包问题是常见的动态规划题目,描述如下:
一个背包有一定的承重cap,有N件物品,
每件都有自己的价值,记录在数组v中,也都有自己的重量,记录在数组w中,
每件物品只能选择要装入背包还是不装入背包,
要求在不超过背包承重的前提下,选出物品的总价值最大。
给定物品的重量w价值v及物品数n和承重cap。请返回最大总价值。
测试样例:
[1,2,3],[1,2,3],3,6
返回:6
暴力实现
针对背包问题,最简单思路是递归,每次有两种选择,装入背包或者不装入背包,继续递归,直到所有物品都递归过了,返回。每个物品有两种选择,放入或者不放入,时间复杂度O(2^n)。递归深度为n,空间复杂度O(n)。
#include <iostream>
#include <vector>
using namespace std;
int recur(vector<int> & w, vector<int>& v, int n, int current_index, int cap, int current_cap, int current_value) {
if (current_index == n)
return current_value;
//装进背包
int use = 0;
if ((current_cap + w[current_index]) <= cap)
use = recur(w, v, n, current_index + 1, cap, current_cap + w[current_index], current_value + v[current_index]);
//不装进背包
int no_use = recur(w, v, n, current_index + 1, cap, current_cap, current_value); return use > no_use ? use : no_use;
}
int max(vector<int> w, vector<int> v, int n, int cap) {
return recur(w, v, n, 0, cap, 0, 0);
}
int main() {
//测试1
//vector<int> w = { 1, 2, 3};
//vector<int> v = { 1, 2, 3};
//int res = max(w, v, 3, 6);//6
//测试2
//vector<int> w = { 42,25,30,35,42,21,26,28 };
//vector<int> v = { 261,247,419,133,391,456,374,591 };
//int res = max(w, v, 8, 297);//2872
//测试3 用暴力会通不过
vector<int> w = { 15, 21, 33, 36, 30, 19, 37, 19, 16, 29, 34, 40, 31, 27, 16, 31, 38, 17, 35, 22, 17, 36, 28, 28, 21, 19, 32, 17 };
vector<int> v = { 523, 612, 359, 90, 79, 630, 437, 83, 520, 463, 262, 44, 354, 531, 150, 186, 642, 122, 93, 383, 125, 70, 271, 519, 76, 368, 46, 278 };
int res = max(w, v, 28, 394);//6849
cout << res << endl;
system("pause");
return 0;
}
记忆搜索方法
暴力递归中会有大量的重复计算,记忆搜索将其优化,用额外的空间存储下已经递归过的结果,help[i][j]代表了重量限制为j时,从第i个到第n个物品所能获取到的最大价值。递归时help[i][j]重复使用到的次数越多,复杂度越低。
int recur(vector<int> & w, vector<int>& v, int n, int current_index, int cap, int **help) {
if (current_index == n)
return 0;
//装进背包
int use = 0;
int cap_remain = cap - w[current_index];
if (cap_remain >= 0) {
use = v[current_index];
if (help[current_index + 1][cap_remain] == -1)
help[current_index + 1][cap_remain] = recur(w, v, n, current_index + 1, cap_remain, help);
use += help[current_index + 1][cap_remain];
}
//不装进背包
if (help[current_index + 1][cap] == -1)
help[current_index + 1][cap] = recur(w, v, n, current_index + 1, cap, help);
int no_use = help[current_index + 1][cap];
//返回较大值
return use > no_use ? use : no_use;
}
int max(vector<int> w, vector<int> v, int n, int cap) {
//help二维数组存下递归过的结果
int ** help = new int*[n+1];
for (int i = 0; i < n+1; i++) {
help[i] = new int[cap + 1];
}
for (int i = 0; i < n+1; i++) {
for (int j = 0; j < cap+1; j++)
help[i][j] = -1;
}
return recur(w, v, n, 0, cap, help);
}
动态规划
从记忆搜索方法进一步发现状态转移的规律,得到动态规划方法。
时间复杂度O(n*cap),空间复杂度O(n*cap)。
int max(vector<int> w, vector<int> v, int n, int cap) {
int ** dp = new int *[n];
for (int i = 0; i < n; i++) {
dp[i] = new int[cap + 1];
}
for (int i = 0; i < n; i++) {
dp[i][0] = 0;
}
for (int i = 0; i < cap + 1; i++) {
if (i >= w[0])
dp[0][i] = v[0];
else
dp[0][i] = 0;
}
//dp[i][j]代表重量限制为j时,前i个物品所能达到的最大价值
for (int i = 1; i < n; i++) {
for (int j = 1; j < cap + 1; j++) {
//物品重量小于背包重量时,选择将物品装入,或者不装入,比较两者哪个能获得最大价值
if (j >= w[i] && dp[i - 1][j] < (dp[i - 1][j - w[i]] + v[i]))
dp[i][j] = dp[i - 1][j - w[i]] + v[i];
else
dp[i][j] = dp[i - 1][j];
}
}
return dp[n - 1][cap];
}
总结
对于动态规划的题目,先把暴力搜索方法写出来,尝试观察其规律,找出动态规划的规律。但往往动态规划的思路比较难想出来,这时候观察递归重复计算的情况,将其优化为记忆搜索,也能使其通过大部分的测试了。