Topics › Dynamic Programming › DP on Strings
Longest palindromic subsequence
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Given a string s, return the length of its longest palindromic subsequence: the longest sequence of characters, taken from s in order but not necessarily adjacent, that reads the same forwards and backwards.
Constraints (LeetCode 516): 1 ≤ n ≤ 1000, lowercase English letters. The Java methods also accept the empty string.
Example 1
s = "bbbab"
answer = 4 // "bbbb": skip the 'a'
Example 2
s = "cbbd"
answer = 2 // "bb"
The recurrence on a range
Let f(i, j) be the answer for the range s[i..j] (both ends included). Compare the two ends:
s[i] == s[j]. Use them as the outer pair:f(i, j) = 2 + f(i + 1, j - 1). This is safe because any palindrome inside the range can be rewritten to start atiand end atj. Its first and last characters are equal; replace them withs[i]ands[j]. Both ends still carry the same letter, so it is still a palindrome, the positions are still in order, and the length is unchanged. Everything between them lies insides[i + 1..j - 1]. (A one-character palindrome is beaten by the pair itself.)s[i] != s[j]. They cannot both be used: in a palindrome the first character equals the last, and if both ends were used they would have to be that first and last character. So drop one:f(i, j) = max(f(i + 1, j), f(i, j - 1)).
Base cases: an empty range (i > j) gives 0, a single character (i == j) gives 1.
Approach 1: Recursion
Intuition. Implement the recurrence directly, starting from the full range (0, n - 1).
Algorithm.
- If
i > jreturn 0; ifi == jreturn 1. - If the ends match, return
2 + f(i + 1, j - 1). - Otherwise return
max(f(i + 1, j), f(i, j - 1)).
static int recursive(String s) {
return solve(0, s.length() - 1, s);
}
/** Longest palindromic subsequence inside s[i..j] (inclusive). */
static int solve(int i, int j, String s) {
if (i > j) return 0; // empty range
if (i == j) return 1; // a single character is a palindrome
if (s.charAt(i) == s.charAt(j)) return 2 + solve(i + 1, j - 1, s); // use both ends as the outer pair
return Math.max(solve(i + 1, j, s), solve(i, j - 1, s)); // at least one end is unused: drop it
}
Complexity. Time O(2^n): on a string with no repeated letters every call branches twice and each branch shrinks the range by one. Space O(n) for the stack.
Approach 2: Memoization
Intuition. A range is identified by (i, j), so there are only about n^2 / 2 of them, but both branches of a mismatch reach the range (i + 1, j - 1) again. Cache every answer; -1 marks “not computed”.
Algorithm.
dp[n][n]filled with-1.- Same recursion, returning cached values and storing new ones.
static int memoization(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for (int[] row : dp) Arrays.fill(row, -1);
return memo(0, n - 1, s, dp);
}
static int memo(int i, int j, String s, int[][] dp) {
if (i > j) return 0;
if (i == j) return 1;
if (dp[i][j] != -1) return dp[i][j];
if (s.charAt(i) == s.charAt(j)) return dp[i][j] = 2 + memo(i + 1, j - 1, s, dp);
return dp[i][j] = Math.max(memo(i + 1, j, s, dp), memo(i, j - 1, s, dp));
}
Complexity. Time O(n^2), space O(n^2) plus O(n) stack.
Approach 3: Tabulation
Intuition. f(i, j) reads f(i + 1, ·) (a shorter range starting later) and f(i, j - 1) (a shorter range in the same row). So fill rows with i going down from n - 1 to 0, and within a row let j go up from i + 1. Cells below the diagonal (i > j) are empty ranges and stay 0, which is exactly what dp[i + 1][j - 1] must return when j == i + 1.
Algorithm.
- For
i = n - 1 .. 0: setdp[i][i] = 1; forj = i + 1 .. n - 1apply the recurrence. - Return
dp[0][n - 1].
static int tabulation(String s) {
int n = s.length();
if (n == 0) return 0;
int[][] dp = new int[n][n]; // entries with i > j stay 0 (empty range)
for (int i = n - 1; i >= 0; i--) {
dp[i][i] = 1;
for (int j = i + 1; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) dp[i][j] = 2 + dp[i + 1][j - 1];
else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
return dp[0][n - 1];
}
Dry run on s = "bbbab" (row i, column j, rows filled bottom to top):
| i \ j | 0 b | 1 b | 2 b | 3 a | 4 b |
|---|---|---|---|---|---|
| 0 b | 1 | 2 | 3 | 3 | 4 |
| 1 b | 1 | 2 | 2 | 3 | |
| 2 b | 1 | 1 | 3 | ||
| 3 a | 1 | 1 | |||
| 4 b | 1 |
For example dp[2][4]: b == b, so 2 + dp[3][3] = 3 (“bab”). dp[0][4]: b == b, so 2 + dp[1][3] = 2 + 2 = 4 (“bbbb”).
Complexity. Time O(n^2), space O(n^2).
Approach 4: Space optimized
Intuition. Row i reads only row i + 1 and itself. Keep next (row i + 1) and cur (row i), swapping after each row. The one subtle read is next[j - 1] when j == i + 1: that cell is the empty range (i + 1, i), which must be 0. With two recycled arrays it happens to be 0 anyway, but the code says so explicitly rather than relying on which stale values the array holds.
Algorithm.
- For
i = n - 1 .. 0:cur[i] = 1; forj > icompute fromnextandcur; swap. - Return
next[n - 1](after the final swap,nextis row 0).
static int spaceOptimized(String s) {
int n = s.length();
if (n == 0) return 0;
int[] next = new int[n], cur = new int[n]; // next = row i + 1, cur = row i
for (int i = n - 1; i >= 0; i--) {
cur[i] = 1;
for (int j = i + 1; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) cur[j] = 2 + (j - 1 > i ? next[j - 1] : 0); // j == i + 1: nothing inside
else cur[j] = Math.max(next[j], cur[j - 1]);
}
int[] tmp = next; next = cur; cur = tmp;
}
return next[n - 1];
}
Complexity. Time O(n^2), space O(n).
Approach 5: Reduce to LCS
Intuition. This is the route the A2Z sheet takes: the answer equals the Longest common subsequence of s and reverse(s).
- A palindromic subsequence of
s, read backwards, is itself, so it is also a subsequence ofreverse(s). Hence LPS ≤ LCS. - Not every common subsequence is a palindrome (
"ab"is common to"abca"and"acba"), but a long one always contains a long palindrome. Let thek-th character of a common subsequence of lengthLsit at positiona_kinsand, read through the reverse, at positionb_kins; thea_kincrease and theb_kdecrease. Say the firsttpairs havea_k ≤ b_k. They are nested (a_1 < … < a_t ≤ b_t < … < b_1), so their characters spell a palindrome of length2t, or2t - 1if the innermost pair shares a position. The remaining pairs are nested the other way and spell a palindrome of length2(L - t). If2t - 1 < Lthent ≤ L / 2and the second one has length at leastL. Either way a palindrome of length at leastLexists, so LCS ≤ LPS.
So the two lengths are equal, and the two-row LCS code answers the problem.
static int viaLcs(String s) {
String r = new StringBuilder(s).reverse().toString();
int n = s.length();
int[] prev = new int[n + 1], cur = new int[n + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (s.charAt(i - 1) == r.charAt(j - 1)) cur[j] = 1 + prev[j - 1];
else cur[j] = Math.max(prev[j], cur[j - 1]);
}
int[] tmp = prev; prev = cur; cur = tmp;
}
return prev[n];
}
Complexity. Time O(n^2), space O(n). It does about twice the work of Approach 4, because it fills a full n × n table instead of the upper triangle, but it reuses code you already trust.
Edge cases and pitfalls
- Empty string.
recursiveandmemoizationstart at(0, -1), an empty range, and return 0; the tabulated versions return 0 before allocating. - All distinct letters. The answer is 1: any single character.
- Already a palindrome.
"racecar"returns its full length 7. - Subsequence vs substring.
"agbdba"has a palindromic subsequence"abdba"of length 5, but its longest palindromic substring is"bdb", length 3. - Loop order. Iterating
iupward readsdp[i + 1][·]before it is filled. The row index must go fromn - 1down. - Testing. Besides the fixed cases, 300 seeded random strings over
{a, b, c}are run through all five methods.
Related problems
- Longest common subsequence is the building block of Approach 5.
- Minimum insertions to make string palindrome - DP-29 is
nminus this answer. - Longest Palindromic Substring requires the palindrome to be contiguous.
- Palindrome partitioning II uses a similar range table to decide which substrings are palindromes.
- Count Palindromic Subsequences counts palindromic subsequences with a similar two-ends recurrence.