The fewest cuts into palindromes
Given a string s, cut it into pieces so that every piece is a palindrome. Return the smallest number of
cuts needed. A string that is already a palindrome needs 0 cuts.
Examples
| Input | Output | Why |
|---|---|---|
s = "aab" | 1 | "aa" | "b". |
s = "aabcbd" | 2 | "aa" | "bcb" | "d". |
s = "racecar" | 0 | Already a palindrome. |
Constraints
- 1 ≤ s.length ≤ 2000
- Lowercase English letters
Hints
Hint 1
Precompute pal[j][i]: is s[j..i] a palindrome? It is when s[j] = s[i] and the inside is.
Hint 2
Let cuts[i] be the fewest cuts for s[0..i]. The last piece is some palindrome s[j..i].
Hint 3
cuts[i] = 0 if s[0..i] is a palindrome, otherwise min over j ≥ 1 with pal[j][i] of cuts[j − 1] + 1.
Where people lose marks
- Testing each candidate piece for being a palindrome from scratch, which makes it O(n³).
- Counting pieces instead of cuts (off by one).
- Greedily taking the longest palindrome first: "aabaa" + "b" style strings defeat it.
Dry run on "aabcbd"
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 6sa0a1b2c3b4d5cuts00·1·2·3·4·5
s[0..0] = "a" is a palindrome: 0 cuts.
-
Step 2 / 6sa0a1b2c3b4d5cuts0001·2·3·4·5
s[0..1] = "aa" is a palindrome: 0 cuts.
-
Step 3 / 6sa0a1b2c3b4d5cuts000112·3·4·5
Best last piece "b" after cuts[1] = 0: 1.
-
Step 4 / 6sa0a1b2c3b4d5cuts00011223·4·5
Best last piece "c" after cuts[2] = 1: 2.
-
Step 5 / 6sa0a1b2c3b4d5cuts0001122314·5
Best last piece "bcb" after cuts[1] = 0: 1.
-
Step 6 / 6sa0a1b2c3b4d5cuts000112231425
Best last piece "d" after cuts[4] = 1: 2. The answer is cuts[5] = 2.
How to think about it
Two dynamic programs. First the palindrome table: pal[j][i] is true when s[j] = s[i] and either
the piece has length at most 3 or pal[j + 1][i − 1]. Then a DP over prefixes: the last piece of an optimal
partition of s[0..i] is some palindrome s[j..i], preceded by an optimal partition of
s[0..j − 1]. So cuts[i] = min(cuts[j − 1] + 1) over palindromic last pieces, or 0 when the whole
prefix is a palindrome. Both tables are filled in one pass if j runs over earlier positions for each i.
Complexity. Time O(n²) · Space O(n²)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def min_cut(s):
n = len(s)
pal = [[False] * n for _ in range(n)] # pal[j][i]: is s[j..i] a palindrome
cuts = [0] * n # cuts[i]: fewest cuts for s[0..i]
for i in range(n):
best = i # cut between every character
for j in range(i + 1):
if s[j] == s[i] and (i - j < 3 or pal[j + 1][i - 1]):
pal[j][i] = True
best = 0 if j == 0 else min(best, cuts[j - 1] + 1)
cuts[i] = best
return cuts[n - 1]#include <algorithm>
#include <string>
#include <vector>
int solve(std::string s) {
const int n = s.size();
std::vector<std::vector<bool>> pal(n, std::vector<bool>(n, false));
std::vector<int> cuts(n, 0);
for (int i = 0; i < n; ++i) {
int best = i;
for (int j = 0; j <= i; ++j)
if (s[j] == s[i] && (i - j < 3 || pal[j + 1][i - 1])) {
pal[j][i] = true; // s[j..i] is a palindrome
best = j == 0 ? 0 : std::min(best, cuts[j - 1] + 1);
}
cuts[i] = best;
}
return cuts[n - 1];
}class Solution {
public int minCut(String s) {
int n = s.length();
boolean[][] pal = new boolean[n][n];
int[] cuts = new int[n];
for (int i = 0; i < n; i++) {
int best = i;
for (int j = 0; j <= i; j++)
if (s.charAt(j) == s.charAt(i) && (i - j < 3 || pal[j + 1][i - 1])) {
pal[j][i] = true; // s[j..i] is a palindrome
best = j == 0 ? 0 : Math.min(best, cuts[j - 1] + 1);
}
cuts[i] = best;
}
return cuts[n - 1];
}
}function minCut(s) {
const n = s.length;
const pal = Array.from({ length: n }, () => new Array(n).fill(false));
const cuts = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
let best = i;
for (let j = 0; j <= i; j++) {
if (s[j] === s[i] && (i - j < 3 || pal[j + 1][i - 1])) {
pal[j][i] = true; // s[j..i] is a palindrome
best = j === 0 ? 0 : Math.min(best, cuts[j - 1] + 1);
}
}
cuts[i] = best;
}
return cuts[n - 1];
}fn solve(s: &str) -> i64 {
let c: Vec<u8> = s.bytes().collect();
let n = c.len();
let mut pal = vec![vec![false; n]; n];
let mut cuts = vec![0i64; n];
for i in 0..n {
let mut best = i as i64;
for j in 0..=i {
if c[j] == c[i] && (i - j < 3 || pal[j + 1][i - 1]) {
pal[j][i] = true; // s[j..i] is a palindrome
best = if j == 0 { 0 } else { best.min(cuts[j - 1] + 1) };
}
}
cuts[i] = best;
}
cuts[n - 1]
}import Data.Array
solve :: String -> Int
solve s = cuts ! (n - 1)
where
n = length s
a = listArray (0, n - 1) s
-- lazy tables: pal ! (j, i) and cuts ! i refer only to entries already defined in terms of smaller ones
pal = array ((0, 0), (n - 1, n - 1)) [((j, i), j <= i && a ! j == a ! i && (i - j < 3 || pal ! (j + 1, i - 1))) | j <- [0 .. n - 1], i <- [0 .. n - 1]] :: Array (Int, Int) Bool
cuts = listArray (0, n - 1) [best i | i <- [0 .. n - 1]] :: Array Int Int
best i
| pal ! (0, i) = 0
| otherwise = minimum [cuts ! (j - 1) + 1 | j <- [1 .. i], pal ! (j, i)]
Variants to try
- Return one optimal partition.
- List every partition into palindromes (backtracking with the same table).
- Do it in O(n) memory by expanding around centres instead of storing pal.
Next problems
The cheapest way up a staircase
Each step has a cost; climb one or two steps at a time; start on step 0 or 1. Return the cheapest way past the top.
Open the problem → Medium15 minCan a subset reach the target?
Decide whether some subset of positive integers adds up to exactly the target. A boolean array over totals, filled downwards for each number.
Open the problem → Medium15 minCount the palindromic substrings
Count every contiguous substring that reads the same backwards, counting different positions separately. Each successful expansion step is one more.
Open the problem →Write a solution and run it against the real test table.
Keep going
The cheapest way up a staircase
Each step has a cost; climb one or two steps at a time; start on step 0 or 1. Return the cheapest way past the top.
Open the problem → Medium15 minCan a subset reach the target?
Decide whether some subset of positive integers adds up to exactly the target. A boolean array over totals, filled downwards for each number.
Open the problem → Medium15 minCount the palindromic substrings
Count every contiguous substring that reads the same backwards, counting different positions separately. Each successful expansion step is one more.
Open the problem → Dynamic programmingThe pattern behind it
Write the recursion, notice the repeats, fill a table in an order that makes each entry final.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.