题目描述

✅ 1049. 最后一块石头的重量 II

image-20260929000619410

image-20260929000619411

题意分析

每次任选两块石头相撞:重量相同则一起消失,不同则留下两者重量差的一块。不断操作到至多一块石头,求能够实现的最小剩余重量,没有剩余时返回零。

每次选哪两块由我们决定,不能固定选择当前最重的两块。石头即使重量相同也仍是不同的可选对象,每一块原石只能参与一次分组归属,不能重复使用它的重量。

解法:0/1 背包接近半和

核心思路

[!blue]

相撞本质上做减法。把最终残块反向展开,每个原石重量都会带一个正号或负号,因此最终结果可写成两组原石总重之差的绝对值。这意味着任何碰撞结果,都不会小于所有分组中能得到的最小差。

还需要说明最优分组差确实能通过碰撞实现。先按最优分组给石头分两边,只拿不同边的残块相撞;较重一边保留差值残块,相等则一起消失。每次操作都保持两边的总重差,且每个残块都代表一组原石的带符号差。

最终某一边为空。若另一边仍有多块正重量残块,总重为最优差 D,取其中一块重量 r,有 0 < r < D。将这个残块代表的全部原石正负号反转,会得到合法的新分组,差变为 abs(D - 2r) < D,与原分组最优矛盾。因此最优分组最终只能留下至多一块,最小分组差与最小碰撞结果相等。

设总重量为 S,较轻组重量为 x <= S / 2,两组差就是 S - 2x。要让差最小,应找不超过一半的最大可达子集和,而不是只判断能否恰好平分。

令 dp[cap] 表示从已经处理的石头中选若干块、总重不超过 cap 时能达到的最大重量。处理石头 stone 时,可以不选,保留旧值;也可以选,得到 dp[cap - stone] + stone,两者取最大。

每块石头只能使用一次,所以容量从大到小更新。读取较小容量 cap - stone 时,它还属于处理当前石头之前的状态,保证不会重复选同一块。全部处理后返回 S - 2 * dp[S / 2]。

解题步骤

  1. 计算总重 sum,令背包容量 target = sum / 2。
  2. 创建全零数组 dp,表示只选空集时各容量下可达重量均为零。
  3. 逐块处理石头,容量从 target 倒序到当前重量。
  4. 更新 dp[cap] = max(dp[cap], dp[cap - stone] + stone)。
  5. 用总重减去两倍最佳半组重量,返回最小差。

代码实现

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;

        for (int stone : stones) {
            sum += stone;
        }

        int target = sum / 2;
        // 每个容量记录不超过它的最大可达重量,未必恰好装满
        int[] dp = new int[target + 1];

        for (int stone : stones) {
            // 容量倒序保证每块石头只选一次
            for (int cap = target; cap >= stone; cap--) {
                dp[cap] = Math.max(dp[cap], dp[cap - stone] + stone);
            }
        }

        // 另一组重量是总和减所选组,最终取两组差
        return sum - 2 * dp[target];
    }
}
func lastStoneWeightII(stones []int) int {
    sum := 0
    for _, stone := range stones {
        sum += stone
    }

    target := sum / 2
    // 每个容量记录不超过它的最大可达重量,未必恰好装满
    dp := make([]int, target+1)
    for _, stone := range stones {
        // 容量倒序保证每块石头只选一次
        for cap := target; cap >= stone; cap-- {
            candidate := dp[cap-stone] + stone
            if candidate > dp[cap] {
                dp[cap] = candidate
            }
        }
    }
    // 另一组重量是总和减所选组,最终取两组差
    return sum - 2*dp[target]
}

复杂度分析

  • 时间复杂度:$O(n(T+1))$,其中 $n$ 为石头数量,$T=\lfloor S/2\rfloor$ 为背包容量,每块石头至多扫描整段容量。
  • 空间复杂度:$O(T+1)$,只保存一行容量状态。

关键点总结

[!green]

  • 将碰撞顺序问题转成正负分组,最优分组差可以由跨组碰撞实现。
  • 只找不超过半和的最大子集和,另一组自动由未选石头组成。
  • 状态含义是“不超过容量时的最大可达重量”,不要求恰好装满。
  • 倒序读取旧状态,将同一块石头的使用次数限制为一次。

易错点总结

[!yellow]

  • 正序更新会读取本轮刚更新的较小容量,等价于允许重复使用同一块石头。
  • 将容量向上取整后仍用 S - 2x,可能选到超过半和的组而得到负结果。
  • 只计算 S - x 得到的是另一组重量,还需要再减去所选组重量。
  • 总重为奇数并不表示无解,它只意味着不可能完全抵消为零,仍应求最小正差。
  • 只模拟每次撞最大的两块,限制了本题允许的选择,不能保证得到最优剩余重量。

相似题目

题目 难度 关联与区别
416. 分割等和子集 中等 通过给石头分配正负组转成子集和,本题找最接近总和一半的可达值,而非只判断恰好平分。
956. 最高的广告牌 困难 同样用两组差值建模,原题允许舍弃材料并最大化等高,本题全部石头参与并最小化剩余差。
494. 目标和 中等 用容量动态规划表示可达和或组合数;本题尽量把总重量划分得接近一半,该题将正负分配转为指定和子集计数。
474. 一和零 中等 用容量动态规划表示可达和或组合数;本题尽量把总重量划分得接近一半,该题把容量扩展为零和一的两维预算。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/90090811
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!