Backtracking · 7 of 19
Print all palindromic partitions of a string
Cut a word into pieces so that every piece reads the same forwards and backwards, and list every way to do it.
- Simple way
- time O(n * 2^n)
room O(n) - Clever way
- time O(n * 2^n)
room O(n^2)
The problem, simply
A palindrome is a word that reads the same forwards and backwards, like level or noon. A single letter is always a palindrome.
You get a string. Cut it into pieces, keeping the letters in order, so that every piece is a palindrome. Give back every possible way to cut it.
A story to remember it
Picture it
The word nitin as a row of letters. The pieces n, iti and n are each palindromes, and so is the whole word.
- answer
- path
Examples
A short word
- In
s = 'aab'- Out
[[a, a, b], [aa, b]]
'aab' is not a palindrome, and neither is 'ab'. So the last piece is always 'b'.
A palindrome inside a palindrome
- In
s = 'nitin'- Out
[[n, i, t, i, n], [n, iti, n], [nitin]]
Three ways. The middle 'iti' and the whole word are palindromes.
All the same letter
- In
s = 'aaa'- Out
[[a, a, a], [a, aa], [aa, a], [aaa]]
Every piece of a's is a palindrome, so every way to cut works. A word of length n has 2^(n-1) ways to cut it.
One letter
- In
s = 'z'- Out
[[z]]
One piece, and it is a palindrome.
Way 1: the simple way
Try every first piece. If it is a palindrome, keep it and cut the rest the same way. Check each piece with two fingers, one at each end, walking inwards.
- If the start is at the end of the word, the current list of pieces is an answer.
- For every
endfromstartto the last letter, check ifs[start..end]is a palindrome. - If it is, add the piece, cut the rest from
end + 1, then remove the piece again.
import java.util.*;
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> result = new ArrayList<>();
cut(s, 0, new ArrayList<>(), result);
return result;
}
private void cut(String s, int start, List<String> pieces, List<List<String>> result) {
if (start == s.length()) {
result.add(new ArrayList<>(pieces)); // reached the end: one answer
return;
}
for (int end = start; end < s.length(); end++) {
if (isPalindrome(s, start, end)) {
pieces.add(s.substring(start, end + 1));
cut(s, end + 1, pieces, result);
pieces.remove(pieces.size() - 1); // tape the piece back on
}
}
}
private boolean isPalindrome(String s, int lo, int hi) {
while (lo < hi) {
if (s.charAt(lo++) != s.charAt(hi--)) return false;
}
return true;
}
}How long it takes: there are up to 2^(n-1) ways to cut, and each answer costs up to n to check and copy, so O(n * 2^n). The same piece is checked again and again in different branches. Extra room: the list of pieces and the call stack, O(n).
Way 2: the clever way
The same pieces get checked over and over. So first fill a table that answers "is s[i..j] a palindrome?" for every i and j. Then the search reads the table instead of walking the piece.
A piece is a palindrome when its two end letters match and the part inside is a palindrome. A piece of one or two matching letters has nothing inside.
- Fill
pal[i][j]forifrom the last letter down to the first, andjfromito the end:pal[i][j] = s[i] == s[j] && (j - i < 2 || pal[i + 1][j - 1]). - Run the same cutting search, but use
pal[start][end]as the check.
The palindrome table for nitin. Row is where the piece starts, column is where it ends. yes means that piece reads the same both ways.
- answer
Cut off a palindrome, then cut the rest
01 / 15
Cut the word into pieces from left to right. Each piece must read the same both ways.
- pieces
- none
- found
- 0
- looking now
- seen
- done
- answer
- rejected
- path
- just added
import java.util.*;
class Solution {
public List<List<String>> partition(String s) {
int n = s.length();
boolean[][] pal = new boolean[n][n];
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
// ends match, and the inside is empty, one letter, or a palindrome
pal[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 2 || pal[i + 1][j - 1]);
}
}
List<List<String>> result = new ArrayList<>();
cut(s, 0, pal, new ArrayList<>(), result);
return result;
}
private void cut(String s, int start, boolean[][] pal,
List<String> pieces, List<List<String>> result) {
if (start == s.length()) {
result.add(new ArrayList<>(pieces));
return;
}
for (int end = start; end < s.length(); end++) {
if (!pal[start][end]) continue; // one table look-up, no walking
pieces.add(s.substring(start, end + 1));
cut(s, end + 1, pal, pieces, result);
pieces.remove(pieces.size() - 1);
}
}
}How long it takes: the table costs O(n^2). The search is still O(n * 2^n) in the worst case, because the answer itself can be that big (think of aaaa...). But every check is now one look-up. Extra room: the table, O(n^2).
Watch out for
- Adding
piecesitself to the answer. It keeps changing. Add a copy. - Using
substring(start, end)and forgetting it stops beforeend. The piece issubstring(start, end + 1). - The empty string. It has one way to cut: no pieces at all, so return
[[]]. Say what you return.
What interviewers ask next
- Fewest cuts so every piece is a palindrome (Palindrome Partitioning II). Use the same table plus
cuts[j] = min(cuts[i - 1] + 1)over every palindromes[i..j]. That isO(n^2), no backtracking. - Only count the ways.
ways[j]adds upways[i]for every palindromes[i..j-1]. AlsoO(n^2). - Can you build the table without extra rows? Yes, expand around each center, one letter or the gap between two letters, and mark every palindrome you grow. Same
O(n^2). - Longest palindrome inside the string. The table gives it in
O(n^2). Manacher's algorithm does it inO(n).
Remember this
Grown-up words: the pieces form a partition, the mirror words are palindromes, cutting and taping back is backtracking, and the yes or no table is a 2D dynamic programming table.