一、题目内容 中等
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
示例1:
输入:stones = [2,7,4,1,8,1] 输出:1 解释: 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1], 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1], 组合 2 和 1,得到 1,所以数组转化为 [1,1,1], 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
示例2:
输入:stones = [31,26,33,21,40] 输出:5
示例3:
输入:stones = [1,2] 输出:1
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 100
二、解题思路
当给我们一个 stones 数组时,我们将其一分为二,看成两个整体,尽量分成两个等重的数组。
将这两个整体看成 x、y,按照题目规则进行相消,结果就是最小。为什么可以看成两个整体呢? 本质是,A、B 两组集合中的元素相消。 因为 所以分拆成小元素相减的结果,肯定也等于整体相减的结果,不必纠结。
那么问题就化归为,怎么将 stones 尽可能分成等重的数组,这跟 6. 分割等和子集(416)思路如出一辙。
确定了如下四点,才能把01背包问题套到本题上来。
- 背包的体积为 sum / 2,取整数
- 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
- 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
- 背包中每一个元素是不可重复放入。
以上分析完,我们就可以套用 01 背包,来解决这个问题了。
动规五部曲分析如下:
- 确定dp数组以及下标的含义
01背包中,dp[j] 表示: 容量为 j 的背包,所背的物品价值可以最大为 dp[j]。
套到本题,dp[j] 表示 背包总容量是 j,最大可以凑成 j 的子集总和为 dp[j]。
jmax = sum / 2
- 确定递推公式
01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
本题,相当于背包里放入石头,那么石头 i 的重量是 nums[i],其价值也是 nums[i]。
所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
- dp 数组如何初始化
在 01 背包,一维 dp 如何初始化,已经讲过,从 dp[j] 的定义来看,首先 dp[0] 一定是 0。
如果题目给的价值都是正整数,那么非 0 下标都初始化为 0 就可以了。
如果题目给的价值有负数,那么非 0 下标就要初始化为负无穷。
这样才能让 dp 数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
- 确定遍历顺序
如果使用一维 dp 数组,物品遍历的 for 循环放在外层,遍历背包的 for 循环放在内层,且内层 for 循环倒序遍历!
for (let i = 0; i < nums.length; i++) {
for (let j = mid; j >= nums[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i])
}
}
-
三、具体代码
/**
* @param {number[]} stones
* @return {number}
*/
var lastStoneWeightII = function (stones) {
const sum = stones.reduce((pre, cur) => pre + cur, 0)
const mid = Math.floor(sum / 2)
const dp = new Array(mid + 1).fill(0)
// 遍历物品
for (let i = 0; i < stones.length; i++) {
// 遍历背包
for (let j = mid; j >= stones[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i])
}
}
return sum - 2 * dp[mid]
};
四、其他解法