LeetCode HOT100 - 戳气球
区间 DP
一开始会想到一些贪心的思路,比如找最大值,或者找中间值
但是从小样本比如 2、3 个出发就会发现似乎没有一个固定的规律,因为和具体的数值或者说左右邻居都有关
于是难点就在于这个修改后对后续状态是有影响的,因此正向就不好做了,因为我们不知道它对未来状态的影响,或者相当于我们要知道枚举现在选的这个后的所有未来情况,才能知道当前怎么选
不过这个反过来说就是如果我们有所有后续的情况,我们就能知道现在要选哪个,或者说我们对选了这个之后的情况是确定的,那么我们就能转移
所以逆向做,考虑最后选择哪一个,因为最后一个被之前所选择的影响,但是换个方向想,知道最后一个,那么前面被选了哪些是确定了的
假设现在某个区间 (l, r) 之间的气球都要被戳掉,并且:
l和r这两个位置的气球还在- 最后才戳中间某个气球
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 它的收益:
枚举所有 k,取最大值:
初始情况,或者说 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];
}
};

浙公网安备 33010602011771号