The Remove Boxes problem presents you with an array of integers called boxes
, where each element represents the color of a box. The goal is to remove boxes from the array to maximize your score, following these rules:
k
adjacent boxes of the same color, you gain k * k
points.Your task is to determine the maximum score you can achieve by removing the boxes in an optimal order. The constraints are:
boxes
is up to 100.
At first glance, you might consider removing the largest group of same-colored boxes at each step (a greedy approach). However, this does not always yield the highest score, because sometimes it is better to leave a group for later to merge with other boxes of the same color, resulting in a much higher score due to the square function (k * k
).
The brute-force approach would be to try every possible removal sequence, but the number of possibilities grows exponentially. To optimize, we need a way to remember the results of subproblems we've already solved, i.e., use dynamic programming with memoization. We need to track not just the subarray we are considering, but also how many boxes of the same color are contiguous to the left (to allow for merging).
We use a 3D dynamic programming approach to solve this problem efficiently. Here’s how the solution is structured:
dp[l][r][k]
be the maximum score for the subarray boxes[l...r]
with k
boxes of the same color as boxes[r]
appended to the right.
l > r
, return 0 (no boxes left).
r
) along with any extra boxes of the same color (k
):
dp[l][r-1][0] + (k+1)^2
i
from l
to r-1
where boxes[i] == boxes[r]
:
i+1
and r-1
first, so that boxes[i]
and boxes[r]
become adjacent. Then, solve for dp[l][i][k+1] + dp[i+1][r-1][0]
.
This approach ensures that all possible merging and removal sequences are considered, but each unique subproblem is solved only once.
Let's walk through an example with boxes = [1, 3, 2, 2, 2, 3, 4, 3, 1]
.
[2,2,2]
(indices 2-4), earning 3*3=9
points. The array becomes [1,3,3,4,3,1]
.
3
s separated by 4
. If we remove 4
first, we can merge the 3
s into a group of 3, earning 3*3=9
points.
In this example, the optimal sequence gives a score of 23
.
O(n!)
.
O(n^3)
subproblems because l
and r
each range from 0
to n-1
, and k
can be up to n
. Each subproblem takes O(n)
time to compute (for the inner loop), so the total is O(n^4)
.
O(n^3)
space.
The Remove Boxes problem is a classic example of how dynamic programming can be used to optimize a seemingly intractable problem. By carefully defining the state and using memoization, we avoid redundant calculations and ensure that all possible merging and removal strategies are considered. The key insight is that sometimes, removing boxes in a non-greedy order allows for larger groups to be formed later, greatly increasing the score. This solution elegantly balances recursion, state tracking, and optimal substructure.
from functools import lru_cache
class Solution:
def removeBoxes(self, boxes):
n = len(boxes)
@lru_cache(None)
def dp(l, r, k):
if l > r:
return 0
# Extend boxes[r] group to the left
while l < r and boxes[r] == boxes[r-1]:
r -= 1
k += 1
# Option 1: remove the group at the end
res = dp(l, r-1, 0) + (k+1)*(k+1)
# Option 2: try to merge non-contiguous groups
for i in range(l, r):
if boxes[i] == boxes[r]:
res = max(res, dp(l, i, k+1) + dp(i+1, r-1, 0))
return res
return dp(0, n-1, 0)
class Solution {
public:
int dp[100][100][100];
int removeBoxes(vector<int>& boxes) {
memset(dp, -1, sizeof(dp));
return helper(boxes, 0, boxes.size() - 1, 0);
}
int helper(vector<int>& boxes, int l, int r, int k) {
if (l > r) return 0;
if (dp[l][r][k] != -1) return dp[l][r][k];
int orig_r = r, orig_k = k;
while (l < r && boxes[r] == boxes[r-1]) {
r--;
k++;
}
int res = helper(boxes, l, r-1, 0) + (k+1)*(k+1);
for (int i = l; i < r; ++i) {
if (boxes[i] == boxes[r]) {
res = max(res, helper(boxes, l, i, k+1) + helper(boxes, i+1, r-1, 0));
}
}
dp[l][orig_r][orig_k] = res;
return res;
}
};
class Solution {
int[][][] dp;
public int removeBoxes(int[] boxes) {
int n = boxes.length;
dp = new int[n][n][n];
return helper(boxes, 0, n-1, 0);
}
private int helper(int[] boxes, int l, int r, int k) {
if (l > r) return 0;
if (dp[l][r][k] != 0) return dp[l][r][k];
while (l < r && boxes[r] == boxes[r-1]) {
r--;
k++;
}
int res = helper(boxes, l, r-1, 0) + (k+1)*(k+1);
for (int i = l; i < r; i++) {
if (boxes[i] == boxes[r]) {
res = Math.max(res, helper(boxes, l, i, k+1) + helper(boxes, i+1, r-1, 0));
}
}
dp[l][r][k] = res;
return res;
}
}
var removeBoxes = function(boxes) {
const n = boxes.length;
const memo = new Map();
function dp(l, r, k) {
if (l > r) return 0;
let key = l + ',' + r + ',' + k;
if (memo.has(key)) return memo.get(key);
while (l < r && boxes[r] === boxes[r-1]) {
r--;
k++;
}
let res = dp(l, r-1, 0) + (k+1)*(k+1);
for (let i = l; i < r; i++) {
if (boxes[i] === boxes[r]) {
res = Math.max(res, dp(l, i, k+1) + dp(i+1, r-1, 0));
}
}
memo.set(key, res);
return res;
}
return dp(0, n-1, 0);
};