Dynamic Programming · 41 of 60
Longest Palindromic Subsequence
Cross out as few letters as you can so the letters left read the same forwards and backwards, and say how many are left.
- Simple way
- time O(2^n)
room O(n) - Clever way
- time O(n^2)
room O(n^2)
The problem, simply
You get a word. You may cross out any letters, but you may not move the ones that stay. The letters that stay must read the same from left to right and from right to left, like "level" or "abba".
What is the most letters you can keep? A word that reads the same both ways is a palindrome, and letters kept in order with gaps allowed are a subsequence. So this is the longest palindromic subsequence.
A story to remember it
Picture it
The word agbcba. Crossing out g leaves a b c b a, which reads the same both ways.
- answer
- rejected
Examples
A normal case
- In
s = 'agbcba'- Out
5
Cross out g to keep a b c b a.
Repeated letters
- In
s = 'bbbab'- Out
4
Cross out the a to keep b b b b.
No two letters match
- In
s = 'abcd'- Out
1
Any single letter reads the same both ways, so the answer is at least 1 for any word that is not empty.
Already a mirror word
- In
s = 'racecar'- Out
7
Nothing needs to be crossed out.
Empty word
- In
s = ''- Out
0
Nothing to keep. Guard the table code, which reads dp[0][n - 1].
Way 1: the simple way
Write f(i, j) for the best answer on the letters from i to j.
- If
i > j, the piece is empty: 0. Ifi == j, one letter: 1. - If the end letters
s[i]ands[j]match, keep both:2 + f(i + 1, j - 1). - Otherwise drop one end:
max(f(i + 1, j), f(i, j - 1)).
The two tries in step 3 overlap. Dropping the left end and then the right end lands on the same piece as dropping the right end first.
Calls for agbcba, written as (left end, right end). Green circles had equal ends and made one call. Faded circles are a single letter. Circles marked again were solved already on another branch.
- done
- rejected
- ignored
class Solution {
public int longestPalinSubseq(String s) {
return f(s, 0, s.length() - 1);
}
private int f(String s, int i, int j) {
if (i > j) return 0; // empty piece
if (i == j) return 1; // one letter
if (s.charAt(i) == s.charAt(j)) {
return 2 + f(s, i + 1, j - 1); // keep both ends
}
return Math.max(f(s, i + 1, j), f(s, i, j - 1)); // drop one end
}
}How long it takes: every mismatch splits into two calls, O(2^n) in the worst case. Extra room: the call chain is at most n deep, O(n).
Way 2: the clever way
There are only about n * n / 2 pieces (i to j). Put each answer in a table dp[i][j]. A piece only needs answers for shorter pieces inside it, so fill the table from short pieces to long ones.
- Every single letter:
dp[i][i] = 1. - For lengths 2, 3, and so on up to
n, for every starti(withj = i + len - 1): - If
s[i] == s[j], thendp[i][j] = dp[i + 1][j - 1] + 2. - Else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]). - The answer is
dp[0][n - 1], the top right box.
In the table, the box one row down is "drop the left end", the box one column left is "drop the right end", and the box down and left is "the inside".
Fill the mirror table from short pieces to long ones
01 / 12
Box (i, j) is the answer for the piece from letter i to letter j. The faded boxes below the diagonal mean nothing, since a piece cannot end before it starts. One letter alone is a mirror word of length 1, so the diagonal is all 1.
- looking now
- checking
- answer
- just added
- ignored
class Solution {
public int longestPalinSubseq(String s) {
int n = s.length();
if (n == 0) return 0;
int[][] dp = new int[n][n];
for (int i = 0; i < n; i++) dp[i][i] = 1; // one letter
for (int len = 2; len <= n; len++) { // short pieces first
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
if (s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i + 1][j - 1] + 2; // keep both ends
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); // drop one end
}
}
}
return dp[0][n - 1];
}
}When len is 2 and the ends match, dp[i + 1][j - 1] is a box below the diagonal. Java fills it with 0, which is the right answer for an empty middle.
How long it takes: about n^2 / 2 boxes, one quick step each, O(n^2). Extra room: the table, O(n^2). Row i only needs row i + 1, so if you loop i from n - 1 down to 0 you can keep two rows, O(n).
Watch out for
- Subsequence, not substring. "agbcba" has the palindromic substring "bcb" of length 3, but the subsequence answer is 5.
- On a match it is
+ 2, except for a single letter, which is 1. The base casedp[i][i] = 1handles that. - Empty string: return 0 before reading
dp[0][n - 1].
What interviewers ask next
- Another way to see it? Yes: it equals the longest common subsequence of the word and the word reversed.
- Fewest deletions to make a palindrome:
n - LPS. Fewest insertions to make a palindrome is the same number. - Print the palindrome itself: walk from
dp[0][n - 1]. On a match take both ends and move inward; otherwise move toward the bigger neighbor. - Count all palindromic subsequences: same table shape, but add counts instead of taking a max, and subtract the double counted middle part.
- Longest palindromic substring is a different problem. There the middle must be a full palindrome too, and expand around center solves it in
O(n^2)time withO(1)room.
Remember this
Grown-up words: the mirror bracelet is a palindrome, keeping beads in order is a subsequence, the chart over (start, end) pairs is interval dynamic programming, and the repeated stretches are overlapping subproblems.