LeetCode 546 - Remove Boxes
The problem gives us an array called boxes, where each integer represents the color of a box. We may repeatedly remove groups of adjacent boxes that share the same color. If we remove a group containing k boxes, we earn k k points.
Difficulty: 🔴 Hard
Topics: Array, Dynamic Programming, Memoization
Solution
LeetCode 546, Remove Boxes
Problem Understanding
The problem gives us an array called boxes, where each integer represents the color of a box. We may repeatedly remove groups of adjacent boxes that share the same color. If we remove a group containing k boxes, we earn k * k points.
The important detail is that removing boxes changes the structure of the array. Boxes that were previously separated may become adjacent after intermediate boxes are removed. This means the order in which we remove groups matters significantly.
The goal is to maximize the total score after all boxes are removed.
For example, suppose we have:
[1,3,2,2,2,3,4,3,1]
If we remove the three adjacent 2s first, we gain 3 * 3 = 9 points. After they disappear, the remaining array becomes:
[1,3,3,4,3,1]
Now more opportunities appear because some equal colors have become closer together.
The constraints are important:
1 <= boxes.length <= 1001 <= boxes[i] <= 100
An array size of 100 is too large for brute force enumeration of all possible removal sequences. The number of different ways to remove groups grows explosively because every removal changes future possibilities.
This immediately suggests that we need dynamic programming with memoization.
Several edge cases are especially important:
- Arrays containing only one color, where removing everything at once is optimal.
- Arrays where equal colors are separated by other colors.
- Arrays with alternating colors, where merging equal values later may produce higher scores.
- Very small arrays such as length 1.
- Situations where removing a smaller group first allows a much larger merged group later.
The main challenge is that the locally optimal move is often not globally optimal.
Approaches
Brute Force Approach
A brute force solution would recursively try every possible removable group at every step.
At any point:
- Find every contiguous segment of equal values.
- Remove one segment.
- Recursively solve the remaining array.
- Return the maximum score among all choices.
This approach is correct because it explores every possible removal order. Eventually, it discovers the optimal sequence.
However, it is far too slow. Every removal creates a new array configuration, and the number of possible sequences grows exponentially. Since the array length can reach 100, brute force becomes completely impractical.
The core issue is overlapping subproblems. The same remaining configurations appear repeatedly during recursion.
Key Insight for the Optimal Solution
The difficult part of this problem is that it may be beneficial to delay removing some boxes so they can merge with matching boxes later.
Consider:
[1, 2, 1]
If we remove the first 1 immediately:
- Remove
1, gain 1 - Remove
2, gain 1 - Remove
1, gain 1 - Total = 3
But if we remove 2 first:
- Remove
2, gain 1 - Array becomes
[1,1] - Remove both
1s together, gain 4 - Total = 5
This means the DP state must remember not only the current subarray, but also how many equal-colored boxes are already grouped with one side.
The standard solution uses a 3D dynamic programming state:
dp(left, right, count)
This means:
- We are solving the subarray
boxes[left:right+1] - There are already
countextra boxes equal toboxes[left]attached to the left side
This additional parameter is the key insight that makes the problem solvable.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential | Exponential | Tries every possible removal sequence |
| Optimal | O(n^4) | O(n^3) | Memoized interval DP with carry count |
Algorithm Walkthrough
Optimal Dynamic Programming Strategy
We define:
dp(left, right, count)
where:
leftandrightdefine the current intervalcountrepresents how many extra boxes equal toboxes[left]are grouped together withboxes[left]
The function returns the maximum score obtainable from this interval.
Step by Step Process
- Define the recursive state.
We solve the problem for a subarray boxes[left:right+1]. The parameter count tracks additional boxes matching boxes[left] that were merged from outside the interval.
This is necessary because larger groups produce disproportionately larger scores. 2. Handle the base case.
If:
left > right
then there are no boxes remaining, so the score is 0. 3. Compress consecutive equal boxes at the beginning.
Suppose we have:
[2,2,2,3,...]
Instead of treating these separately, we immediately merge them into the carry count.
For example:
dp(left, right, count)
becomes:
dp(newLeft, right, count + mergedCount)
This optimization reduces repeated states. 4. Compute the direct removal option.
One possibility is to remove the current group immediately.
If the current merged group size is:
count + 1
then removing it gives:
(count + 1)^2
plus the optimal score for the remaining interval. 5. Try merging with matching boxes later.
We scan the interval looking for positions where:
boxes[mid] == boxes[left]
If such a position exists, we can:
- Remove everything between them first
- Then merge the matching boxes into a larger group
This often produces a better score. 6. Take the maximum result.
The DP state returns the best score among:
- Removing immediately
- Delaying removal to merge later
- Memoize results.
Since many recursive calls repeat the same states, memoization avoids recomputation.
Why it works
The algorithm works because every optimal strategy for an interval must eventually choose one of two possibilities for the leading group:
- Remove it immediately
- Merge it with a matching box later
The DP explores both possibilities exhaustively while memoization guarantees each unique state is solved only once. The carry parameter preserves all information needed to make optimal future decisions, so the recursion always computes the true optimal score.
Python Solution
from functools import lru_cache
from typing import List
class Solution:
def removeBoxes(self, boxes: List[int]) -> int:
n = len(boxes)
@lru_cache(None)
def dp(left: int, right: int, count: int) -> int:
if left > right:
return 0
# Merge consecutive equal boxes
while left < right and boxes[left] == boxes[left + 1]:
left += 1
count += 1
# Option 1:
# Remove current group immediately
best = (count + 1) * (count + 1) + dp(left + 1, right, 0)
# Option 2:
# Merge with future matching boxes
for mid in range(left + 1, right + 1):
if boxes[mid] == boxes[left]:
score = (
dp(left + 1, mid - 1, 0)
+ dp(mid, right, count + 1)
)
best = max(best, score)
return best
return dp(0, n - 1, 0)
The implementation follows the recursive interval DP structure directly.
The dp(left, right, count) function represents the optimal answer for the interval while carrying extra matching boxes.
The first while loop compresses consecutive equal values. This is an important optimization because contiguous equal boxes should always be treated as one larger group.
The variable best first stores the score obtained by removing the current group immediately.
Then the loop searches for future matching boxes. Whenever a matching color is found, the algorithm removes the middle section first so the matching groups can merge.
Memoization through @lru_cache(None) ensures each state is computed only once.
Finally, the solution starts with the entire array and no carry:
dp(0, n - 1, 0)
Go Solution
func removeBoxes(boxes []int) int {
n := len(boxes)
memo := make(map[[3]int]int)
var dp func(int, int, int) int
dp = func(left, right, count int) int {
if left > right {
return 0
}
key := [3]int{left, right, count}
if val, exists := memo[key]; exists {
return val
}
originalLeft := left
originalCount := count
// Merge consecutive equal boxes
for left < right && boxes[left] == boxes[left+1] {
left++
count++
}
// Remove current group immediately
best := (count+1)*(count+1) + dp(left+1, right, 0)
// Try merging with future matching boxes
for mid := left + 1; mid <= right; mid++ {
if boxes[mid] == boxes[left] {
score := dp(left+1, mid-1, 0) +
dp(mid, right, count+1)
if score > best {
best = score
}
}
}
memo[[3]int{originalLeft, right, originalCount}] = best
return best
}
return dp(0, n-1, 0)
}
The Go version uses a map[[3]int]int as the memoization structure because Go does not provide a built in memoization decorator like Python's lru_cache.
The recursive logic is otherwise identical.
Go slices are lightweight references to arrays, so passing boxes into recursive calls is efficient.
Integer overflow is not a concern because the maximum score is bounded by:
100 * 100 = 10000
which easily fits in Go's int.
Worked Examples
Example 1
Input:
[1,3,2,2,2,3,4,3,1]
Initial call:
dp(0, 8, 0)
| Step | Action | Score |
|---|---|---|
| 1 | Remove 2,2,2 |
9 |
| 2 | Array becomes [1,3,3,4,3,1] |
|
| 3 | Remove 4 |
1 |
| 4 | Array becomes [1,3,3,3,1] |
|
| 5 | Remove 3,3,3 |
9 |
| 6 | Array becomes [1,1] |
|
| 7 | Remove 1,1 |
4 |
| Total | 23 |
The DP discovers this sequence automatically by exploring merge opportunities.
A critical observation is that the middle 3s become more valuable after removing the 4.
Example 2
Input:
[1,1,1]
The algorithm compresses consecutive equal boxes immediately.
State progression:
| State | Meaning |
|---|---|
dp(0,2,0) |
Start |
Compress consecutive 1s |
count becomes 2 |
| Remove all together | (2+1)^2 = 9 |
Final answer:
9
Example 3
Input:
[1]
The only move is:
| Step | Action | Score |
|---|---|---|
| 1 | Remove 1 |
1 |
Final answer:
1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n^4) | There are O(n^3) states, each may scan O(n) positions |
| Space | O(n^3) | Memoization table stores all DP states |
The DP state consists of three dimensions:
leftrightcount
Each can vary up to n, producing O(n^3) states.
For every state, we may scan the interval looking for matching colors, which costs O(n) time.
Therefore, the total complexity becomes:
O(n^4)
This is acceptable for n <= 100.
Test Cases
sol = Solution()
assert sol.removeBoxes([1,3,2,2,2,3,4,3,1]) == 23 # official example
assert sol.removeBoxes([1,1,1]) == 9 # all same color
assert sol.removeBoxes([1]) == 1 # single element
assert sol.removeBoxes([1,2,1]) == 5 # merging later is better
assert sol.removeBoxes([1,2,2,2,1]) == 13 # remove middle first
assert sol.removeBoxes([1,2,3,4]) == 4 # no merges possible
assert sol.removeBoxes([1,1,2,2,2,1,1]) == 25 # large merge after removal
assert sol.removeBoxes([2,2,2,2]) == 16 # one large group
assert sol.removeBoxes([1,2,1,2,1,2]) == 12 # alternating pattern
assert sol.removeBoxes([3,3,3,1,3]) == 17 # preserve merge opportunity
| Test | Why |
|---|---|
[1,3,2,2,2,3,4,3,1] |
Validates official complex example |
[1,1,1] |
Tests full compression of equal values |
[1] |
Minimum input size |
[1,2,1] |
Ensures delayed merging works |
[1,2,2,2,1] |
Tests removing middle section first |
[1,2,3,4] |
No merges possible |
[1,1,2,2,2,1,1] |
Large merge formed later |
[2,2,2,2] |
Single large group |
[1,2,1,2,1,2] |
Alternating colors |
[3,3,3,1,3] |
Tests carrying counts correctly |
Edge Cases
Single Box
A single element array is the smallest valid input. A buggy implementation might accidentally recurse into invalid ranges or mishandle base cases.
For example:
[1]
The implementation handles this correctly because:
(left > right)
returns 0 for empty intervals, and the single box contributes:
(0 + 1)^2 = 1
All Boxes Have the Same Color
Consider:
[2,2,2,2]
The optimal strategy is obviously to remove everything at once for:
4^2 = 16
Without the consecutive compression optimization, the DP would generate many redundant states. The implementation avoids this by merging adjacent equal values immediately inside the while loop.
Equal Colors Separated by Other Colors
This is the most important edge case.
Example:
[1,2,1]
A greedy algorithm that removes the first available group would fail.
The correct solution removes the middle 2 first so the two 1s become adjacent.
The count parameter in the DP state is specifically designed to handle this situation. It allows the recursion to remember postponed groups and later merge them into larger scoring removals.