Last Updated: March 28, 2026
We need to find the length of the longest subsequence of s that reads the same forwards and backwards. A subsequence doesn't have to be contiguous, so we can skip characters as long as we keep the relative order.
For example, in "bbbab", the subsequence "bbbb" (characters at indices 0, 1, 2, 4) is a palindrome of length 4. We can't do better than that, so the answer is 4.
The key observation is this: if the first and last characters of a string match, they can both be part of the palindromic subsequence, and we recurse on the substring between them. If they don't match, we try dropping either the first or the last character and take the better result. This naturally leads to a recursive structure with overlapping subproblems, which is a strong signal for dynamic programming.
1 <= s.length <= 1000 --> With n up to 1000, O(n^2) gives us 1 million operations, which is comfortable. O(n^3) would mean 1 billion, which is too slow. So we need O(n^2) or better.s consists only of lowercase English letters --> 26 possible characters. This limits the alphabet size but doesn't change our approach significantly.Let's start from the recursive structure. Consider the substring s[i..j]. We want the longest palindromic subsequence in this range.
If s[i] == s[j], both characters can sit on opposite ends of our palindrome. We take them both and recurse on s[i+1..j-1], adding 2 to the result.
If s[i] != s[j], at least one of them can't be in the palindrome. So we try two options: skip the left character (solve s[i+1..j]) or skip the right character (solve s[i..j-1]), and take the maximum.
The base cases are simple: a single character is always a palindrome of length 1, and an empty range gives 0.
Without memoization, this recursion explores overlapping subproblems repeatedly. For example, solving s[2..5] might be needed when we drop the left from s[1..5] and also when we drop the right from s[2..6]. Memoization caches these results so each subproblem is solved only once.
solve(i, j) that returns the longest palindromic subsequence in s[i..j].i > j, return 0. If i == j, return 1.s[i] == s[j], return solve(i+1, j-1) + 2.max(solve(i+1, j), solve(i, j-1)).(i, j) pair.solve(0, n-1) for the final answer.The recursion stack can go up to O(n) deep, adding overhead from function calls. What if we filled the DP table iteratively, bottom-up, eliminating all recursion overhead?
The memoization approach reveals the subproblem structure clearly: dp[i][j] depends on dp[i+1][j-1], dp[i+1][j], and dp[i][j-1]. All three involve either incrementing i or decrementing j (or both). So if we fill the table in the right order, every dependency is already computed when we need it.
The trick is to iterate by substring length. Start with substrings of length 1 (single characters, all palindromes of length 1), then length 2, then length 3, and so on. For each length, sweep across all starting positions. By the time we compute dp[i][j] for a substring of length len, all shorter substrings are already filled in.
dp of size n x n, initialized to 0.dp[i][i] = 1 for all i (every single character is a palindrome).len from 2 to n:i from 0 to n - len:j = i + len - 1 (the ending index).s[i] == s[j], set dp[i][j] = dp[i+1][j-1] + 2.dp[i][j] = max(dp[i+1][j], dp[i][j-1]).dp[0][n-1].We're using O(n^2) space for the DP table, but when computing row i, we only need row i+1. Can we reduce space from O(n^2) to O(n) by keeping just one row?
Look at the dependencies again. dp[i][j] depends on dp[i+1][j-1] (row below, one column left), dp[i+1][j] (row below, same column), and dp[i][j-1] (same row, one column left). So when filling row i, we only ever look at row i+1. Once we're done with row i, we never need row i+2 or any earlier row again. This means we can compress the entire 2D table into a single 1D array of length n.
The trick is careful handling of the dp[i+1][j-1] dependency. When we update dp[j] in our 1D array (processing left to right), we're overwriting what used to be the old row's value. We need to save that old value in a prev variable before it gets overwritten.
dp of size n, initialized to 1 (each character is a palindrome of length 1).i from n-2 down to 0 (processing rows bottom to top):prev = 0 to store the old dp[j-1] before it's overwritten.j from i+1 to n-1:temp = dp[j] (this is the old dp[i+1][j]).s[i] == s[j], set dp[j] = prev + 2.dp[j] = max(dp[j], dp[j-1]).prev = temp.dp[n-1].