0-1背包理论基础(一)
要先知道暴力解法,
然后用动态规划来解决,相对于暴力解法,DP在性能上提高了多少。
1️⃣ 确定 dp数组 以及 下标 的含义
dp[i][j] :从下标为 [0 ~ i] 之间的物品里任取,放进容量为 j 的背包,价值总和最大是多少。
(后面在写递推公式,初始化,确定遍历顺序都是紧紧围绕着dp数组的含义)
2️⃣ 确定递推公式
那么可以有两个方向推出来dp[i][j],
- 不放物品i:背包容量为j,所能装的最大价值是
dp[i - 1][j] - 放物品i:放物品i的最大价值是 背包容量减去物品i的容量所能放的最大价值,加上物品i的价值。
由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么 dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
可以这么理解:放了物品i,还剩下j - weight[i]的容量可以放其他物品,剩下空间能得到的最大值是dp[i-1][j-weight[i]]
这里的意思是:你放了物品 i 之后剩下的空间(就是他说的不放物品i的空间)能放的最大价值。
先把物品i放进去,则还剩j-weight的容量,这些容量用来放前i-1个
3️⃣ dp数组如何初始化
非0下标初始化成什么都可以,都不影响最后求 dp[i][j] 的值。
4️⃣ 确定遍历顺序
对于二维dp数组实现的0-1背包问题,2层for循环是可以颠倒的。
5️⃣ 举例推导dp数组
0-1背包理论基础(二)
网友:应该是把 dp[i][j] 覆盖到 dp[i-1][j] 那里
网友:就像爬楼梯中维护的一维dp数组,实际有用的就前两个元素,就可以简化为只维护两个元素,而不是一个数组,这里也是同理。
网友:只有一个数组,实际上自身拷贝自身,虽然数值相同但表示的意义不同相同
递推公式:
- 不放物品i:dp[j]
- 放物品i:dp[j - weight[i] + value[i]]
dp[j] = max(dp[j], dp[j - weight[i] + value[i]]);
不放物品就表示不覆盖原来的数据
dp数组的初始化:
首先,背包容量不能是负数。(若容量是负数,就不在背包问题的讨论范围内了。)
若背包容量是正数,应该初始化为多少?
由递推公式:当前值 = max (本身,前一个值 + value)
遍历顺序:
倒序遍历,保证每个物品只被添加过一次。
⚠️只能先遍历物品,再遍历背包
在一维dp数组中,把一个矩阵压缩成一维数组,数据滚动利用,我们使用倒序来保证每个物品添加一次,
如果先遍历背包,再遍历物品,这样dp数组里面记录的就都是一个物品的数值了。
(不理解,可以画表格解释,很直观)
用代码,把dp数组打印出来。
416.分割等和子集
题目链接:416.分割等和子集
难度指数:😀😐😕😟
AC代码: (核心代码模式)
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
//d[i]中的i表示背包内总和
//题目中说:每个数组中的元素不会超过100,数组的长度不会超过200
//因此总和不会大于20000,背包最大只需要其中的一半,所以10001就可以了
vector<int> dp(10001, 0);
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
}
if (sum % 2 == 1) {
return false;
}
int target = sum / 2;
//开始0-1背包
for (int i = 0; i < nums.size(); i++) {
for (int j = target; j >= nums[i]; j--) { //每个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
//集合中的元素正好可以凑成总和target
if (dp[target] == target) return true;
return false;
}
};