Day36-动态规划,leetcode1049,494,474
- 最后一块石头的重量 II
- 有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
- 每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
- 如果 x == y,那么两块石头都会被完全粉碎;
- 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
- 最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
- 思路
- 01背包应用:尽量装满背包
- 把总石头分成重量总和近似相等的两堆,让重量相同的两堆相撞,这样相撞后的值最小
- 一堆的石头重量是sum,尽可能拼成 重量为 sum / 2 的石头堆。 这样剩下的石头堆也是 尽可能接近 sum/2 的重量。 那么此时问题就是有一堆石头,每个石头都有自己的重量,是否可以 装满 最大重量为 sum / 2的背包。
- 1.确定dp数组定义及下标的含义:dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]。
- 2.确定递推公式:第j个石头装还是不装,取大者,dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])
- 3.dp数组如何初始化:1 <= stones.length <= 30,1 <= stones[i] <= 100,所以最大重量就是30 * 100 。target是最大重量的一半,所以dp数组开到1500大小就可以了。也可以把石头遍历一遍,计算出石头总重量 然后除2,得到dp数组的大小
- 4.确定遍历顺序:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历。
- 5.举例推导dp数组,打印dp数组:
/**
* 本质是把石头分成两堆,重量差最小。
* 转化为 01 背包问题,求最接近 sum/2 的一堆。
* 最终返回两堆重量的差值。
*/
var lastStoneWeightII = function(stones) {
// 计算总重量和目标重量
// sum:所有石头的总重量。
// target:我们希望分成两堆,每堆尽量接近 sum/2,这样剩下的差值最小。
let sum = stones.reduce((a, b) => a + b, 0);
let target = Math.floor(sum / 2);
// 定义dp数组,dp[j] 表示:在容量为 j 的背包里,最多能装的石头总重量是多少(01背包问题)
let dp = new Array(target + 1).fill(0);
// 01背包
/**
* 外层循环遍历每块石头。
内层循环倒序遍历背包容量,从 target 到 stones[i]。
状态转移方程:dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]),表示当前容量 j,要么不选当前石头(dp[j]),要么选当前石头(dp[j - stones[i]] + stones[i]),取最大值。
*/
for (let i = 0; i < stones.length; i++) {
for (let j = target; j >= stones[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
// 返回最小剩余重量
/**
* dp[target] 是最接近 sum/2 的一堆石头的总重量。
另一堆就是 sum - dp[target]。
两堆的差值是 sum - 2 * dp[target],即剩下的最小可能重量。
*/
return sum - dp[target] - dp[target];
};
- 目标和
- 给你一个非负整数数组 nums 和一个整数 target 。
- 向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
- 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
- 返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
-
思路
-
01背包应用:装满这个背包有多少种方法
-
二维数组
- 1.确定dp数组定义及下标的含义:dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。
- 2.确定递推公式:不放物品i:即背包容量为j,里面不放物品i,装满有dp[i - 1][j]中方法。放物品i: 即:先空出物品i的容量,背包容量为(j - 物品i容量),放满背包有 dp[i - 1][j - 物品i容量] 种方法。递推公式:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]],j - nums[i] 作为数组下标,如果 j - nums[i] 小于零,说明背包容量装不下 物品i,所以此时装满背包的方法值 等于 不放物品i的装满背包的方法,即:dp[i][j] = dp[i - 1][j];
- 3.dp数组如何初始化:先明确递推的方向, dp[i][j] 是由 上方和左上方推出,那么二维数组的最上行 和 最左列一定要初始化,这是递推公式推导的基础。dp[0][0],装满背包容量为0 的方法数量是1,即 放0件物品。dp[0][j]:只放物品0, 把容量为j的背包填满有几种方法。只有背包容量为 物品0 的容量的时候,方法为1,正好装满。其他情况下,要不是装不满,要不是装不下。所以初始化:dp[0][nums[0]] = 1 ,其他均为0 。表格最左列也要初始化,dp[i][0] : 背包容量为0, 放物品0 到 物品i,装满有几种方法。都是有一种方法,就是放0件物品。即 dp[i][0] = 1。如果有两个物品,物品0为0, 物品1为0,装满背包容量为0的方法有几种。放0件物品
、放物品0、放物品1、放物品0 和 物品1,此时是有4种方法。其实就是算数组里有t个0,然后按照组合数量求,即 2^t 。- 4.确定遍历顺序: 从上到下,从左到右,才能基于之前的数值做推导。先遍历物品(行)还是先遍历背包(列)都可以
- 5.举例推导dp数组,打印dp数组:
/*
1. 问题转化
把加减号问题转化为“选取一部分数,使其和为 bagSize”,即 01 背包问题。
bagSize = (target + sum) / 2,如果不为整数或超界,直接返回 0。
2. dp数组定义
dp[i][j]:前 i 个数,和为 j 的方案数。
3. 初始化
dp[0][0] = 1:前0个数,和为0的方法只有1种(都不选)。
dp[0][nums[0]] = 1:如果第一个数不为0,可以单独选它。
处理前面有0的情况,0可以选或不选,方法数翻倍。
4. 状态转移
不选当前数:dp[i][j] = dp[i-1][j]
选当前数:dp[i][j] += dp[i-1][j-nums[i]](如果 j >= nums[i])
5. 返回结果
dp[nums.length - 1][bagSize],即所有数都用上,和为 bagSize 的方案数。
本题用二维动态规划,转化为“装满背包有多少种方法”,核心是 01 背包计数型动态规划。
*/
function findTargetSumWays(nums, target) {
const sum = nums.reduce((a, b) => a + b, 0);
if (Math.abs(target) > sum) return 0; // 绝对值大于总和,无解
if ((target + sum) % 2 === 1) return 0; // 奇偶性不符,无解
const bagSize = (target + sum) / 2;
// dp[i][j]:前i个数,和为j的方法数
const dp = Array.from({ length: nums.length }, () => Array(bagSize + 1).fill(0));
// 初始化最上行
if (nums[0] <= bagSize) dp[0][nums[0]] = 1;
dp[0][0] = 1;
// 处理前面有0的情况
let numZero = 0;
for (let i = 0; i < nums.length; i++) {
if (nums[i] === 0) numZero++;
dp[i][0] = Math.pow(2, numZero);
}
// 动态规划填表
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j <= bagSize; j++) {
if (nums[i] > j) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
}
}
}
return dp[nums.length - 1][bagSize];
}
- 一维数组
- 1.确定dp数组定义及下标的含义:dp[j],表示:填满j(包括j)这么大容积的包,有dp[j]种方法
- 2.确定递推公式:
- 二维DP数组递推公式: dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
- 去掉维度i 之后,递推公式:dp[j] = dp[j] + dp[j - nums[i]] ,即:dp[j] += dp[j - nums[i]]
- 3.dp数组如何初始化:dp[0]=0,即装满背包为0的方法有一种,放0件物品。
- 4.确定遍历顺序:遍历物品放在外循环,遍历背包在内循环,且内循环倒序(为了保证物品只使用一次)
- 5.举例推导dp数组,打印dp数组:
/**
1. 问题转化
把加减号问题转化为“选取一部分数,使其和为 bagSize”,即 01 背包计数问题。
bagSize = (target + sum) / 2,如果不为整数或超界,直接返回 0。
2. dp数组定义
dp[j]:填满容量为 j 的背包有多少种方法。
3. 初始化
dp[0] = 1:容量为0的方法只有1种(什么都不选)。
4. 状态转移
对每个数,从大到小遍历背包容量 j。
dp[j] += dp[j - nums[i]]:当前容量 j 的方法数等于不选当前数的方法数加上选当前数的方法数。
5. 返回结果
dp[bagSize],即所有方案数。
本题用一维动态规划,核心是 01 背包计数型动态规划,dp[j] 表示和为 j 的方案数。
*/
function findTargetSumWays(nums, target) {
const sum = nums.reduce((a, b) => a + b, 0);
if (Math.abs(target) > sum) return 0; // 绝对值大于总和,无解
if ((target + sum) % 2 === 1) return 0; // 奇偶性不符,无解
const bagSize = Math.floor((target + sum) / 2);
// dp[j]:填满j这么大容积的包,有dp[j]种方法
const dp = new Array(bagSize + 1).fill(0);
dp[0] = 1; // 装满容量为0的方法只有1种(什么都不选)
for (let i = 0; i < nums.length; i++) {
for (let j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
- 一和零
- 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
- 请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
- 如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
- 思路
- 01背包应用:装满这个背包最多有多少个物品,这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。
- 1.确定dp数组定义及下标的含义:dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]
- 2.确定递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1),strs里的字符串有zeroNum个0,oneNum个1
- 3.dp数组如何初始化:物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖
- 4.确定遍历顺序:外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!
- 5.举例推导dp数组,打印dp数组:
/**
1. dp数组定义
dp[i][j] 表示:最多有 i 个 0 和 j 个 1 时,strs 的最大子集长度。
2. 初始化
dp 数组全部初始化为 0,表示没有选任何字符串时子集长度为 0。
3. 遍历物品(字符串)
对每个字符串,统计其中 0 和 1 的数量(zeroNum, oneNum)。
4. 倒序遍历背包容量
外层 i 从 m 到 zeroNum,内层 j 从 n 到 oneNum,倒序是为了保证每个字符串只用一次(01背包)。
状态转移:dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1),表示当前容量下,选或不选当前字符串,取最大子集长度。
5. 返回结果
dp[m][n] 即为最多有 m 个 0 和 n 个 1 时的最大子集长度。
本题是二维 01 背包问题,dp[i][j] 表示容量为 i 个 0、j 个 1 时能选的最大字符串子集数,核心是倒序遍历和状态转移。
*/
function findMaxForm(strs, m, n) {
// dp[i][j]:最多有i个0和j个1的strs的最大子集的大小
const dp = Array.from(Array(m+1), () => Array(n+1).fill(0))
for (const str of strs) { // 遍历每个物品(字符串)
let zeroNum = 0, oneNum = 0;
for (const c of str) {
if (c === '0') zeroNum++;
else oneNum++;
}
// 倒序遍历背包容量,保证每个字符串只用一次
for (let i = m; i >= zeroNum; i--) {
for (let j = n; j >= oneNum; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。

浙公网安备 33010602011771号