Split a string into every possible list of substrings where each piece is a palindrome. The trick is simple: at each position, try every prefix — and only recurse if that prefix passes a two-pointer palindrome check. Everything else is pruned before the recursive call ever happens.
Given a string s, return all ways to partition it so that every substring in the partition is a palindrome. Example: s = "aab" → [["a","a","b"],["aa","b"]]. Both partitions cover the whole string and every piece reads the same forwards and backwards. The input s = "a" has exactly one partition: [["a"]].
end. For each candidate prefix s[start..end], run a two-pointer check in O(end − start) time. If it is not a palindrome, skip — do not recurse. This prunes the entire subtree before any recursive cost is paid. Only palindrome prefixes open a new branch.start === s.length, the entire string is consumed — push a copy of path into result.end from start to s.length - 1. Each iteration considers the prefix s[start..end] as the next piece.l = start, r = end moving inward. If any pair mismatches, continue to the next end. This prunes the branch without recursing.s.slice(start, end + 1) onto path, recurse with backtrack(end + 1, path), then path.pop() to restore state.path.pop()after the recursive call. Without it, the path accumulates stale entries and every subsequent partition is wrong. The invariant is: push before recurse, pop after recurse — always.Both approaches share an O(n · 2ⁿ) worst-case bound — the number of palindrome partitions can itself be exponential (e.g. "aaa…a"). The improvement from early pruning is practical rather than asymptotic. A DP precomputation of all isPalin[l][r] values reduces each check to O(1) at the cost of O(n²) preprocessing and space — useful when the string is long.
"aab". We try every cut point; only recurse when the prefix is a palindrome.1▶function partition(s: string): string[][] {2▶ const result: string[][] = [];34 function isPalin(l: number, r: number): boolean {5 while (l < r) {6 if (s[l] !== s[r]) return false;7 l++;8 r--;9 }10 return true;11 }1213▶ function backtrack(start: number, path: string[]): void {14 if (start === s.length) {15 result.push([...path]);16 return;17 }18 for (let end = start; end < s.length; end++) {19 if (!isPalin(start, end)) continue; // prune non-palindrome prefixes20 path.push(s.slice(start, end + 1));21 backtrack(end + 1, path);22 path.pop();23 }24 }2526▶ backtrack(0, []);27 return result;28}
function partition(s: string): string[][] {
const result: string[][] = [];
function isPalin(l: number, r: number): boolean {
while (l < r) {
if (s[l] !== s[r]) return false;
l++;
r--;
}
return true;
}
function backtrack(start: number, path: string[]): void {
if (start === s.length) {
result.push([...path]);
return;
}
for (let end = start; end < s.length; end++) {
if (!isPalin(start, end)) continue; // prune non-palindrome prefixes
path.push(s.slice(start, end + 1));
backtrack(end + 1, path);
path.pop();
}
}
backtrack(0, []);
return result;
}isPalin. Two inward-moving pointers starting at l and r. If any pair differs it returns false immediately; otherwise true. O(r − l) per call — no extra space.backtrack(start, path). The main recursive worker. start is the next unconsumed index; path holds the palindromes chosen so far.start === s.length the entire string is covered. We push a copy of path (not the mutable reference) into result.end from start onward: if isPalin(start, end) is false, continue — the branch dies without a recursive call. Otherwise push the slice, recurse from end + 1, and pop to restore path.dp[l][r] table in O(n²) reduces each palindrome check to O(1) without changing the output size.pal[l][r] with DP in O(n²) time and space. Each isPalin call then becomes O(1), which helps when s is long.cuts[i] = min(cuts[j] + 1) for every palindrome s[j+1..i].n frames (worst case: every single character is its own palindrome). Stack overflow is not a concern for typical constraints (n ≤ 16)."a" (palindrome → recurse), from index 1: "b" (palindrome → recurse), from index 2: "a" (palindrome → base case) → partition ["a","b","a"]; then back to index 1: "ba" (not palindrome, skip); then from index 0: "ab" (not palindrome), then "aba" (palindrome) → base case → ["aba"]. Two total partitions.| "all partitions where each piece is a palindrome" | backtracking over cut points |
| generating all valid decompositions of a string | try prefix, validate, recurse on suffix |
| palindrome substring check in a loop | two-pointer l/r check, O(n) per call |
| "minimum cuts" variant of same problem | 1D DP — LC 132, not backtracking |
function partition(s: string): string[][] {
const result: string[][] = [];
function isPalin(l: number, r: number): boolean {
while (l < r) { if (s[l] !== s[r]) return false; l++; r--; }
return true;
}
function backtrack(start: number, path: string[]): void {
if (start === s.length) { result.push([...path]); return; }
for (let end = start; end < s.length; end++) {
if (!isPalin(start, end)) continue;
path.push(s.slice(start, end + 1));
backtrack(end + 1, path);
path.pop();
}
}
backtrack(0, []);
return result;
}start === s.length — the entire string is consumed, so path is a complete valid partition. Push a copy into result.partition("aab")?backtrack(end + 1, path) returns, what must happen next?