题意
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

提示:
1 <= stones.length <= 30
1 <= stones[i] <= 100
思路分析
本题和 分割等和子集 实际上一模一样。在分割等和子集中,是将给定的nums数组分为两个子集,使得两个子集中各自元素和相等,它求的是是否能把这个 容量为nums数组元素和的一半的背包 “装满”。那为什么说本题和分割等和子集一模一样?
要求得最后一块石头的重量且该重量是最优(小)值,实际上就是要将石头分成质量 尽量接近或者相等 的两堆:

从图中可看到,12 为数组stone的所有元素和的一半,即中间值,如果第一堆石头重量和与第二堆石头重量和相等,那么n == sum/2,若不相等,则n会在sum/2的两侧,那么n 与sum/2的差值即为最后一块石头的重量,求得的差值的最小值即为最优值,换言之,如果在stone数组中选取石头使得这些被选取的石头的重量之和 最接近或者等于 sum/2,那么n与sum/2的差值即为题目所求。
有一点需要注意的是,由于当stone数组的元素和为奇数时,除2折半后为向下取整,如上图的例子中求得sum=23,则sum/2 = 11,那么在求答案时,2 × dp[ sum/2 ] < sum,故而最后返回sum - 2*dp[sum/2],即相当于 (sum - dp[sum/2]) - dp[sum/2],第二个等号左边代表一堆石头的重量和、右边代表一堆石头的重量和。
因此本题和分割等和子集是一样的,只不过在分割等和子集中求的是 是否能装满背包,本题求的是 背包最多能装多少。
动态规划五部曲
确定dp数组含义
dp[ j ]代表从stone数组中选择石头,所选取的石头的重量和的最大值。细讲则为:从stone[0] ~ stone[i]中选取石头放入重量限重为 j 的容器中,容器中石头的重量和的最大值为 dp[j]。
确定递推公式
和分割等和子集类似,本题中 物品的重量weight和价值value 都为 石头的重量,01背包中各个量的对应关系在分割等和子集中已经详细解说,此不赘述。这里石头的重量 与 容器的限重 相对应的、石头的 重量 与dp数组的含义 也是相对应的。
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
初始化dp数组
本题使用一维dp数组,因此可以不必初始化。
遍历顺序
遍历顺序与01背包相同,使用一维数组情况下,先遍历物品再遍历背包,即先遍历石头stone数组再遍历容器,且容器的遍历顺序是从大到小倒叙的。
for (int i = 0; i < stones.length; i++) {
for (int j = target; j > 0; j--) {
if (j >= stones[i]) {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
}
举例推导dp数组
举例中每个列出来的数组代表每次遍历完容器(背包)后dp数组的情况。

完整Java代码实现
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int n : stones) sum += n;
int target = sum / 2;
int[] dp = new int[sum + 1];
for (int i = 0; i < stones.length; i++) {
for (int j = target; j > 0; j--) {
if (j >= stones[i]) {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
}
return sum - 2 * dp[target];
}
}
《代码随想录》刷题记