刷题笔记
0-1背包问题
动态规划
有关于动态规划可以解决0-1背包问题的证明,即证明原问题的最优解包含子问题的最优解,可以采用反证法来证明。(教材上有)
dp数组的定义以及含义:首先采用二维dp,我们需要同时考虑value和weight两个变量。dp[i][j] 表示从下标为[1-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。(为了回溯结果方便,物品下标编号从1到n)
dp状态转移公式:对于第i个物品,决策只有两种,一种是第i个物品放入背包x[i] = 1,这时dp[i][j] = dp[i - 1][j - weight[i]] + value[i]。因为dp[i][j] 的定义是从下标为[1-i]的物品里任意取,放进容量为j的背包,价值总和的最大值。所以dp[i - 1][j - weight[i]] 就是给第i个物品留下空间,然后在[1-i-1]的物品里任意取,放进容量为j - weight[i]的背包,价值总和的最大值,再加上物品i的价值value[i]就等于dp[i][j]。另一种是,第i个物品不放入背包,对于第i个物品不放入背包的情况,dp[i][j] = dp[i - 1][j],此时背包的状态没有发生变化。综合这两种决策的情况,dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])。
dp数组的初始化:这里因为物品编号是从1开始的,所以对于dp的二维数组全部初始化为0就可以递推出结果,从dp[0][0]开始递归,不过dp[0]那一行是没有任何实际意义的,物品0并不存在,仅仅因为在回溯dp数组,得到最后的最优解向量时,代码实现方便判断terminate condition。
如果物品的编号是从0开始的,那么就需要单独对一些情况进行初始化了,首先对于dp[i][0]即dp数组的第0列,全部初始化为0,因为当背包当前重量j==0时,不能装入任何物品。对于dp[0][j]即dp数组的第1行,初始化时,需要看当前递推过程中背包的重量能不能装下编号为0的物品,如果不能装下那么dp[0][j]依然初始化为0,如果可以装下,那么dp[0][j]初始化时就需要初始化为value[0]了。这里需要注意因为是0-1背包问题,每个物品都只有选或者不选两种状态,只能选取一次。
weight = {2, 3, 4, 5 };
value = {3, 4, 5, 6 };
n = 4;
bagWeight = 9;
dp[i][0]全部为0,weight = 0
dp[0][j]初始化
for (int j = weight[0]; j <= bagWeight; j++) { 正序遍历,错误选择
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
}
dp[0][2] = dp[0][0] + value[0] = 3
dp[0][3] = dp[0][1] + value[0] = 3
dp[0][4] = dp[0][2] + value[0] = 6
出现错误,因为0-1背包每个物品只能选择一次,前面dp[0][2]选择了一次编号0的物品,
dp[0][4]又在dp[0][2]基础上又选择了一次。
所以需要采用逆序遍历的方式初始化。
for (int j = bagWeight; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
dp[0][9] = dp[0][7] + value[0] = 3 前面选择的状态并不会影响后面的状态
dp[0][7] = dp[0][5] + value[0] = 3
dp数组的遍历方式:对于二维数组而言,无论是先遍历物品,还是先遍历容量都是可以的,从递推公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])可以看出,dp[i][j]的最终的值是依赖于二维数组的上面元素dp[i-1][j]的值,或者它的上方偏左的元素dp[i-1][j-weight[i]]的值。而无论是先遍历物品,按行生成dp数组,还是先遍历容量,按列生成dp数组,递推公式需要的条件都是满足的。
dp数组结果的回溯:采用回溯的方式收集最终结果,如果dp[i][j] == dp[i - 1][j]说明没有选择物品i即x[i] = 0,继续向下递归backtracking(i - 1, j)。如果dp[i][j] == dp[i - 1][j - weight[i]] + value[i]说明选择了物品i即x[i] = 1,继续向下递归backtracking(i - 1, j - weight[i])。终止条件if (i == 0 || j <= 0) return。
二维dp的代码如下:
class knapsackdp
{
public:
void initialFunc() { //为了方便对dp结果的回溯,物品编号从1开始,对应数组下标1,数组下标0不关心初始化为0
weight = {0, 2, 3, 4, 5 };
value = {0, 3, 4, 5, 6 };
x = {0, 0, 0, 0, 0 };
n = 4;
bagWeight = 9;
dp = vector<vector<int>>(weight.size(), vector<int>(bagWeight+1, 0));
}
void dynamicFunc() {
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= bagWeight; j++) {
if (weight[i] > j) {
dp[i][j] = dp[i - 1][j];
}
else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
backtracking(n, bagWeight);
}
void backtracking(int i, int j) {
if (i == 0 || j <= 0) {
return;
}
if (dp[i][j] == dp[i - 1][j]) {
x[i] = 0;
backtracking(i - 1, j);
}
else if (dp[i][j] == dp[i - 1][j - weight[i]] + value[i]) {
x[i] = 1;
cout << "index = " << i << ", weight = " << weight[i] << ", value = " << value[i] << endl;
backtracking(i - 1, j - weight[i]);
}
}
void printdp() {
cout << " ";
for (int j = 0; j <= bagWeight; j++) {
cout << j << " ";
}
cout << endl;
for (int i = 0; i <= n; i++) {
cout << i << " ";
for (int j = 0; j <= bagWeight; j++) {
cout << dp[i][j] << " ";
}
cout << endl;
}
}
private:
vector<int> weight; //物品重量数组
vector<int> value; //物品价值数组
vector<int> x; //x[i] == 1表示第i个物品放入背包, x[i] == 0表示第i个物品不放入背包
vector<vector<int>> dp; //二维dp数组,dp[i][j] 表示从下标为[1-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
int n; //物品数量
int bagWeight; //背包容量
};
二维dp数组的定义以及含义:dp[i][j] 表示从下标为[1-i]的物品里任意取,放进容量为j的背包,所背的物品的最大价值和
一维dp数组的定义以及含义:dp[j]表示:容量为j的背包,所背的物品的最大价值和。
对于二维dp的状态转移公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])。可以看出dp[i][j]的状态仅仅依赖于二维数组的上一层的状态,利用这个特性可以对二维dp数组进行降维,降维为一维数组。这样考虑,在遍历的时候,先对物品编号i进行遍历,假设对i=1的遍历刚遍历完毕,此时降维的数组dp[j]中存储的是i=1时的背包中的最大价值,然后第二次遍历i的时候,i=2,但是此时dp[]数组中还存的是上一轮i=1的遍历的结果,这样就可以直接进行比较了。这里需要注意背包容量的遍历必须是逆序的遍历,因为在二维dp中当前位置的状态依赖于上方和左上方的元素的状态,如果是上方其实没有影响dp[i][j] = dp[i-1][j] (i=2),直接降维为dp[j] (i=2) = dp[j] (i=1) 就可以了。但是如果是左上方就有影响了,左上方在降维为一维度dp时就是一维度dp左边的元素,这时遍历到当前位置时,dp数组的前面如果采用正序遍历状态已经更新过了,已经用i=2的状态把i=1的上一层的状态覆盖了,就不能得到正确结果,原本的dp[i][j] = dp[i-1][j - weight[i]] + value[i] (i = 2),变成了dp[j] = dp[j-weight[i]] + value[i],采用正序遍历的情况,二维dp需要的是dp[1][j-weight[2]],而一维dp中的这部分语义为dp[2][j-weight[2]],就不对了。 并且这里也必须先对物品进行遍历,再对背包容量逆序遍历,如果先对背包容量遍历,再对物品进行遍历,dp[j]中放物品会被不断覆盖,实际只会放一个物品,导致语义错误。
\ 0 1 2 3 4 5 6 7 8 9 //二维dp
1 0 0 3 3 3 3 3 3 3 3
2 0 0 3 4 4 7 7 7 7 7
3 0 0 3 4 5 7 8 9 9 12
4 0 0 3 4 5 7 8 9 10 12
\ 2 3 4 5 6 7 8 9 //一维dp每层状态
1 3 3 3 3 3 3 3 3 //i = 1
2 4 4 7 7 7 7 7 //i = 2
3 5 7 8 9 9 12 //i = 3
4 7 8 9 10 12 //i = 4
class knapsackdp2
{
public:
void initialFunc() {
weight = { 0, 2, 3, 4, 5 };
value = { 0, 3, 4, 5, 6 };
x = { 0, 0, 0, 0, 0 };
n = 4;
bagWeight = 9;
dp = vector<int>(bagWeight + 1, 0);
}
void dynamicFunc() {
vector<stack<int>> result;
for (int i = 0; i <= n; i++) {
stack<int> sck;
for (int j = bagWeight; j >= weight[i]; j--) {
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
sck.push(dp[j]);
}
result.push_back(sck);
}
printdp(result);
}
void printdp(vector<stack<int>> result) {
for (int i = 0; i < result.size(); i++) {
while (!result[i].empty()) {
cout << result[i].top() << " ";
result[i].pop();
}
cout << endl;
}
}
private:
vector<int> weight; //物品重量数组
vector<int> value; //物品价值数组
vector<int> x; //x[i] == 1表示第i个物品放入背包, x[i] == 0表示第i个物品不放入背包
vector<int> dp; //一维dp数组,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
int n; //物品数量
int bagWeight; //背包容量
};