Dynamic Programming · Medium
325. Longest Palindromic Subsequence
Topics: Dynamic Programming, String
Solve it on DeVert Campus - free code runner
Problem
Given a string, find the length of its longest palindromic subsequence - the longest sequence of characters (not necessarily contiguous) that reads the same forwards and backwards.
Input and output
Input: A single line contains the string s.
Output: Print a single integer - the length of the longest palindromic subsequence of s.
Example
Input: bbbab Output: 4
Constraints
1 <= |s| <= 2000
Key observation
Filling the table by INCREASING SUBSTRING LENGTH (not simply by increasing i or j alone) is essential - dp[i][j] depends on strictly SHORTER ranges, so the fill order must respect this dependency to ensure every needed value is already computed.
Intuition
Find the length of the longest subsequence of a string that reads the same forwards and backwards.
Brute force: Try every possible subsequence (2^n of them) and check which are palindromes, taking the longest - this is exponential, impractical for |s| up to 2000.
Optimised: The DP table builds up from SHORTER substrings to LONGER ones, reusing already-computed smaller answers - matching outer characters extends a palindrome found in the middle, while non-matching characters fall back to the best of two slightly smaller sub-problems, avoiding any re-exploration of overlapping ranges.
Plain recursion over substring bounds
Define a recursive function on the substring bounds (i, j): if i > j return 0, if i == j return 1, if s[i] == s[j] return 2 plus the recursive result for (i+1, j-1), otherwise return the maximum of the recursive results for (i+1, j) and (i, j-1). Called without any caching, this recomputes the same (i, j) subproblems repeatedly, giving exponential blowup because the same substring bounds are revisited along many different recursive paths.
Time: O(2^n)
Space: O(n) recursion depth
Top-down memoized recursion
Use the same recursive relation as the brute approach, but store the result for each (i, j) pair the first time it's computed in a 2D memo table, and return the cached value immediately on any later call with the same bounds. Since there are only O(n^2) distinct (i, j) pairs and each is computed once in O(1) work given its subresults, the exponential recursion collapses to polynomial time.
Time: O(n^2)
Space: O(n^2)
Bottom-up DP with rolling 1D array
Observe that the longest palindromic subsequence of s equals the longest common subsequence of s and its reverse, and that computing it via increasing substring length (bottom-up dynamic programming, filling dp[i][j] for smaller lengths first) only ever needs the row for length-1-shorter substrings to compute the current row. Iterate substring lengths from 1 up to n, keeping only the previous length's values in a 1D array and updating in place, using the same match/no-match recurrence as before. This keeps the same O(n^2) time as the full 2D table but reduces the memory footprint to linear.
Time: O(n^2)
Space: O(n)
Step by step
1. dp[i][j] = length of the longest palindromic subsequence within substring s[i..j].
2. Every single character is trivially a palindrome of length 1: dp[i][i]=1.
3. If s[i]==s[j], dp[i][j]=dp[i+1][j-1]+2 (or just 2 when j==i+1); otherwise dp[i][j]=max(dp[i+1][j], dp[i][j-1]) - fill by increasing substring length, since longer ranges depend on shorter ones.
Complexity
Time: O(n²) - filling the DP table, one cell per substring range.
Space: O(n²) for the full table (reducible to O(n) with careful diagonal-order processing, since each cell only depends on cells with shorter ranges).
Edge cases
Test a single-character string (answer 1), a string that is already a palindrome (answer equals |s|), and |s| = 2000 (upper bound) for the O(n^2) DP's performance.
Interview tip
Be ready to connect this directly to LCS: the longest palindromic subsequence of a string equals the LCS of that string and its own REVERSE - an elegant alternative technique worth mentioning alongside the direct interval DP.