摘要
本文主要介绍了LeetCode动态规划的几个题目,包括1049. 最后一块石头的重量 II、494. 目标和、474.一和零,并在最后对01背包问题进行了总结。
1、1049. 最后一块石头的重量 II
1.1 思路
- 同416.分割等和子集
1、如果套用01背包问题到本题中?
这里,我们可以将总重量的一半看作是背包的容量
m,每个石头的重量看作是物品的重量weight[i],然后我们可以尝试在背包容量为m的情况下选择一些石头,使得它们的总重量尽可能接近m。这就是一个 0/1 背包问题,其中目标是尽可能填满背包容量m
1.2 代码
public int lastStoneWeightII(int[] stones) {
int sum = getSum(stones);
int target = sum / 2;
int[] dp = new int[target+1];
for(int j=target; j>=1; j--) {
if(j < stones[0]) {
break;
}
dp[j] = stones[0];
}
for(int i=1; i<stones.length; i++) {
for(int j=target; j>=1; j--) {
if(j < stones[i]) {
dp[j] = dp[j];
} else {
dp[j] = Math.max(dp[j], dp[j-stones[i]] + stones[i]);
}
}
}
return Math.abs(sum - dp[target] * 2);
}
public int getSum(int[] stones) {
int sum = 0;
for(int stone : stones) {
sum += stone;
}
return sum;
}
2、494. 目标和
2.1 思路
1、如果套用01背包问题到本题中?
- 既然为target,那么就一定有 left组合 - right组合 = target。
- left + right = sum,而sum是固定的。right = sum - left
- 公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2 。
- target是固定的,sum是固定的,left就可以求出来。
- 此时问题就是在集合nums中找出和为left的组合。
2、如何推导出 dp[j] += dp[j - nums[i]]?
只要搞到
nums[i],凑成dp[j]就有dp[j - nums[i]]种方法例如:
dp[j],j 为5
- 已经有一个1(
nums[i]) 的话,有dp[4]种方法 凑成 容量为5的背包。- 已经有一个2(
nums[i]) 的话,有dp[3]种方法 凑成 容量为5的背包。- 已经有一个3(
nums[i]) 的话,有dp[2]中方法 凑成 容量为5的背包- 已经有一个4(
nums[i]) 的话,有dp[1]中方法 凑成 容量为5的背包- 已经有一个5 (
nums[i])的话,有dp[0]中方法 凑成 容量为5的背包
3、dp[0] 为什么等于 1?
如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。
所以本题我们应该初始化 dp[0] 为 1。
-
动规五部曲
dp数组以及下标的含义:填满j(包括j)这么大容积的包,有dp[j]种方法- 递推公式:
dp[j] += dp[j - nums[i]] dp数组如何初始化:dp[0]为 1dp数组遍历顺序:一维数组,先遍历物品,然后遍历背包,并且背包倒序遍历- 打印
dp数组
输入:nums: [1, 1, 1, 1, 1], S: 3
bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4
2.2 代码
public int findTargetSumWays(int[] nums, int target) {
int sum = getSum(nums);
int left = (sum + target) / 2;
if(left < 0 || (sum + target) % 2 != 0) {
return 0;
}
int[] dp = new int[left+1];
dp[0] = 1;
for(int i=0; i<nums.length; i++) {
for(int j=left; j>=0; j--) {
if(j >= nums[i]) {
dp[j] += dp[j - nums[i]];
}
}
}
return dp[left];
}
public int getSum(int[] nums) {
int sum = 0;
for(int num : nums) {
sum += num;
}
return sum;
}
3、474.一和零 *
3.1 思路
这个解法使用一个二维数组
dp,其中dp[i][j]表示在允许最多i个 0 和j个 1 的情况下,能够组成的最大字符串数量。我们遍历每个字符串,统计它包含的 0 和 1 的数量,然后通过动态规划更新dp数组。
在更新
dp数组时,我们从右下角开始,考虑每个可能的情况:选择当前字符串或不选择当前字符串。如果选择当前字符串,则可以从dp[i - zeros][j - ones]中得到一个额外的字符串数量,然后更新dp[i][j]为当前最大值。最终,dp[m][n]中存储的就是能够组成的最大字符串数量。
动规五部曲
-
dp数组以及下标的含义dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]
-
递推公式
dp[i][j]可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1dp[i][j]就可以是dp[i - zeroNum][j - oneNum] + 1- 然后我们在遍历的过程中,取
dp[i][j]的最大值 - 所以递推公式:
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
-
dp数组如何初始化- 01背包的
dp数组初始化为0就可以。
- 01背包的
-
dp数组遍历顺序- 在动态规划:关于01背包问题,你该了解这些!(滚动数组)中,我们讲到了01背包为什么一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!
- 那么本题也是,物品就是
strs里的字符串,背包容量就是题目描述中的m和n。
-
举例推导
dp数组
3.2 代码
public int findMaxForm(String[] strs, int m, int n) {
//dp[i][j]表示i个0和j个1时的最大子集
int[][] dp = new int[m + 1][n + 1];
int oneNum, zeroNum;
for (String str : strs) {
oneNum = 0;
zeroNum = 0;
for (char ch : str.toCharArray()) {
if (ch == '0') {
zeroNum++;
} else {
oneNum++;
}
}
//倒序遍历
for (int i = m; i >= zeroNum; i--) {
for (int j = n; j >= oneNum; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
4、01背包总结
- 纯 0 - 1 背包是求 给定背包容量 装满背包 的最大价值是多少。
- 416. 分割等和子集是求 给定背包容量,能不能装满这个背包。
- 1049. 最后一块石头的重量 II 是求 给定背包容量,尽可能装,最多能装多少
- 494. 目标和是求 给定背包容量,装满背包有多少种方法。
- 474.一和零是求给定背包容量,装满背包最多有多少个物品。