546. Remove Boxes
Problem Description
You have a row of boxes, where each box has a color represented by a positive number. Your goal is to remove all boxes and maximize your total score.
The game works as follows:
- In each round, you can select a group of consecutive boxes that all have the same color
- When you remove
kconsecutive boxes of the same color, you earnk * kpoints - After removing boxes, the remaining boxes shift together (maintaining their order)
- You continue removing boxes until none are left
For example, if you have boxes [1, 3, 2, 2, 2, 3, 4, 3, 1]:
- You could remove the three consecutive 2's in the middle to get
3 * 3 = 9points - Or you might remove boxes strategically in a different order to get a higher total score
The challenge is to find the optimal sequence of removals that maximizes your total points. Since removing boxes causes the remaining boxes to come together, you might want to remove certain boxes first to create larger groups of the same color later.
Your task is to determine the maximum possible points you can achieve by removing all the boxes.
Intuition
The key insight is that when we remove boxes, we might want to delay removing certain boxes to combine them with others of the same color later. This creates a complex decision tree where the order of removal significantly impacts the final score.
Consider a simple example: [1, 2, 1]. If we remove the middle 2 first, we get 1 * 1 = 1 point, then the two 1s come together and we can remove them for 2 * 2 = 4 points, totaling 5 points. But if we removed each box individually, we'd only get 1 + 1 + 1 = 3 points.
This suggests we need dynamic programming with memoization. But what state should we track? The naive approach of tracking which boxes have been removed would be too complex.
The clever observation is that we can define our subproblem as: "What's the maximum score for removing boxes from index i to j, where we already have k boxes to the right of position j (outside the range [i, j]) that share the same color as boxes[j] and will be combined with it later?"
Why track boxes outside the current range? When we remove boxes in the middle of the array, previously separated boxes come together. For example, if we have [1, 2, 2, 1] and remove the two middle 2s first, the two 1s become adjacent. The parameter k lets us track that the 1 at position 3 is "waiting" to be combined with the 1 at position 0, even though position 3 is outside our current working range.
For the recursive case, we have two main strategies:
- Remove
boxes[j]along with thekboxes of the same color, getting(k + 1) * (k + 1)points - Look for another box with the same color as
boxes[j]somewhere in the range[i, j-1], remove the boxes between them first, then combine them for a potentially higher score
The second strategy is powerful because it allows us to "save" boxes of the same color to remove them together later, potentially earning more points than removing them separately.
Learn more about Memoization and Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming with memoization to solve this problem. Let's break down the implementation:
State Definition:
dfs(i, j, k)returns the maximum points we can get from removing boxes in the range[i, j], where we havekadditional boxes to the right of positionj(outside the current range) that share the same color asboxes[j]and will eventually be combined with it whenboxes[j]is removed.
Base Case:
- If
i > j, there are no boxes to remove, so return 0.
Optimization Step: Before processing, we consolidate consecutive boxes of the same color at the end:
while i < j and boxes[j] == boxes[j - 1]: j, k = j - 1, k + 1
This merges adjacent boxes of the same color as boxes[j] into our k counter, reducing redundant calculations.
Recursive Cases:
-
Direct Removal Strategy:
ans = dfs(i, j - 1, 0) + (k + 1) * (k + 1)Remove
boxes[j]along with thekboxes of the same color. This gives us(k + 1) * (k + 1)points, plus whatever we get from removing the remaining boxes[i, j-1]. -
Merge Strategy:
for h in range(i, j): if boxes[h] == boxes[j]: ans = max(ans, dfs(h + 1, j - 1, 0) + dfs(i, h, k + 1))For each box at position
hin the range[i, j-1]that has the same color asboxes[j]:- First, completely remove all boxes between positions
handj:dfs(h + 1, j - 1, 0) - Now
boxes[h]andboxes[j](plus thekboxes to the right ofj) can all be grouped together - Solve the subproblem
dfs(i, h, k + 1)whereboxes[h]now hask + 1boxes waiting to be combined with it: the originalkboxes that were to the right ofj, plusboxes[j]itself
- First, completely remove all boxes between positions
Memoization:
The @cache decorator automatically memoizes the results, preventing redundant calculations for the same (i, j, k) state.
Time Complexity: O(n^4) where n is the number of boxes. We have O(n^3) possible states and each state takes O(n) time to compute.
Space Complexity: O(n^3) for the memoization cache.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with boxes [2, 1, 2, 1] to illustrate the solution approach.
Initial Call: dfs(0, 3, 0) - process boxes from index 0 to 3 with 0 additional boxes.
Step 1: Processing dfs(0, 3, 0)
- Current range:
[2, 1, 2, 1],boxes[3] = 1,k = 0 - No consecutive same-color boxes at the end, so no optimization step needed
Option 1 - Direct Removal:
- Remove
boxes[3](the1at index 3) immediately - Score:
dfs(0, 2, 0) + (0 + 1)² = dfs(0, 2, 0) + 1
Option 2 - Merge Strategy:
- Look for boxes with same color as
boxes[3](which is1) - Found
boxes[1] = 1 - Remove boxes between index 1 and 3 first:
dfs(2, 2, 0)- This removes the
2at index 2, giving us1point
- This removes the
- Then handle
dfs(0, 1, 1)where the1parameter means we have 1 box waiting outside the range[0, 1](specifically,boxes[3]which we're saving to merge later)- This processes
[2, 1]where the1at index 1 can eventually be combined with the1at index 3 (the one we're tracking with k=1) - We can remove the
2first:dfs(0, 0, 0) = 1point - Then remove both
1s together (the one at index 1 plus the one at index 3 that we've been tracking):(1 + 1)² = 4points
- This processes
- Total for this path:
1 + 1 + 4 = 6points
Continuing the Direct Removal path from Step 1:
dfs(0, 2, 0)processes[2, 1, 2]- Direct removal: Remove the
2at index 2 for1point, then solvedfs(0, 1, 0) - Merge strategy:
boxes[0] = 2matchesboxes[2]- Remove middle box:
dfs(1, 1, 0) = 1point (remove the1) - Then
dfs(0, 0, 1): Remove two2s together for(1 + 1)² = 4points - Total:
1 + 4 = 5points
- Remove middle box:
- Best for
dfs(0, 2, 0)is5points - So direct removal total:
5 + 1 = 6points
Final Result: Both strategies give us 6 points as the maximum.
The key insight demonstrated here is how the merge strategy allows us to "save" boxes of the same color (the two 1s) to remove them together later for more points (2² = 4) instead of removing them separately (1² + 1² = 2). This dynamic programming approach explores all possible removal orders efficiently through memoization.
Solution Implementation
1class Solution:
2 def removeBoxes(self, boxes: List[int]) -> int:
3 """
4 Remove boxes to maximize points. When removing k consecutive boxes of the same color,
5 you get k * k points.
6
7 Args:
8 boxes: List of integers representing box colors
9
10 Returns:
11 Maximum points obtainable by removing all boxes
12 """
13 from functools import cache
14
15 @cache
16 def dfs(left: int, right: int, extra_count: int) -> int:
17 """
18 Dynamic programming function to find maximum points for boxes[left:right+1]
19 with extra_count additional boxes of the same color as boxes[right] that can be
20 merged with boxes[right].
21
22 Args:
23 left: Left boundary index (inclusive)
24 right: Right boundary index (inclusive)
25 extra_count: Number of boxes to the right of position 'right' (outside
26 the current range) that share the same color as boxes[right]
27 and will be merged with it when removed
28
29 Returns:
30 Maximum points obtainable from this subproblem
31 """
32 # Base case: no boxes left
33 if left > right:
34 return 0
35
36 # Optimization: merge consecutive boxes of same color at the right end
37 # This reduces redundant states in memoization
38 while left < right and boxes[right] == boxes[right - 1]:
39 right -= 1
40 extra_count += 1
41
42 # Option 1: Remove boxes[right] along with extra_count boxes of same color
43 max_points = dfs(left, right - 1, 0) + (extra_count + 1) * (extra_count + 1)
44
45 # Option 2: Try to merge boxes[right] with boxes[mid] where they have same color
46 # Split the problem into two subproblems
47 for mid in range(left, right):
48 if boxes[mid] == boxes[right]:
49 # Remove boxes between mid and right first, then merge boxes[mid] with boxes[right]
50 points = (dfs(mid + 1, right - 1, 0) +
51 dfs(left, mid, extra_count + 1))
52 max_points = max(max_points, points)
53
54 return max_points
55
56 # Calculate result for entire array
57 n = len(boxes)
58 result = dfs(0, n - 1, 0)
59
60 # Clear cache to free memory
61 dfs.cache_clear()
62
63 return result
641class Solution {
2 // Memoization table: dp[left][right][extraBoxes] stores the maximum points
3 // for boxes[left...right] with extraBoxes same-colored boxes to the right
4 private int[][][] dp;
5 private int[] boxes;
6
7 public int removeBoxes(int[] boxes) {
8 this.boxes = boxes;
9 int n = boxes.length;
10 dp = new int[n][n][n];
11 return calculateMaxPoints(0, n - 1, 0);
12 }
13
14 /**
15 * Calculate maximum points for removing boxes in range [left, right]
16 * @param left - left boundary of the range
17 * @param right - right boundary of the range
18 * @param extraCount - number of boxes to the right of position 'right' (outside
19 * the current range [left, right]) that share the same color
20 * as boxes[right] and will be merged with it when removed
21 * @return maximum points achievable
22 */
23 private int calculateMaxPoints(int left, int right, int extraCount) {
24 // Base case: no boxes in range
25 if (left > right) {
26 return 0;
27 }
28
29 // Optimization: merge consecutive same-colored boxes at the right end
30 // This reduces redundant calculations
31 while (left < right && boxes[right] == boxes[right - 1]) {
32 right--;
33 extraCount++;
34 }
35
36 // Return memoized result if already calculated
37 if (dp[left][right][extraCount] > 0) {
38 return dp[left][right][extraCount];
39 }
40
41 // Option 1: Remove all boxes at the right end together
42 // Remove boxes[right] along with extraCount same-colored boxes
43 int maxPoints = calculateMaxPoints(left, right - 1, 0) +
44 (extraCount + 1) * (extraCount + 1);
45
46 // Option 2: Look for boxes with same color as boxes[right] in the range
47 // and try to merge them before removal
48 for (int mid = left; mid < right; mid++) {
49 if (boxes[mid] == boxes[right]) {
50 // Remove boxes between mid and right first,
51 // then merge boxes[mid] with boxes[right] and extra boxes
52 int points = calculateMaxPoints(mid + 1, right - 1, 0) +
53 calculateMaxPoints(left, mid, extraCount + 1);
54 maxPoints = Math.max(maxPoints, points);
55 }
56 }
57
58 // Memoize and return the result
59 dp[left][right][extraCount] = maxPoints;
60 return maxPoints;
61 }
62}
631class Solution {
2public:
3 int removeBoxes(vector<int>& boxes) {
4 int n = boxes.size();
5 // dp[left][right][count] represents the maximum points we can get
6 // by removing boxes[left...right] with 'count' additional boxes
7 // to the right of position 'right' (outside the current range) that
8 // share the same color as boxes[right] and will be merged with it
9 vector<vector<vector<int>>> dp(n, vector<vector<int>>(n, vector<int>(n, 0)));
10
11 // Define recursive function with memoization
12 function<int(int, int, int)> calculateMaxPoints;
13
14 calculateMaxPoints = [&](int left, int right, int additionalCount) -> int {
15 // Base case: no boxes in the range
16 if (left > right) {
17 return 0;
18 }
19
20 // Optimization: merge consecutive boxes with same color at the right end
21 // This reduces redundant calculations
22 while (left < right && boxes[right] == boxes[right - 1]) {
23 right--;
24 additionalCount++;
25 }
26
27 // Return memoized result if already calculated
28 if (dp[left][right][additionalCount] != 0) {
29 return dp[left][right][additionalCount];
30 }
31
32 // Option 1: Remove boxes[right] along with additionalCount boxes of same color
33 // This gives us (additionalCount + 1)^2 points
34 int maxPoints = calculateMaxPoints(left, right - 1, 0) +
35 (additionalCount + 1) * (additionalCount + 1);
36
37 // Option 2: Try to find boxes with same color as boxes[right] in range [left, right-1]
38 // and merge them together before removing
39 for (int mid = left; mid < right; mid++) {
40 if (boxes[mid] == boxes[right]) {
41 // Remove boxes between mid and right first (boxes[mid+1...right-1])
42 // Then remove boxes[left...mid] with boxes[right] and additional boxes
43 maxPoints = max(maxPoints,
44 calculateMaxPoints(mid + 1, right - 1, 0) +
45 calculateMaxPoints(left, mid, additionalCount + 1));
46 }
47 }
48
49 // Store the result in dp table for memoization
50 dp[left][right][additionalCount] = maxPoints;
51 return maxPoints;
52 };
53
54 // Start with the entire array and no additional boxes
55 return calculateMaxPoints(0, n - 1, 0);
56 }
57};
581function removeBoxes(boxes: number[]): number {
2 const n = boxes.length;
3 // dp[left][right][count] represents the maximum points we can get
4 // by removing boxes[left...right] with 'count' additional boxes
5 // to the right of position 'right' (outside the current range) that
6 // share the same color as boxes[right] and will be merged with it
7 const dp: number[][][] = Array(n).fill(0).map(() =>
8 Array(n).fill(0).map(() =>
9 Array(n).fill(0)
10 )
11 );
12
13 // Define recursive function with memoization
14 const calculateMaxPoints = (left: number, right: number, additionalCount: number): number => {
15 // Base case: no boxes in the range
16 if (left > right) {
17 return 0;
18 }
19
20 // Optimization: merge consecutive boxes with same color at the right end
21 // This reduces redundant calculations
22 while (left < right && boxes[right] === boxes[right - 1]) {
23 right--;
24 additionalCount++;
25 }
26
27 // Return memoized result if already calculated
28 if (dp[left][right][additionalCount] !== 0) {
29 return dp[left][right][additionalCount];
30 }
31
32 // Option 1: Remove boxes[right] along with additionalCount boxes of same color
33 // This gives us (additionalCount + 1)^2 points
34 let maxPoints = calculateMaxPoints(left, right - 1, 0) +
35 (additionalCount + 1) * (additionalCount + 1);
36
37 // Option 2: Try to find boxes with same color as boxes[right] in range [left, right-1]
38 // and merge them together before removing
39 for (let mid = left; mid < right; mid++) {
40 if (boxes[mid] === boxes[right]) {
41 // Remove boxes between mid and right first (boxes[mid+1...right-1])
42 // Then remove boxes[left...mid] with boxes[right] and additional boxes
43 maxPoints = Math.max(maxPoints,
44 calculateMaxPoints(mid + 1, right - 1, 0) +
45 calculateMaxPoints(left, mid, additionalCount + 1)
46 );
47 }
48 }
49
50 // Store the result in dp table for memoization
51 dp[left][right][additionalCount] = maxPoints;
52 return maxPoints;
53 };
54
55 // Start with the entire array and no additional boxes
56 return calculateMaxPoints(0, n - 1, 0);
57}
58Time and Space Complexity
Time Complexity: O(n^4)
The time complexity analysis involves understanding the memoization pattern:
- The
dfsfunction has three parameters:i,j, andk iandjrange from0ton-1, giving usO(n^2)possible combinationskrepresents the count of consecutive boxes equal toboxes[j]that follow positionj. In the worst case,kcan be at mostn- This gives us
O(n^3)possible states to memoize
For each state (i, j, k):
- The while loop at the beginning runs in
O(n)time in the worst case - The for loop iterates through positions from
itoj-1, which isO(n)iterations - Each iteration makes recursive calls that are already memoized or will be memoized
Therefore, the total time complexity is O(n^3) states × O(n) work per state = O(n^4)
Space Complexity: O(n^3)
The space complexity consists of:
- The memoization cache stores up to
O(n^3)states (all possible combinations ofi,j, andk) - The recursion stack depth can go up to
O(n)in the worst case when we recurse through all boxes - Since
O(n^3)dominatesO(n), the overall space complexity isO(n^3)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect State Definition - Missing the "Extra Count" Parameter
One of the most common mistakes is trying to solve this problem with a simpler DP state like dp[i][j] representing the maximum points from removing boxes in range [i, j]. This approach fails because it doesn't account for boxes outside the current range that could be merged later.
Why this fails:
Consider boxes [1, 2, 1, 2]. If we just use dp[i][j]:
- We might remove the middle
[2, 1]first, getting some points - But we lose the opportunity to merge the two 1's together for a better score
Solution:
The third parameter k (or extra_count) is crucial. It tracks boxes of the same color as boxes[j] that are to the right of position j (outside the current range [i, j]) and will be merged with boxes[j] when it's removed. This allows the algorithm to consider strategies where we remove intermediate boxes first, bringing distant same-colored boxes together to create larger groups.
2. Forgetting the Optimization Step
Without the consolidation step that merges consecutive same-colored boxes:
while left < right and boxes[right] == boxes[right - 1]: right -= 1 extra_count += 1
Why this matters:
- Without this optimization, you'd have many redundant states in your memoization
- For example,
dfs(0, 5, 2)anddfs(0, 4, 3)might represent the same actual problem ifboxes[4] == boxes[5] - This leads to exponential blowup in the number of states and makes the solution too slow
Solution:
Always consolidate consecutive boxes of the same color at the boundary. This ensures each memoized state is unique and reduces the total number of states from potentially O(n^4) to O(n^3).
3. Incorrect Merging Logic
A subtle but critical mistake is incorrectly handling the merge strategy:
Wrong approach:
# INCORRECT: This doesn't properly account for the merging
for h in range(i, j):
if boxes[h] == boxes[j]:
ans = max(ans, dfs(i, h, k) + dfs(h + 1, j, 0)) # Wrong!
Why this fails:
This approach doesn't correctly model what happens when we remove the middle section. When we remove boxes [h+1, j-1], we're left with boxes[h] that should now be adjacent to boxes[j] and the k extra boxes.
Solution: The correct formulation is:
ans = max(ans, dfs(h + 1, j - 1, 0) + dfs(i, h, k + 1))
- First remove the middle section
[h+1, j-1]completely - Then solve for
[i, h]whereboxes[h]now hask+1additional boxes to merge with
4. Memory Limit Issues with Large Inputs
The memoization cache can grow very large (O(n^3) entries), potentially causing memory issues.
Solution:
- Clear the cache after getting the result:
dfs.cache_clear() - Consider using an iterative DP approach with arrays if memory is critically constrained
- Use appropriate data types (avoid unnecessary large integers if the problem constraints allow)
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!