1049. 最后一块石头的重量 II (last stone weight ii)

3,635 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第29天,点击查看活动详情

1049. 最后一块石头的重量 II 题目描述:有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

示例1示例2
输入stones = [2,7,4,1,8,1]
输出11
解释
组合 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],这就是最优值。
输入stones = [31,26,33,21,40]
输出55

中规中矩的动态规划

简化此问题:我们在每个元算前添加上 + / - 号之后,将所有元素相加后的值最小是多少?本题就与 # 494. 目标和 有着异曲同工之处。我们假设可将所有的石头分成两个部分,如果我们想让两部分作用后的结果最小,那么这两个部分各自所有元素之和就要逼近 sum/2sum / 2,其中 sumsum 是数组 stonesstones 所有元素之和。因此这道题就转化成了一道传统的 0-1 背包问题,背包容量为 sum/2sum / 2,物品为 stonesstones,试问不超过背包容量的最大承重是多少。

1、确定 dp 状态数组

定义 dp[i][j]dp[i][j] 为在 stones[0,i)stones[0,i) 区间内选择元素装进容量为 jj 的背包后的最大重量,其中 i[0,n],n=stones.lengthi \in [0, n],n=stones.lengthj=sum/2j = sum / 2sumsum 是所有 stonesstones 元素重量之和。

2、确定 dp 状态方程

j<stones[i1]j \lt stones[i - 1] 时,有 dp[i][j]=dp[i1][j]dp[i][j] = dp[i - 1][j];

jstones[i1]j \ge stones[i - 1] 时,有 dp[i][j]=dp[i1][jstones[i1]]+stones[i1]dp[i][j] = dp[i - 1][j - stones[i - 1]] + stones[i - 1]

综上所述,

dp[i][j]={dp[i1][j],j<stones[i1]max(dp[i1][j],dp[i1][jstones[i1]],jstones[i1]dp[i][j] = \begin{cases} dp[i - 1][j], & j \lt stones[i-1] \\ max(dp[i - 1][j],dp[i-1][j-stones[i-1]], & j \ge stones[i-1] \end{cases}

3、确定 dp 初始状态

  • dp[0][j]dp[0][j] 代表在 stones[0,0)stones[0,0) 区间内选择元素装进容量为 jj 的背包后的最大重量,此时没有元素可以选择,故 dp[0][j]=0dp[0][j] = 0

  • dp[i][0]dp[i][0] 代表在 stones[0,i)stones[0,i) 区间内选择元素装进容量为 00 的背包后的最大重量,此时背包容量0,故 dp[i][0]=0dp[i][0] = 0

4、确定遍历顺序

对于0-1背包的二维情况,可以先遍历物品再遍历背包

  • 外层循环遍历物品,从 i=1i = 1 遍历到 i=ni = n

  • 内层循环遍历背包,从 j=1j = 1 遍历到 j=sum/2j = sum / 2

5、确定最终返回值

dp[n][sum/2]dp[n][sum/2] 仅代表的是stones[0,n)stones[0,n) 区间内选择元素装进容量为 sum/2sum/2 的背包后的最大重量,最终的返回值应为 sum2×dp[n][sum/2]sum - 2 \times dp[n][sum/2]

6、代码示例

/**
 * 空间复杂度 O(n*sum),n是stones数组的长度,sum是stones元素之和
 * 时间复杂度 O(n*sum)
 */
 function lastStoneWeightII(stones: number[]): number {
    const n = stones.length;
    const sum = stones.reduce((acc, curr) => acc + curr, 0);
    const weight = ~~(sum / 2);
    const dp = Array.from({ length: n + 1 }, () => new Array(weight + 1).fill(0));

    for (let i = 1; i <= n; i++) {
        const stone = stones[i - 1];
        for (let j = 1; j <= weight; j++) {
            if (j < stone) {
                dp[i][j] = dp[i - 1][j];
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - stone] + stone);
            }
        }
    }

    return sum - 2 * dp[n][weight];
};

0-1 背包问题可以压缩空间:

  1. 定义 dp[j]dp[j] 是装入容量为 jj 的背包的最大重量。

  2. 转移方程为 dp[j]=max(dp[j],dp[jstone]+stone)dp[j] = max(dp[j], dp[j - stone] + stone)

  3. 初始化状态 dp[j]=0dp[j] = 0

  4. 先遍历物品,再 倒序 遍历背包。

  5. 最终返回值 sum2×dp[sum/2]sum - 2 \times dp[sum/2]

/**
 * 空间复杂度 O(n*sum),n是stones数组的长度,sum是stones元素之和
 * 时间复杂度 O(sum)
 */
function lastStoneWeightII(stones: number[]): number {
    const sum = stones.reduce((acc, curr) => acc + curr, 0);
    const weight = ~~(sum / 2);
    const dp = new Array(weight + 1).fill(0);

    for (const stone of stones) {
        for (let j = weight; j >= stone; j--) {
            dp[j] = Math.max(dp[j], dp[j - stone] + stone);
        }
    }

    return sum - 2 * dp[weight];
};

参考

# 重识背包问题(上)

# 重识背包问题(下)

# 重识动态规划