0-1背包问题——动态规划,二维dp和一维dp

刷题笔记

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;          //背包容量
};
上一篇:记一次提升18倍的性能优化


下一篇:动态规划