Palindrome Removal is a classic interval DP problem. It teaches the core interval-DP move: build answers for small ranges first, then combine them into bigger ranges, with a special bonus whenever the two ends of a range match.
Problem. Given an integer array a, in one move you may remove a contiguous palindromic
subarray (after which the remaining pieces join). Return the minimum number of moves to remove
every element.
Example: a = [1, 3, 4, 1, 5] → answer 3. (One efficient plan: remove 3, remove 4, then the
array reads [1, 1, 5]; remove 5, leaving [1, 1], a palindrome you clear in one more move — but
careful accounting gives the true minimum, which the DP computes for you.)
The slow way first
You could try every possible sequence of removals by brute force, but the number of ways to choose and order palindromic deletions explodes combinatorially. That is far too slow.
The question to ask: what is the smallest piece of the problem whose answer is obvious, and how do I build up from there? A single element is always a palindrome, so clearing it costs exactly one move. That tiny fact is the seed for everything else.
The idea: solve every interval, shortest first
Let dp[l][r] be the minimum moves to clear the subarray a[l..r]. Build it up by interval length. For a range:
- By default, split it:
dp[l][r] = min over k of dp[l][k] + dp[k+1][r]. Clear the left part, then the right. - Bonus when
a[l] == a[r]: the two equal ends can be carried out together with whatever palindrome clears the inside, sodp[l][r]may also takedp[l+1][r-1].
Because every range is built from strictly shorter ranges, filling intervals in increasing length guarantees the pieces we need are already computed.
Walk through it
Step through the animation. First the length-1 intervals are seeded to 1. Then a full interval a[0..4] whose ends differ (1 vs 5) is solved purely by splitting. Finally the interval a[0..3] whose ends match (1 and 1) gets the discount: it may ride along with the inside dp[1][2]. The final answer lives in dp[0][n-1].
Pseudocode
dp = n x n table
for each i:
dp[i][i] = 1 # one element is one removal
for length in 2..n:
for each interval [l, r] of that length:
dp[l][r] = dp[l][r-1] + 1 # start: clear last, then the rest
for each split point k in [l, r):
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r])
if a[l] == a[r]: # matching ends -> discount
inner = dp[l+1][r-1] if the inside exists else 0
dp[l][r] = min(dp[l][r], max(inner, 1))
return dp[0][n-1]The Python solution
def min_removals(a):
n = len(a)
dp = [[0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
for length in range(2, n + 1):
for l in range(n - length + 1):
r = l + length - 1
dp[l][r] = dp[l][r - 1] + 1
for k in range(l, r):
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r])
if a[l] == a[r]:
inner = dp[l + 1][r - 1] if r - l >= 2 else 0
dp[l][r] = min(dp[l][r], max(inner, 1))
return dp[0][n - 1]dp[i][i] = 1seeds every single element: one move clears it.- We iterate by
lengthso shorter intervals are always solved before longer ones depend on them. dp[l][r] = dp[l][r-1] + 1is a safe starting guess: cleara[l..r-1], then spend one move ona[r].- The split loop tries every cut point
k, combining two already-solved halves. - The
a[l] == a[r]branch is the interval-DP discount: matching ends remove together with the inside, sodp[l][r]can drop tomax(inner, 1). - The answer is
dp[0][n-1]— the whole array.
Complexity
| Case | Time | Notes |
|---|---|---|
| Filling the table | O(n²) (slow) | one entry per interval (l, r) |
| Split loop per entry | O(n) (moderate) | every cut point k |
| Total | O(n³) (moderate) | n² intervals times an O(n) split scan |
O(n²) (slow)The O(n²) space is the DP table itself — one cell for every interval. The cubic time comes from trying every split point inside every interval.
When this pattern shows up
When a problem asks for an optimum over a sequence and the answer for a range depends on smaller
ranges inside it, reach for interval DP: a 2-D table indexed by [l][r], filled by increasing
length. Matrix-chain multiplication, burst balloons, and palindrome problems are all the same shape.
Order matters: you must fill intervals shortest first, because dp[l][r] reads dp[l+1][r-1] and
the split halves. If you loop l and r in the wrong order, you will read cells that are not filled
yet and get garbage answers.
Practice
For the interval a[0..3] = [1, 3, 4, 1], the ends a[0] and a[3] both equal 1. What extra option does that unlock for dp[0][3]?
1. What does dp[l][r] represent?
2. Why must intervals be filled in increasing length?
3. What special case applies when a[l] == a[r]?
4. What is the time complexity of this solution?