目录
01背包问题——二维数组
01背包问题——一维数组
LeetCode 416. 分割等和子集
LeetCode 1049. 最后一块石头的重量 II
LeetCode 494. 目标和
LeetCode 474. 一和零
总结
01背包问题——二维数组
有n件物品和一个最多能放入重量为w的背包。第i件物品的重量是weight[i],得到的价值是value[i]。每件物品只能使用一次,求解将哪些物品装入背包里物品价值总和最大。
- 确定dp数组以及下标含义
对于背包问题,第一种写法为使用二维数组,即dp[i][j]表示从下标为[0-i]的物品里随意取,放进容量为[j]的背包,价值总和最大是多少。
- 确定递推公式
dp[i][j]有两个方向能推出来:首先就是,不放物品i,当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面的价值相同。即dp[i][j] = dp[i-1][j];其次就是,放入物品i,此时放入物品i之前背包里面的最大价值为dp[i-1][j-weight[i]],那么当放入物品i之后,该背包的最大价值为dp[i-1][j-weight[i]] + value[i]。
所以递推公式应为:dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])。
- dp数组的初始化
如果背包容量j为0的话,无论是选取哪些物品,背包价值总和一定为0。即dp[i][0] = 0;
由递推公式可以看出来,物品i的状态是由i-1推出来的,所以dp[0][j]一定要初始化。即存放编号0物品的时候,各个容量的背包所能存放的最大价值。那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
其他的地方就可以随便初始化了。
- 确定遍历顺序
背包里面有两种遍历顺序,先物品后重量,先重量后物品。先遍历物品更好理解一点。
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
- 举例验证
这一步就自己在本子上一遍就可以了。
01背包问题——一维数组
通过上面二维数组的递推公式可以看出,其实每个数组在更新的时候只需要两层数据就行了,那么在进行递推的时候,可以把上一层的数据拷贝到当前层,在当前层进行计算就OK了。
- dp数组以及下标的含义
这里使用到了一维数组,即dp[j],其代表的含义是容量为j的背包所具有的最大价值。
- 递推公式的确定
dp[j]和dp[i][j]一样有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值。所以递推公式为:dp[j] = max(dp[j], dp[j - weight[i]) + value[i])。
- dp数组的初始化
dp数组下标为0的时候,初始化为0,这一点跟二维数组一样。其他下标,在价值全为正的时候初始化0;有负数参与进来的时候就取负无穷大。
- dp数组的遍历顺序
在二维dp遍历的时候,是先物品后重量。这里也是,但有些许变化。重量这一项应该为倒序遍历。倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
- 举例验证
跟上述一样就自己推一推就行了。
LeetCode 416. 分割等和子集
题目链接:LeetCode 416. 分割等和子集
思想:要把一个集合分割成两个元素和相等的子集,这个问题也可以转化成01背包问题。每个数的值就是其价值和重量,背包的容量就是集合元素总和的一半,因为只用找出一半就行了。套入动态规划的公式。
- 确定dp数组以及下标的含义
这里的dp数组我采用一维数组dp[j],即容量为j的背包所含的最大价值。其中每个物品的价值和重量都是其本身的值。
- 确定递推公式
递推公式也是背包一维数组的递推公式,即dp[j] = max(dp[j], dp[j - weight[i]] + value[i])。
- dp数组的初始化
这道题dp数组中每个元素的值都挺好确认,因为没有负数元素,所以初始化为0。但dp数组的大小不好确认,因为j为重量的含义,在本题中也代表着目前子集和数的大小。根据题目给出的提醒,数组不超过200个元素,每个元素不超过100,所以nums数组最大子集和为200*100=20000,因为这里只需要确认一半,所以dp数组的大小可以设置为10001。
- dp数组遍历顺序
遍历顺序就跟上述一样
代码如下:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
int target = 0;
if (sum % 2 == 1) return false;
target = sum / 2;
vector<int> dp(10001, 0);
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]);
}
}
if (target == dp[target]) return true;
return false;
}
时间复杂度:O(n^2),空间复杂度:O(n)。
LeetCode 1049. 最后一块石头的重量 II
题目链接:LeetCode 1049. 最后一块石头的重量 II
思想:本题跟上题十分相似,可以借助上一题的想法,两块石头互相比,那么就可以更改为找出两个总和差不多的子集合,然后作差,得到最小的可能重量。思想跟上题差不多就不过多赘述了。这里注意还是dp数组的大小取舍。
代码如下:
int lastStoneWeightII(vector<int>& stones) {
int sum = accumulate(stones.begin(), stones.end(), 0);
int target = sum / 2;
vector<int> dp(1501, 0);
for (int i = 0; i < stones.size(); i++) {
for (int j = target; j >= stones[i]; j--) {
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return (sum - dp[target]) - dp[target];
}
时间复杂度:O(n^2),空间复杂度:O(n)。
LeetCode 494. 目标和
题目链接:LeetCode 494. 目标和
思想:本题在回溯算法的时候做过一次,本次可以用01背包的问题再做一次。假设sum是数组内所有元素的总和,设所有加法加起来的总和为x,那么减法为sum - x。根据题意,有x-(sum - x) =target。 x = (target + sum) / 2。此时就可以看出,可以把x设为背包的容量,dp[j]代表着容量为j的背包,满足target的办法有多少种。然后再套入01背包的公式里面。需要注意的是,本题的递推公式和初始化有些许不同。关于递推公式,只要搞到nums[i],凑成dp[j]就有dp[j-nums[i]]种方法。那么终于有多少方法呢,就是把所有dp[j - nums[i]]累加起来。即dp[j] += dp[j - nums[i]]。而关于初始化,根据递推公式可以看出,如果初始化dp[0]为0,那么所有的结果都为0。所以这里应该要把dp[0]初始化为1。其他的就跟上述大差不差。
代码如下:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if ((sum + target) % 2 == 1) return 0;
if (abs(target) > sum) return 0;
int x = (sum + target) / 2;
vector<int> dp(x + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = x; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[x];
}
时间复杂度:O(n^2),空间复杂度:O(n)。
LeetCode 474. 一和零
题目链接:LeetCode 474. 一和零
思想:本题相当于是01背包问题,只不过背包会有两个维度,m和n,而不同长度的字符串就是不同大小的待装物品。关于dp数组的含义,最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。而递推公式的话,dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。然后我们在遍历的过程中,取dp[i][j]的最大值。所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。
物品就是strs里的字符串,背包容量就是题目描述中的m和n。
代码如下:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0));
for (string str : strs) {
int one = 0, zero = 0;
for (char c : str) {
if (c == '0') zero++;
else one++;
}
for (int i = m; i >= zero; i--) {
for (int j = n; j >= one; j--) {
dp[i][j] = max(dp[i][j], dp[i - zero][j - one] + 1);
}
}
}
return dp[m][n];
}
时间复杂度:O(n^3),空间复杂度:O(n^2)。
总结
动态规划中的01背包问题,最最最最最最主要的就是要把问题抽象成01背包问题,什么是背包,什么是物品,物品的重量是什么,物品的价值又是什么。之后就好做了。