Remove Boxes is one of the hardest interval-DP problems out there. What makes it special is that two indices are not enough to describe a subproblem — you need a third dimension that remembers how many equal boxes are already glued to the left. Once you see why, it becomes a clean recurrence.
Problem. You have several boxes, each a positive number (a color). Repeatedly pick a contiguous
run of k boxes of the same color and remove them, scoring k * k points. Remove all boxes to
maximize the total score.
Example: boxes = [1, 3, 2, 3, 1] → answer 13. (Pop the lone 2 for 1, burst the two 3s for
4, then the three 1s are adjacent — burst them for 3 * 3 = 9. Total 1 + 4 + 9 = ... = 13.)
The slow way first
A greedy try — always burst the biggest available run — fails. In [1, 3, 2, 3, 1] the best 1-run looks tiny at first, but if you clear the middle the 1s collide into a run of three worth 9. So the move that pays off depends on what you do later. That screams dynamic programming over intervals.
The trap: a plain dp(l, r) over a sub-array is not enough. Whether it is worth merging the boxes at l with a later equal box depends on how many equal boxes were already sitting to the left, outside this interval. Two numbers cannot capture that, so the state must carry a third value.
The idea: add a third dimension k
Define dp(l, r, k) = the best points obtainable from boxes[l..r] given that k extra boxes equal to boxes[l] are already attached on the left. At box l you have two moves:
- Pop now: burst the run of
k + 1equal boxes immediately for(k + 1)^2points, then solvedp(l + 1, r, 0). - Merge later: keep the run, find some
m > lwithboxes[m] == boxes[l], clear everything strictly between them withdp(l + 1, m - 1, 0), and that gluing lets the equal boxes join — solvedp(m, r, k + 1).
The whole point of k is the merge branch: it lets us defer popping the left run so a later equal box can join it, turning two small bursts into one big one.
Walk through it
Step through the animation on [1, 3, 2, 3, 1]. We start at dp(0, 4, 0). Option A pops the lone 1 for 1 point. Option B spots the matching 1 at index 4, clears the inside [3, 2, 3] (the two 3s merge for 4, plus 1 for the 2), then the two 1s become adjacent and burst together. Comparing the branches, merging wins — and the same reasoning across all intervals gives 13.
Pseudocode
define dp(l, r, k): # k = equal boxes glued left of l
if l > r: return 0
absorb equal neighbours: while boxes[l+1] == boxes[l]: l += 1; k += 1
best = (k + 1)^2 + dp(l + 1, r, 0) # option A: pop the left run now
for m from l+1 to r: # option B: merge with a later equal box
if boxes[m] == boxes[l]:
best = max(best, dp(l+1, m-1, 0) + dp(m, r, k+1))
return best
answer = dp(0, n - 1, 0)The Python solution
def remove_boxes(boxes):
from functools import lru_cache
n = len(boxes)
@lru_cache(maxsize=None)
def dp(l, r, k):
if l > r:
return 0
while l < r and boxes[l + 1] == boxes[l]:
l, k = l + 1, k + 1
best = (k + 1) ** 2 + dp(l + 1, r, 0)
for m in range(l + 1, r + 1):
if boxes[m] == boxes[l]:
inside = dp(l + 1, m - 1, 0)
joined = dp(m, r, k + 1)
best = max(best, inside + joined)
return best
return dp(0, n - 1, 0)dp(l, r, k)is the subproblem: solveboxes[l..r]withkequal boxes already glued to the left.- The
whileloop absorbs equal neighbours into the left run up front, soboxes[l]starts a fresh color andkcounts the whole run minus one. - Line 11 is option A — pop the
k + 1glued boxes now for(k + 1)^2, then solve the rest from scratch. - Lines 14-16 are option B — for each later box of the same color, clear the
insideand recurse withk + 1so the equal boxes merge; keep the better total. @lru_cachememoizes the three-argument state, turning exponential recursion into polynomial time.
Complexity
| Case | Time | Notes |
|---|---|---|
| States | O(n^3) (moderate) | all (l, r, k) triples |
| Work per state | O(n) (moderate) | the merge loop over m |
| Total | O(n^4) (moderate) | n^3 states times n work |
O(n^3) (moderate)It looks heavy, but n is small for this problem, and memoization keeps it from blowing up. The lesson is the modeling move: when two interval endpoints cannot describe a subproblem, add a dimension that captures the missing context.
When this pattern shows up
When interval DP over dp(l, r) is not enough, ask what extra fact about the boundary you keep needing.
Encoding it as a third index — here, how many equal boxes are glued to the left — is the recurring trick
behind Remove Boxes, Strange Printer, and similar merge-style problems.
The merge branch must clear the inside with dp(l + 1, m - 1, 0) (a fresh run, k = 0) and carry the
glued count into dp(m, r, k + 1). Mixing those up — passing k into the inside, or 0 into the join —
silently computes the wrong score.
Practice
For boxes = [1, 3, 2, 3, 1], why is it worth clearing the middle [3, 2, 3] before touching the two 1s?
1. What does the third index k in dp(l, r, k) represent?
2. Why is a plain dp(l, r) over just two endpoints not enough?
3. What are the two options the recurrence chooses between at box l?
4. What is the overall time complexity?