LeetCode HOT100 - 戳气球

区间 DP

一开始会想到一些贪心的思路,比如找最大值,或者找中间值

但是从小样本比如 2、3 个出发就会发现似乎没有一个固定的规律,因为和具体的数值或者说左右邻居都有关

于是难点就在于这个修改后对后续状态是有影响的,因此正向就不好做了,因为我们不知道它对未来状态的影响,或者相当于我们要知道枚举现在选的这个后的所有未来情况,才能知道当前怎么选

不过这个反过来说就是如果我们有所有后续的情况,我们就能知道现在要选哪个,或者说我们对选了这个之后的情况是确定的,那么我们就能转移

所以逆向做,考虑最后选择哪一个,因为最后一个被之前所选择的影响,但是换个方向想,知道最后一个,那么前面被选了哪些是确定了的

假设现在某个区间 (l, r) 之间的气球都要被戳掉,并且:

  • lr 这两个位置的气球还在
  • 最后才戳中间某个气球 k

那么当 k 最后被戳时:

  • 它左边只剩下 l
  • 它右边只剩下 r

这一刻的收益是确定的:\( val[l] \times val[k] \times val[r] \)

而且在 k 之前:

  • (l, k) 中间的气球已经都处理完
  • (k, r) 中间的气球也都处理完

于是问题就被拆成了两个独立子问题。

这就是区间 DP 的核心突破点,知道子区间(子问题)的解,来得到更大区间的解

实际上把这个当作是第一个做也是可以的,相当于分治解决各自区间,但是最后都是要自底向上传递回来信息的,类似于树 dp 的操作(因为按照选择的点拆开区间来看就是一个树,比如笛卡尔树),这就是区间 dp

状态定义:dp[l][r] 表示戳掉开区间 (l, r) 内所有气球,能获得的最大硬币数

注意是开区间,不包含 l 和 r,这是因为当我们枚举最后一个戳的气球 k 时:

  • k 一定在 (l,r) 里面
  • 戳掉 k 时,它的左右邻居正好就是 l 和 r

这样最后一个的贡献才明确

接着考虑状态转移显然,对于 (l, r) 中的 k 它的收益:

\[dp[l][k] + dp[k][r] + val[l]\times val[k]\times val[r] \]

枚举所有 k,取最大值:

\[dp[l][r] = \max_{l<k<r}\{dp[l][k]+dp[k][r]+val[l]\times val[k]\times val[r]\} \]

初始情况,或者说 dp 入口是 (l, r) 之间没有元素,也就是 r - l = 1,这时候 dp 值为 0

同时因为 dp[l][r] 依赖于 dp[l][k] 和 dp[k][r],也就是两个更小的区间,所以要从小区间向大区间处理,因此遍历顺序是按照区间长度来做

区间大小很小,我们每次遍历 l, r, k 也同样能通过

区间 DP 的特征就是 一个区间的最优解,可以通过枚举这个区间中的某个“分割点 / 最后操作点”来拆成两个更小区间,这类区间 DP 里,很多时候“最后一步”就是分界点。
状态对应一段连续区间,且这段区间的答案可以由更小的子区间组合出来

class Solution {
public:
    int maxCoins(vector<int>& nums) {
        int n = nums.size();
        vector<int> val(n + 2, 1);
        for (int i = 0; i < n; i++) {
            val[i + 1] = nums[i];
        }
        vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
        for (int len = 3; len <= n + 2; len++) {
            for (int l = 0; l + len - 1 < n + 2; l++) {
                int r = l + len - 1;
                for (int k = l + 1; k < r; k++) {
                    dp[l][r] = max(dp[l][r], dp[l][k] + dp[k][r] + val[l] * val[k] * val[r]);
                }
            }
        }
        return dp[0][n + 1];
    }
};
posted @ 2026-03-31 21:10  rdcamelot  阅读(17)  评论(0)    收藏  举报