0546-移除盒子

Raphael Liu Lv10

给出一些不同颜色的盒子 boxes ,盒子的颜色由不同的正数表示。

你将经过若干轮操作去去掉盒子,直到所有的盒子都去掉为止。每一轮你可以移除具有相同颜色的连续 k 个盒子(k >= 1),这样一轮之后你将得到 `k

  • k` 个积分。

返回 你能获得的最大积分和

示例 1:

**输入:** boxes = [1,3,2,2,2,3,4,3,1]
**输出:** 23
**解释:**
[1, 3, 2, 2, 2, 3, 4, 3, 1] 
----> [1, 3, 3, 4, 3, 1] (3*3=9 分) 
----> [1, 3, 3, 3, 1] (1*1=1 分) 
----> [1, 1] (3*3=9 分) 
----> [] (2*2=4 分)

示例 2:

**输入:** boxes = [1,1,1]
**输出:** 9

示例 3:

**输入:** boxes = [1]
**输出:** 1

提示:

  • 1 <= boxes.length <= 100
  • 1 <= boxes[i] <= 100

方法一:动态规划

我们很容易陷入这样一个错误的思路:用 f(l, r) 来表示移除区间 [l, r] 内所有的盒子能得到的最大积分,然后去探索某一种移除盒子的策略来进行状态转移。而实际上,我们并不能直接使用起始节点和结束节点决定最大分数,因为这个分数并不只依赖于子序列,也依赖于之前的移动对当前数组的影响,这可能让最终的子序列不是一个连续的子串。比如 { 3, 4, 2, 4, 4 \,如果先把 2 移除,3 个 4 会合并在一起,对答案的贡献是 3^2 = 9,如果先移除左右两边的 4 再移除 2 这里 3 个 4 的贡献就是 1^2 + 2^2 = 5,最优的办法当然是先取走 2,但是这样剩下的 3 个 4 其实并不是原串的某个连续子串。

那么正确的思路是什么呢?

我们可以换一种思路,用 f(l, r, k) 表示移除区间 [l, r] 的元素 a_l, a_{l + 1}, a_{l + 2} \cdots a_r加上该区间右边等于 a_r 的 k 个元素组成的这个序列的最大积分。例如序列 { 6, 3, 6, 5, 6, 7, 6, 6, 8, 6 \,l = 0,r = 4,那么 f(l, r, 3) 对应的元素就是 { {\color{red}[6, 3, 6, 5, 6]}, 7, {\color{red}6}, {\color{red}6}, 8, {\color{red}6} \ 中标记为红色的部分。f(l, r, k) 的定义是移除这个红色的序列获得的最大积分。请注意此时我们约定 7 和 8 已经先被移除,所以在这个状态下我们可以认为最后四个 6 是连续的,也就是说实际上待删除的序列是这样的:{ [6, 3, 6, 5, 6], 6, 6, 6 \,此时我们可以有这样一些策略来移除盒子:

  • { {\color{orange}[6, 3, 6, 5}, {\color{red} 6]}, 7, {\color{red}6, 6}, 8, {\color{red}6} \,删除后面的四个 6,再删除前面的这个区间,这样获得的分数为 f(0, 3, 0) + 4^2。
  • { {\color{orange}[6, 3}, {\color{red}6]}, [5], {\color{red} 6}, 7, {\color{red}6, 6}, 8, {\color{red}6} \,删除一个单独的 a_3(即 5),分数是 f(3, 3, 0);然后问题就变成了删除区间 [0, 2] 以及这个区间后面和 a_2 相同的 4 个数,分数是 f(0, 2, 4);这样获得的分数是 f(0, 2, 4) + f(3, 3, 0)。
  • { {\color{orange}[ }{\color{red}6]},[3, 6, 5], {\color{red} 6}, 7, {\color{red}6, 6}, 8, {\color{red}6} \,删除 a_1、a_2、a_3,分数为 f(1, 3, 0);之后再删除区间 [0, 0] 和这个区间后和 a_1 相同的 4 个数,分数是 f(0, 0, 4);这样获得的分数是 f(0, 0, 4) + f(1, 3, 0)。

这个就是我们转移的时候使用的策略,我们可以推导出这样的动态规划转移方程:

f(l, r, k) = \max \left{ f(l, r - 1, 0) + (k + 1)^2, \max_{i = l}^{r - 1} { [f(l, i, k + 1) + f(i + 1, r - 1, 0)] \times { \color{red} \epsilon (a_i = a_r)} } \right}

  • f(l, r - 1, 0) + (k + 1)^2 代表我们把 a_r 和后面的 k 个数一起删除,再删除 [l, r - 1] 这个区间。也就是说,当我们删除 f(l, r, k) 对应的数字的时候,我们可以考虑先删除 a_r 和后面 k 个与 a_r 相等的数,即一共 k + 1 个数,得分为 (k+1)^2,再删除 [l, r - 1] 这个区间,由于第 r - 1 个位置后面所有的数字都删除了,所以这里不用继续考虑后面的数字中和 a_{r - 1 相等的那些数字,所以这里的分数是 f(l, r - 1, 0)。

  • [f(l, i, k + 1) + f(i + 1, r - 1, 0)] \times { \color{red} \epsilon (a_i = a_r) 代表当 a_i = a_r (l \leq i < r) 的时候,考虑先删掉 [i + 1, r - 1] 这个区间,然后再删除 [l, i] 区间和后面的 k + 1 个和 a_r 相等的数构成的序列,其中 \epsilon(x) 为选择函数:

\epsilon(x) = \left {
\begin{aligned}
& 1 ,& x = {\rm True} & \
& 0 ,& x = {\rm False}
\end{aligned}
\right .

这里我们只考虑和 a_r 相等的 a_i,因为一个序列 { a_l, \cdots, a_i, \cdots, a_r, x, x, x \,当 a_r = x 时,我们可以考虑先删除 [i, r - 1] 这个区间的元素,即 a_i, a_{i + 1}, \cdots, a_{r - 1,我们把这些元素单独拿出来考虑,即不用考虑 r - 1 后面和 a_{r - 1 相等的元素(因为 a_r 和后面等于它的数字会在下一步中删除,而 a_{r 后面其他的数字已经被删除),故这里的分数为 f(i, r - 1, 0);接着这个问题就变成了删除区间 [l, i] 和 a_r 以及 a_r 后方和它相等的 k 个数,因为 a_i = a_r,所以这个问题就是 [l, i] 区间和它的后方和 a_i 相等的 k + 1 个数,这里的分数是 f(l, i, k + 1)。

这样我们就可以计算出 f(0, n-1, 0) 的值,即为答案。

我们不难写出这样的代码:

[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int dp[100][100][100];

int removeBoxes(vector<int>& boxes) {
memset(dp, 0, sizeof dp);
return calculatePoints(boxes, 0, boxes.size() - 1, 0);
}

int calculatePoints(vector<int>& boxes, int l, int r, int k) {
if (l > r) {
return 0;
}
if (dp[l][r][k] == 0) {
dp[l][r][k] = calculatePoints(boxes, l, r - 1, 0) + (k + 1) * (k + 1);
for (int i = l; i < r; i++) {
if (boxes[i] == boxes[r]) {
dp[l][r][k] = max(dp[l][r][k], calculatePoints(boxes, l, i, k + 1) + calculatePoints(boxes, i + 1, r - 1, 0));
}
}
}
return dp[l][r][k];
}
};
[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
int[][][] dp;

public int removeBoxes(int[] boxes) {
int length = boxes.length;
dp = new int[length][length][length];
return calculatePoints(boxes, 0, length - 1, 0);
}

public int calculatePoints(int[] boxes, int l, int r, int k) {
if (l > r) {
return 0;
}
if (dp[l][r][k] == 0) {
dp[l][r][k] = calculatePoints(boxes, l, r - 1, 0) + (k + 1) * (k + 1);
for (int i = l; i < r; i++) {
if (boxes[i] == boxes[r]) {
dp[l][r][k] = Math.max(dp[l][r][k], calculatePoints(boxes, l, i, k + 1) + calculatePoints(boxes, i + 1, r - 1, 0));
}
}
}
return dp[l][r][k];
}
}
[sol1-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func removeBoxes(boxes []int) int {
dp := [100][100][100]int{}
var calculatePoints func(boxes []int, l, r, k int) int
calculatePoints = func(boxes []int, l, r, k int) int {
if l > r {
return 0
}
if dp[l][r][k] == 0 {
dp[l][r][k] = calculatePoints(boxes, l, r - 1, 0) + (k + 1) * (k + 1)
for i := l; i < r; i++ {
if boxes[i] == boxes[r] {
dp[l][r][k] = max(dp[l][r][k], calculatePoints(boxes, l, i, k + 1) + calculatePoints(boxes, i + 1, r - 1, 0))
}
}
}
return dp[l][r][k]
}
return calculatePoints(boxes, 0, len(boxes) - 1, 0)
}

func max(x, y int) int {
if x > y {
return x
}
return y
}
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int dp[100][100][100];

int calculatePoints(int* boxes, int l, int r, int k) {
if (l > r) {
return 0;
}
if (dp[l][r][k] == 0) {
dp[l][r][k] = calculatePoints(boxes, l, r - 1, 0) + (k + 1) * (k + 1);
for (int i = l; i < r; i++) {
if (boxes[i] == boxes[r]) {
dp[l][r][k] = fmax(dp[l][r][k], calculatePoints(boxes, l, i, k + 1) + calculatePoints(boxes, i + 1, r - 1, 0));
}
}
}
return dp[l][r][k];
}

int removeBoxes(int* boxes, int boxesSize) {
memset(dp, 0, sizeof dp);
return calculatePoints(boxes, 0, boxesSize - 1, 0);
}

在这份代码中,我们把 f(l, r, k) 初始化成 f(l, r - 1, 0) + (k + 1)^2。我们可以做一个小小的优化。假设当前区间是这样的:{ a_l, a_{l + 1}, \cdots, x, x, x, a_{r}, x, y, x, x, \cdots \,此时如果 a_r = x,那么初始化的时候 f(l, r, k) = f(l, r - 1, 0) + (k + 1)^2,接下来的循环从 l 到 r - 1。我们观察到其实 a_r 前面还有若干个连续的和 a_r 相等的数,这些数可以和 a_r 及后面的 x 作为一个整体,在这里我们可以初始化 f(l, r, k) = f(l, r - 3, 0) + (k + 3)^2。为什么可以这样初始化呢?这样初始化相当于把前面 3 个 x 和 a_r 和后面的一坨 x 捆绑到了一起,因为分开计算一定是没有合并计算优的,因为当 k \ge 0 的时候 (k + 1 + 1 + 1)^2 > (k + 1)^2 + (1 + 1)^2 > (k + 1)^2 + 1^2 + 1^2。

下面是优化的代码。

[sol2-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
int dp[100][100][100];

int removeBoxes(vector<int>& boxes) {
memset(dp, 0, sizeof dp);
return calculatePoints(boxes, 0, boxes.size() - 1, 0);
}

int calculatePoints(vector<int>& boxes, int l, int r, int k) {
if (l > r) {
return 0;
}
if (dp[l][r][k] == 0) {
int r1 = r, k1 = k;
while (r1 > l && boxes[r1] == boxes[r1 - 1]) {
r1--;
k1++;
}
dp[l][r][k] = calculatePoints(boxes, l, r1 - 1, 0) + (k1 + 1) * (k1 + 1);
for (int i = l; i < r1; i++) {
if (boxes[i] == boxes[r1]) {
dp[l][r][k] = max(dp[l][r][k], calculatePoints(boxes, l, i, k1 + 1) + calculatePoints(boxes, i + 1, r1 - 1, 0));
}
}
}
return dp[l][r][k];
}
};
[sol2-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
int[][][] dp;

public int removeBoxes(int[] boxes) {
int length = boxes.length;
dp = new int[length][length][length];
return calculatePoints(boxes, 0, length - 1, 0);
}

public int calculatePoints(int[] boxes, int l, int r, int k) {
if (l > r) {
return 0;
}
if (dp[l][r][k] == 0) {
int r1 = r, k1 = k;
while (r1 > l && boxes[r1] == boxes[r1 - 1]) {
r1--;
k1++;
}
dp[l][r][k] = calculatePoints(boxes, l, r1 - 1, 0) + (k1 + 1) * (k1 + 1);
for (int i = l; i < r1; i++) {
if (boxes[i] == boxes[r1]) {
dp[l][r][k] = Math.max(dp[l][r][k], calculatePoints(boxes, l, i, k1 + 1) + calculatePoints(boxes, i + 1, r1 - 1, 0));
}
}
}
return dp[l][r][k];
}
}
[sol2-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func removeBoxes(boxes []int) int {
dp := [100][100][100]int{}
var calculatePoints func(boxes []int, l, r, k int) int
calculatePoints = func(boxes []int, l, r, k int) int {
if l > r {
return 0
}
if dp[l][r][k] == 0 {
r1, k1 := r, k
for r1 > l && boxes[r1] == boxes[r1 - 1] {
r1--
k1++
}
dp[l][r][k] = calculatePoints(boxes, l, r1 - 1, 0) + (k1 + 1) * (k1 + 1)
for i := l; i < r1; i++ {
if boxes[i] == boxes[r1] {
dp[l][r][k] = max(dp[l][r][k], calculatePoints(boxes, l, i, k1 + 1) + calculatePoints(boxes, i + 1, r1 - 1, 0))
}
}
}
return dp[l][r][k]
}
return calculatePoints(boxes, 0, len(boxes) - 1, 0)
}

func max(x, y int) int {
if x > y {
return x
}
return y
}
[sol2-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int dp[100][100][100];

int calculatePoints(int* boxes, int l, int r, int k) {
if (l > r) {
return 0;
}
if (dp[l][r][k] == 0) {
int r1 = r, k1 = k;
while (r1 > l && boxes[r1] == boxes[r1 - 1]) {
r1--;
k1++;
}
dp[l][r][k] = calculatePoints(boxes, l, r1 - 1, 0) + (k1 + 1) * (k1 + 1);
for (int i = l; i < r1; i++) {
if (boxes[i] == boxes[r1]) {
dp[l][r][k] = fmax(dp[l][r][k], calculatePoints(boxes, l, i, k1 + 1) + calculatePoints(boxes, i + 1, r1 - 1, 0));
}
}
}
return dp[l][r][k];
}

int removeBoxes(int* boxes, int boxesSize) {
memset(dp, 0, sizeof dp);
return calculatePoints(boxes, 0, boxesSize - 1, 0);
}

复杂度分析

  • 时间复杂度:O(n^4),其中 n 是盒子的数量。最坏情况下每个 f(l, r, k) 被计算一次,每次状态转移需要 O(n) 的时间复杂度。
  • 空间复杂度:O(n^3)。数组 dp 的空间代价是 O(n^3),递归使用栈空间的代价为 O(n)。
 Comments
On this page
0546-移除盒子