[LeetCode] 546. Remove Boxes
grandyang opened this issue · comments
You are given several boxes
with different colors represented by different positive numbers.
You may experience several rounds to remove boxes until there is no box left. Each time you can choose some continuous boxes with the same color (i.e., composed of k
boxes, k >= 1
), remove them and get k * k
points.
Return the maximum points you can get.
Example 1:
Input: boxes = [1,3,2,2,2,3,4,3,1]
Output: 23
Explanation:
[1, 3, 2, 2, 2, 3, 4, 3, 1]
----> [1, 3, 3, 4, 3, 1] (3*3=9 points)
----> [1, 3, 3, 3, 1] (1*1=1 points)
----> [1, 1] (3*3=9 points)
----> [] (2*2=4 points)
Example 2:
Input: boxes = [1,1,1]
Output: 9
Example 3:
Input: boxes = [1]
Output: 1
Constraints:
1 <= boxes.length <= 100
1 <= boxes[i] <= 100
刚开始看这道题的时候,感觉跟之前那道 Zuma Game 很像,于是就写了一个暴力破解的方法,结果 TLE 了。无奈之下只好上网搜大神们的解法,又看了 fun4LeetCode 大神写的帖子,之前那道 Reverse Pairs 就是参考的 fun4LeetCode 大神的帖子,惊为天人,这次又是这般精彩,大神请收下我的膝盖。那么下面的解法就大部分参考 fun4LeetCode 大神的帖子来讲解吧。在之前帖子 Reverse Pairs 的讲解中,大神归纳了两种重现模式,这里也试着看能不能套用上。对于这种看来看去都没思路的题来说,抽象建模的能力就非常的重要了。对于题目中的具体场景啊,具体代表的东西都可忽略不看,这样能帮助我们接近问题的本质,这道题的本质就是一个数组,每次消去一个或多个数字,并获得相应的分数,让求最高能获得的分数。而之前那道 Zuma Game 也是给了一个数组,让往某个位置加数字,使得相同的数字至少有3个才能消除,二者是不是很像呢,但是其实解法却差别很大。那道题之所以暴力破解没有问题是因为数组的长度和给定的数字个数都有限制,而且都是相对较小的数,那么即便遍历所有情况也不会有太大的计算量。而这道题就不一样了,既然不能暴力破解,那么对于这种玩数组和子数组的题,刷题老司机们都会优先考虑用动态规划 Dynamic Programming 来做吧。既然要玩子数组,肯定要限定子数组的范围,那么至少应该是个二维的 dp 数组,其中 dp[i][j] 表示在子数组 [i, j] 范围内所能得到的最高的分数,那么最后返回 dp[0][n-1] 就是要求的结果。
那么对于 dp[i][j],如果移除 boxes[i] 这个数字,那么总得分应该是 1 + dp[i+1][j],但是通过分析题目中的例子,能够获得高积分的 trick 是,移除某个或某几个数字后,如果能使得原本不连续的相同数字变的连续是坠好的,因为同时移除的数字越多,那么所得的积分就越高。那么假如在 [i, j] 中间有个位置m,使得 boxes[i] 和 boxes[m] 相等,那么就不应该只是移除 boxes[i] 这个数字,而是还应该考虑直接移除 [i+1, m-1] 区间上的数,使得 boxes[i] 和 boxes[m] 直接相邻,那么获得的积分就是 dp[i+1][m-1],那么剩余了什么,boxes[i] 和 boxes[m, j] 区间的数,此时无法处理子数组 [m, j],因为有些信息没有包括在 dp 数组中, 此类的题目归纳为不自己包含的子问题,其解法依赖于一些子问题以外的信息 。这类问题通常没有定义好的重现关系,所以不太容易递归求解。为了解决这类问题, 我们需要修改问题的定义,使得其包含一些外部信息,从而变成自包含子问题 。
那么对于这道题来说,无法处理 boxes[m, j] 区间是因为其缺少了关键信息,我们不知道 boxes[m] 左边相同数字的个数k,只有知道了这个信息,那么m的位置才有意义,所以 dp 数组应该是一个三维数组 dp[i][j][k],表示区间 [i, j] 中能获得的最大积分,当 boxes[i] 左边有k个数字跟其相等,那么目标就是要求 dp[0][n-1][0] 了,而且也能推出 dp[i][i][k] = (1+k) * (1+k) 这个等式。那么来推导重现关系,对于 dp[i][j][k],如果移除 boxes[i],那么得到 (1+k)*(1+k) + dp[i+1][j][0]。对于上面提到的那种情况,当某个位置m,有 boxes[i] == boxes[m] 时,也应该考虑先移除 [i+1,m-1] 这部分,得到积分 dp[i+1][m-1][0],然后再处理剩下的部分,得到积分 dp[m][j][k+1],这里k加1点原因是,移除了中间的部分后,原本和 boxes[m] 不相邻的 boxes[i] 现在相邻了,又因为二者值相同,所以k应该加1,因为k的定义就是左边相等的数字的个数。讲到这里,那么DP方法最难的状态转移方程也就得到了,那么代码就不难写了,需要注意的是,这里的 C++ 的写法不能用 vector 来表示三维数组,好像是内存限制超出,只能用C语言的写法,由于C语言数组的定义需要初始化大小,而题目中说了数组长度不会超 100,所以就用 100 来初始化,参见代码如下:
解法一:
class Solution {
public:
int removeBoxes(vector<int>& boxes) {
int n = boxes.size();
int dp[100][100][100] = {0};
return helper(boxes, 0, n - 1, 0, dp);
}
int helper(vector<int>& boxes, int i, int j, int k, int dp[100][100][100]) {
if (j < i) return 0;
if (dp[i][j][k] > 0) return dp[i][j][k];
int res = (1 + k) * (1 + k) + helper(boxes, i + 1, j, 0, dp);
for (int m = i + 1; m <= j; ++m) {
if (boxes[m] == boxes[i]) {
res = max(res, helper(boxes, i + 1, m - 1, 0, dp) + helper(boxes, m, j, k + 1, dp));
}
}
return dp[i][j][k] = res;
}
};
下面这种写法是上面解法的迭代方式,但是却有一些不同,这里需要对 dp 数组的部分值做一些初始化,将每个数字的所有k值的情况的积分都先算出来,然后在整体更新三维 dp 数组的时候也很有意思,并不是按照原有的顺序更新,而是块更新,先更新 dp[1][0][k], dp[2][1][k], dp[3][2][k]....,再更新 dp[2][0][k], dp[3][1][k], dp[4][2][k]...., 再更新 dp[3][0][k], dp[4][1][k], dp[5][2][k]....,之前好像也有一道是这样区域更新的题,但是博主想不起来是哪一道了,以后想起来了再来补充吧,参见代码如下:
解法二:
class Solution {
public:
int removeBoxes(vector<int>& boxes) {
int n = boxes.size();
int dp[n][n][n] = {0};
for (int i = 0; i < n; ++i) {
for (int k = 0; k <= i; ++k) {
dp[i][i][k] = (1 + k) * (1 + k);
}
}
for (int t = 1; t < n; ++t) {
for (int j = t; j < n; ++j) {
int i = j - t;
for (int k = 0; k <= i; ++k) {
int res = (1 + k) * (1 + k) + dp[i + 1][j][0];
for (int m = i + 1; m <= j; ++m) {
if (boxes[m] == boxes[i]) {
res = max(res, dp[i + 1][m - 1][0] + dp[m][j][k + 1]);
}
}
dp[i][j][k] = res;
}
}
}
return n == 0 ? 0 : dp[0][n - 1][0];
}
};
Github 同步地址:
类似题目:
参考资料:
https://leetcode.com/problems/remove-boxes/
https://leetcode.com/problems/remove-boxes/discuss/101312/memoization-dfs-c
https://leetcode.com/problems/remove-boxes/discuss/101310/java-top-down-and-bottom-up-dp-solutions
现在的算法都很慢, 原因是没有必要在相同的数字中间分开, 在leetcode上看到应该增加一个vector counts, 来记录连续的相同数字的个数, 在现有的testcases情况下, 速度会加快10~15倍