LeetCode 1049 最后一块石头的重量 II
思路
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
- 如果
x == y,那么两块石头都会被完全粉碎; - 如果
x != y,那么重量为x的石头将会完全粉碎,而重量为y的石头新重量为y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
考虑什么时候,可以没有剩余石头。每次粉碎都是粉碎其中一块石头的两倍重量,所以经过n次粉碎能够粉碎所有石头,即把石头分为两部分,每个部分的重量和相等。于是本题和LeetCode 416 分割等和子集思路相同。
考虑如果按照背包问题思路,dp数组最后的值代表被粉碎过的所有重量的一半。所以最后只需要返回(sum - dp[-1])*2
总之,本题可以抽象为求容量为sum/2的背包,在尽可能装满后,剩余的容量的两倍。 石头的重量和价值相等。sum为所有石头的重量之和。
如果sum为奇数,对背包容量向下取整。在最后返回结果时➕1.
考虑动态规划五部曲:
- dp数组和下标含义:dp[j]为容量为j的背包能装下的数字的最大和
- 确定递推公式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); - dp数组如何初始化:全部初始化为0
- 确定遍历顺序:从右向左
- 举例推导dp数组:假设stones为[1,2,2]。sum为5,背包容量为2。dp数组依次为
| 0 | 0 | 0 |
|---|---|---|
| 0 | 1 | 1 |
| 0 | 1 | 2 |
| 0 | 1 | 2 |
| 所以剩余容量是0,但sum是奇数,所以返回1. |
解法
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int i : stones) {
sum += i;
}
int[] dp = new int[sum / 2 + 1];
for (int i = 0; i < dp.length; i++) {
dp[i] = 0;
}
for (int i = 0; i < stones.length; i++) {
for (int j = dp.length-1; j >= 0; j--) {
if (j >= stones[i]) {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
}
int left = sum / 2 - dp[dp.length-1];
if (sum % 2 == 1) {
return left * 2 + 1;
}
return left * 2;
}
}
LeetCode 494 目标和
思路
设取正的数总和为pos,取负的数为neg,那么neg+pos = sum,即neg=sum-pos 而pos-neg = target。所以pos = (target + sum) / 2,而pos确定了neg也会确定。 所以把本题转化为背包问题,问装满容量为(target + sum) / 2的背包有几种方法。
考虑如果target+sum是奇数,就不存在满足题目的组合。直接返回0. 如果target是负数,可以想到反转全部正负号就可以得到target绝对值得组合。所以对target取绝对值,结果相同
使用二维dp数组
考虑动态规划五部曲:
- dp数组和下标含义:
dp[i][j]表示用下标为0-i的num装满容量为j的背包有几种方法 - 确定递推公式:
dp[i][j] = dp[i-1][j]+dp[i-1][j-nums[i]];.前者是不装nums[i]的方法数量,后者是装入nums[i]的方法数量。如果无法装入nums[i],就直接继承前者 - dp数组如何初始化:
dp[0][0]一定为1.把第一行其余初始化为0。如果存在j==nums[0],dp[0][j]➕1。 - 确定遍历顺序:从左到右,从上到下
- 举例推导dp数组
使用一维dp数组
考虑动态规划五部曲:
- dp数组和下标含义:
dp[j]表示装满容量为j的背包有几种方法 - 确定递推公式:
dp[j] = dp[j]+dp[j-nums[i]];.前者是不装nums[i]的方法数量,后者是装入nums[i]的方法数量。如果无法装入nums[i],就直接继承前者 - dp数组如何初始化:
dp[0]一定为1.其余为0. - 确定遍历顺序:从右到左
- 举例推导dp数组
解法
使用二维dp数组
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int i : nums) {
sum += i;
}
target = Math.abs(target);
if ((sum + target) % 2 == 1) {
return 0;
}
int capacity = (sum + target) / 2;
int[][] dp = new int[nums.length][capacity+1];
// 初始化
for (int i = 0; i < dp[0].length; i++) {
if (i == 0) {
dp[0][0] = 1;
}
if (i == nums[0]) {
dp[0][i] += 1;
}
}
// 递推
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j <= capacity; j++) {
if (j >= nums[i]) {
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]];
}
else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[nums.length-1][capacity];
}
}
使用一维dp数组
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int i : nums) {
sum += i;
}
target = Math.abs(target);
if ((sum + target) % 2 == 1) {
return 0;
}
int capacity = (sum + target) / 2;
int[] dp = new int[capacity+1];
// 初始化
dp[0] = 1;
// 递推
for (int i = 0; i < nums.length; i++) {
for (int j = capacity; j >= 0; j--) {
if (j >= nums[i]) {
dp[j] = dp[j] + dp[j-nums[i]];
}
}
}
return dp[capacity];
}
}
LeetCode 474 一和零
思路
本题乍一看要满足两个条件,联想到多重背包。但回顾定义,多重背包问题的定义是,每个物品的数量不同。而本题中每个01字符串都只有一个,所以其实仍然是01背包。只是需要考虑两个维度。
考虑动态规划五部曲:
- dp数组和下标含义:
dp[i][j]表示最多含有i个0,j个1的最大子集长度 - 确定递推公式:设k是对strs的遍历索引。
dp[i][j] = max(dp[i][j], dp[i-zero[k]][j-one[k]])+1 - dp数组如何初始化:考虑dp的定义,在未开始遍历前,没有str会被考虑放入,全部初始化为0.
- 确定遍历顺序:从右到左,从下到上。
- 举例推导dp数组
解法
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
// 提取strs中的01个数
int[] zero = new int[strs.length];
int[] one = new int[strs.length];
for (int i = 0; i < strs.length; i++) {
for (int j = 0; j < strs[i].length(); j++) {
if (strs[i].charAt(j) == '0') {
zero[i]++;
}
else {
one[i]++;
}
}
}
// m-->0 n-->1
int[][] dp = new int[m+1][n+1];
for (int k = 0; k < strs.length; k++) {
for (int i = m; i >= 0; i--) {
for (int j = n; j >= 0; j--) {
if (i >= zero[k] && j >= one[k]) {
dp[i][j] = Math.max(dp[i][j], dp[i-zero[k]][j-one[k]]+1);
}
}
}
}
return dp[m][n];
}
}
今日收获总结
今日学习时长5小时,今天的背包问题有很多共性但也有一些挑战