Before attempting this problem, you should be comfortable with:
Dynamic Programming (2D) - The solution requires a 2D DP table to track palindrome lengths for substrings defined by two indices
Recursion with Memoization - Top-down approaches use recursive calls with caching to avoid recomputation
Longest Common Subsequence (LCS) - One approach reduces this problem to finding the LCS between the string and its reverse
String Manipulation - Understanding how to work with substrings and character comparisons
1. Dynamic Programming (Top Down)
Intuition
A palindrome reads the same forwards and backwards, so we can think of building one by expanding outward from a center. For each possible center (single character for odd length, between two characters for even length), we try to extend the palindrome by checking if the characters on both sides match.
If the characters match, we include both in our subsequence and continue expanding. If they don't match, we have a choice: skip the left character or skip the right character, and take whichever gives a longer result. Memoization prevents us from recomputing the same subproblems.
Algorithm
Create a 2D memoization table dp where dp[i][j] stores the longest palindromic subsequence that can be formed starting at index i and ending at index j.
Define a recursive function dfs(i, j) that:
Returns 0 if indices are out of bounds.
Returns the cached result if already computed.
If characters at i and j match, adds 1 (if same index) or 2 (different indices) plus the result of expanding outward.
Otherwise, takes the maximum of skipping either the left or right character.
Call dfs for all possible centers (both odd and even length palindromes).
classSolution:deflongestPalindromeSubseq(self, s:str)->int:
n =len(s)
dp =[[-1]* n for _ inrange(n)]defdfs(i, j):if i <0or j == n:return0if dp[i][j]!=-1:return dp[i][j]if s[i]== s[j]:
length =1if i == j else2
dp[i][j]= length + dfs(i -1, j +1)else:
dp[i][j]=max(dfs(i -1, j), dfs(i, j +1))return dp[i][j]for i inrange(n):
dfs(i, i)# odd length
dfs(i, i +1)# even lengthreturnmax(max(row)for row in dp if row !=-1)
publicclassSolution{privateint[][] dp;publicintlongestPalindromeSubseq(String s){int n = s.length();
dp =newint[n][n];for(int[] row : dp){Arrays.fill(row,-1);}for(int i =0; i < n; i++){dfs(i, i, s);// Odd lengthdfs(i, i +1, s);// Even length}int maxLength =0;for(int[] row : dp){for(int val : row){
maxLength =Math.max(maxLength, val);}}return maxLength;}privateintdfs(int i,int j,String s){if(i <0|| j == s.length()){return0;}if(dp[i][j]!=-1){return dp[i][j];}if(s.charAt(i)== s.charAt(j)){int length =(i == j)?1:2;
dp[i][j]= length +dfs(i -1, j +1, s);}else{
dp[i][j]=Math.max(dfs(i -1, j, s),dfs(i, j +1, s));}return dp[i][j];}}
classSolution{private:
vector<vector<int>> dp;public:intlongestPalindromeSubseq(string s){int n = s.size();
dp.resize(n,vector<int>(n,-1));for(int i =0; i < n; i++){dfs(i, i, s);// Odd lengthdfs(i, i +1, s);// Even length}int maxLength =0;for(constauto& row : dp){for(int val : row){
maxLength =max(maxLength, val);}}return maxLength;}private:intdfs(int i,int j,const string& s){if(i <0|| j == s.size()){return0;}if(dp[i][j]!=-1){return dp[i][j];}if(s[i]== s[j]){int length =(i == j)?1:2;
dp[i][j]= length +dfs(i -1, j +1, s);}else{
dp[i][j]=max(dfs(i -1, j, s),dfs(i, j +1, s));}return dp[i][j];}};
classSolution{/**
* @param {string} s
* @return {number}
*/longestPalindromeSubseq(s){const n = s.length;const dp = Array.from({length: n },()=>Array(n).fill(-1));constdfs=(i, j)=>{if(i <0|| j === n){return0;}if(dp[i][j]!==-1){return dp[i][j];}if(s[i]=== s[j]){const length = i === j ?1:2;
dp[i][j]= length +dfs(i -1, j +1);}else{
dp[i][j]= Math.max(dfs(i -1, j),dfs(i, j +1));}return dp[i][j];};for(let i =0; i < n; i++){dfs(i, i);// Odd lengthdfs(i, i +1);// Even length}let maxLength =0;for(const row of dp){for(const val of row){
maxLength = Math.max(maxLength, val);}}return maxLength;}}
publicclassSolution{privateint[,] dp;publicintLongestPalindromeSubseq(string s){int n = s.Length;
dp =newint[n, n];for(int i =0; i < n; i++){for(int j =0; j < n; j++){
dp[i, j]=-1;}}for(int i =0; i < n; i++){Dfs(i, i, s);// Odd lengthDfs(i, i +1, s);// Even length}int maxLength =0;for(int i =0; i < n; i++){for(int j =0; j < n; j++){
maxLength = Math.Max(maxLength, dp[i, j]);}}return maxLength;}privateintDfs(int i,int j,string s){if(i <0|| j == s.Length){return0;}if(dp[i, j]!=-1){return dp[i, j];}if(s[i]== s[j]){int length =(i == j)?1:2;
dp[i, j]= length +Dfs(i -1, j +1, s);}else{
dp[i, j]= Math.Max(Dfs(i -1, j, s),Dfs(i, j +1, s));}return dp[i, j];}}
funclongestPalindromeSubseq(s string)int{
n :=len(s)
dp :=make([][]int, n)for i :=range dp {
dp[i]=make([]int, n)for j :=range dp[i]{
dp[i][j]=-1}}var dfs func(i, j int)int
dfs =func(i, j int)int{if i <0|| j == n {return0}if dp[i][j]!=-1{return dp[i][j]}if s[i]== s[j]{
length :=2if i == j {
length =1}
dp[i][j]= length +dfs(i-1, j+1)}else{
dp[i][j]=max(dfs(i-1, j),dfs(i, j+1))}return dp[i][j]}for i :=0; i < n; i++{dfs(i, i)// Odd lengthdfs(i, i+1)// Even length}
maxLength :=0for i :=0; i < n; i++{for j :=0; j < n; j++{if dp[i][j]> maxLength {
maxLength = dp[i][j]}}}return maxLength
}funcmax(a, b int)int{if a > b {return a
}return b
}
class Solution {privatelateinitvar dp: Array<IntArray>funlongestPalindromeSubseq(s: String): Int {val n = s.length
dp =Array(n){IntArray(n){-1}}for(i in0 until n){dfs(i, i, s)// Odd lengthdfs(i, i +1, s)// Even length}var maxLength =0for(row in dp){for(value in row){
maxLength =maxOf(maxLength, value)}}return maxLength
}privatefundfs(i: Int, j: Int, s: String): Int {if(i <0|| j == s.length){return0}if(dp[i][j]!=-1){return dp[i][j]}
dp[i][j]=if(s[i]== s[j]){val length =if(i == j)1else2
length +dfs(i -1, j +1, s)}else{maxOf(dfs(i -1, j, s),dfs(i, j +1, s))}return dp[i][j]}}
classSolution{funclongestPalindromeSubseq(_ s:String)->Int{let chars =Array(s)let n = chars.count
var dp =[[Int]](repeating:[Int](repeating:-1, count: n), count: n)funcdfs(_ i:Int,_ j:Int)->Int{if i <0|| j == n {return0}if dp[i][j]!=-1{return dp[i][j]}if chars[i]== chars[j]{let length =(i == j)?1:2
dp[i][j]= length +dfs(i -1, j +1)}else{
dp[i][j]=max(dfs(i -1, j),dfs(i, j +1))}return dp[i][j]}for i in0..<n {_=dfs(i, i)// Odd length_=dfs(i, i +1)// Even length}var maxLength =0for row in dp {for val in row {
maxLength =max(maxLength, val)}}return maxLength
}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n2)
2. Dynamic Programming (Top-Down Optimized)
Intuition
Instead of expanding from centers, we can approach this problem by considering the entire string and shrinking inward. We define our subproblem as finding the longest palindromic subsequence within the substring from index i to index j.
When the first and last characters match, they can both be part of our palindrome, so we add 2 and solve for the inner substring. When they don't match, one of them must be excluded, so we try both options and take the better one.
Algorithm
Create a memoization cache (hash map or 2D array) to store computed results.
Define a recursive function dfs(i, j) that:
Returns 0 if i > j (empty substring).
Returns 1 if i == j (single character is a palindrome of length 1).
Returns the cached result if already computed.
If s[i] == s[j], returns 2 + dfs(i+1, j-1).
Otherwise, returns max(dfs(i+1, j), dfs(i, j-1)).
Call dfs(0, n-1) to get the answer for the entire string.
funclongestPalindromeSubseq(s string)int{
n :=len(s)
dp :=make([][]int, n)for i :=range dp {
dp[i]=make([]int, n)for j :=range dp[i]{
dp[i][j]=-1}}var dfs func(i, j int)int
dfs =func(i, j int)int{if i > j {return0}if i == j {return1}if dp[i][j]!=-1{return dp[i][j]}if s[i]== s[j]{
dp[i][j]=dfs(i+1, j-1)+2}else{
dp[i][j]=max(dfs(i+1, j),dfs(i, j-1))}return dp[i][j]}returndfs(0, n-1)}funcmax(a, b int)int{if a > b {return a
}return b
}
class Solution {privatelateinitvar dp: Array<IntArray>funlongestPalindromeSubseq(s: String): Int {val n = s.length
dp =Array(n){IntArray(n){-1}}returndfs(0, n -1, s)}privatefundfs(i: Int, j: Int, s: String): Int {if(i > j){return0}if(i == j){return1}if(dp[i][j]!=-1){return dp[i][j]}
dp[i][j]=if(s[i]== s[j]){dfs(i +1, j -1, s)+2}else{maxOf(dfs(i +1, j, s),dfs(i, j -1, s))}return dp[i][j]}}
classSolution{funclongestPalindromeSubseq(_ s:String)->Int{let chars =Array(s)let n = chars.count
var dp =[[Int]](repeating:[Int](repeating:-1, count: n), count: n)funcdfs(_ i:Int,_ j:Int)->Int{if i > j {return0}if i == j {return1}if dp[i][j]!=-1{return dp[i][j]}if chars[i]== chars[j]{
dp[i][j]=dfs(i +1, j -1)+2}else{
dp[i][j]=max(dfs(i +1, j),dfs(i, j -1))}return dp[i][j]}returndfs(0, n -1)}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n2)
3. Dynamic Programming (Using LCS Idea)
Intuition
A clever observation: the longest palindromic subsequence of a string is the same as the longest common subsequence (LCS) between the string and its reverse. Why? Any palindromic subsequence appears in the same order when read forwards or backwards, making it a common subsequence of both strings.
This transforms our problem into the classic LCS problem, which has a well-known dynamic programming solution.
Algorithm
Create the reverse of the input string.
Use the standard LCS algorithm:
Create a 2D DP table where dp[i][j] represents the LCS length of the first i characters of the original string and the first j characters of the reversed string.
publicclassSolution{publicintLongestPalindromeSubseq(string s){char[] arr = s.ToCharArray();
Array.Reverse(arr);returnLongestCommonSubsequence(s,newstring(arr));}privateintLongestCommonSubsequence(string s1,string s2){int N = s1.Length, M = s2.Length;int[,] dp =newint[N +1, M +1];for(int i =0; i < N; i++){for(int j =0; j < M; j++){if(s1[i]== s2[j]){
dp[i +1, j +1]=1+ dp[i, j];}else{
dp[i +1, j +1]= Math.Max(dp[i +1, j], dp[i, j +1]);}}}return dp[N, M];}}
funclongestPalindromeSubseq(s string)int{
reversed :=reverse(s)returnlongestCommonSubsequence(s, reversed)}funcreverse(s string)string{
runes :=[]rune(s)for i, j :=0,len(runes)-1; i < j; i, j = i+1, j-1{
runes[i], runes[j]= runes[j], runes[i]}returnstring(runes)}funclongestCommonSubsequence(s1, s2 string)int{
N, M :=len(s1),len(s2)
dp :=make([][]int, N+1)for i :=range dp {
dp[i]=make([]int, M+1)}for i :=0; i < N; i++{for j :=0; j < M; j++{if s1[i]== s2[j]{
dp[i+1][j+1]=1+ dp[i][j]}else{
dp[i+1][j+1]=max(dp[i+1][j], dp[i][j+1])}}}return dp[N][M]}funcmax(a, b int)int{if a > b {return a
}return b
}
class Solution {funlongestPalindromeSubseq(s: String): Int {returnlongestCommonSubsequence(s, s.reversed())}privatefunlongestCommonSubsequence(s1: String, s2: String): Int {val N = s1.length
val M = s2.length
val dp =Array(N +1){IntArray(M +1)}for(i in0 until N){for(j in0 until M){if(s1[i]== s2[j]){
dp[i +1][j +1]=1+ dp[i][j]}else{
dp[i +1][j +1]=maxOf(dp[i +1][j], dp[i][j +1])}}}return dp[N][M]}}
The bottom-up DP approach can be optimized for space. When filling the DP table, each cell only depends on cells from the current and previous rows (or in this formulation, the current row and the previous diagonal value). By processing in the right order and keeping track of just the necessary values, we can reduce space from O(n^2) to O(n).
classSolution:deflongestPalindromeSubseq(self, s:str)->int:
n =len(s)
dp =[0]* n
for i inrange(n -1,-1,-1):
dp[i]=1
prev =0for j inrange(i +1, n):
temp = dp[j]if s[i]== s[j]:
dp[j]= prev +2else:
dp[j]=max(dp[j], dp[j -1])
prev = temp
return dp[n -1]
publicclassSolution{publicintlongestPalindromeSubseq(String s){int n = s.length();int[] dp =newint[n];for(int i = n -1; i >=0; i--){
dp[i]=1;int prev =0;for(int j = i +1; j < n; j++){int temp = dp[j];if(s.charAt(i)== s.charAt(j)){
dp[j]= prev +2;}else{
dp[j]=Math.max(dp[j], dp[j -1]);}
prev = temp;}}return dp[n -1];}}
classSolution{public:intlongestPalindromeSubseq(string s){int n = s.size();
vector<int>dp(n,0);for(int i = n -1; i >=0; i--){
dp[i]=1;int prev =0;for(int j = i +1; j < n; j++){int temp = dp[j];if(s[i]== s[j]){
dp[j]= prev +2;}else{
dp[j]=max(dp[j], dp[j -1]);}
prev = temp;}}return dp[n -1];}};
classSolution{/**
* @param {string} s
* @return {number}
*/longestPalindromeSubseq(s){const n = s.length;const dp =newArray(n).fill(0);for(let i = n -1; i >=0; i--){
dp[i]=1;let prev =0;for(let j = i +1; j < n; j++){const temp = dp[j];if(s[i]=== s[j]){
dp[j]= prev +2;}else{
dp[j]= Math.max(dp[j], dp[j -1]);}
prev = temp;}}return dp[n -1];}}
publicclassSolution{publicintLongestPalindromeSubseq(string s){int n = s.Length;int[] dp =newint[n];for(int i = n -1; i >=0; i--){
dp[i]=1;int prev =0;for(int j = i +1; j < n; j++){int temp = dp[j];if(s[i]== s[j]){
dp[j]= prev +2;}else{
dp[j]= Math.Max(dp[j], dp[j -1]);}
prev = temp;}}return dp[n -1];}}
funclongestPalindromeSubseq(s string)int{
n :=len(s)
dp :=make([]int, n)for i := n -1; i >=0; i--{
dp[i]=1
prev :=0for j := i +1; j < n; j++{
temp := dp[j]if s[i]== s[j]{
dp[j]= prev +2}else{if dp[j-1]> dp[j]{
dp[j]= dp[j-1]}}
prev = temp
}}return dp[n-1]}
class Solution {funlongestPalindromeSubseq(s: String): Int {val n = s.length
val dp =IntArray(n)for(i in n -1 downTo 0){
dp[i]=1var prev =0for(j in i +1 until n){val temp = dp[j]if(s[i]== s[j]){
dp[j]= prev +2}else{
dp[j]=maxOf(dp[j], dp[j -1])}
prev = temp
}}return dp[n -1]}}
A subsequence does not require contiguous characters, while a substring does. For the string "bbbab", the longest palindromic subsequence is "bbbb" (length 4), but the longest palindromic substring is "bbb" (length 3). Make sure your solution allows skipping characters when they do not contribute to the palindrome.
Incorrect Base Case Handling
When implementing the recursive or DP solution, a common error is mishandling the base cases. A single character (i == j) is always a palindrome of length 1, and when i > j (empty range), the length is 0. Forgetting to return 1 for single characters or incorrectly initializing the DP table leads to off-by-one errors.
Wrong Iteration Order in Bottom-Up DP
In the bottom-up approach, the DP table must be filled in the correct order so that smaller subproblems are solved before larger ones. You need to iterate i from n-1 down to 0 and j from i to n-1. Iterating in the wrong direction causes the algorithm to read uncomputed values, producing incorrect results.