1682. Longest Palindromic Subsequence II 🔒
Problem Description
You need to find the longest "good palindromic subsequence" in a given string s
.
A good palindromic subsequence must satisfy all of these conditions:
- It is a subsequence of the original string (you can delete some characters but keep the relative order)
- It reads the same forwards and backwards (palindrome)
- It has an even length
- No two consecutive characters are the same, except for the two middle characters
For example, given s = "abcabcabb"
:
-
"abba"
is a good palindromic subsequence because:- It's a subsequence of the original string
- It's a palindrome (reads same both ways)
- It has even length (4)
- The consecutive characters are different (
a≠b
,b≠b
is allowed in the middle,b≠a
)
-
"bcb"
is NOT good because it has odd length (3) -
"bbbb"
is NOT good because it has equal consecutive characters throughout (not just in the middle)
The task is to return the length of the longest good palindromic subsequence that can be formed from the input string s
.
The solution uses dynamic programming with memoization. The function dfs(i, j, x)
finds the longest good palindrome in the substring from index i
to j
, where x
represents the previous character used (to ensure no consecutive characters are the same except in the middle). When s[i]
equals s[j]
and differs from x
, we can use these characters as the outer pair of our palindrome and recursively check the inner substring.
Intuition
The key insight is that a good palindromic subsequence has a special structure: it's symmetric with no repeated consecutive characters except possibly in the middle. This means if we build it from outside to inside, each pair of matching characters we add must be different from the previous pair.
Think about how palindromes work - they're mirror images. For a subsequence to be palindromic, we need matching characters at positions that mirror each other. The challenge here is the additional constraint about consecutive characters.
When we look at a string from indices i
to j
, we have choices:
- If
s[i] == s[j]
, these characters could potentially form the outer layer of our palindrome - But we can only use them if they're different from the character we used in the previous outer layer (to avoid consecutive duplicates)
This naturally leads to a recursive approach where we track:
- The current range we're examining (
i
toj
) - The last character we used (
x
) to ensure we don't repeat it consecutively
The beauty of this approach is that by building from outside to inside, we automatically handle the "except the two middle ones" condition. When our recursion reaches the center (when i >= j
), we've already built the entire palindrome, and the middle two characters are the last pair we added, which are allowed to be the same.
The memoization comes naturally because we'll often revisit the same subproblems - the same range [i, j]
with the same previous character x
. By caching these results, we avoid recalculating them.
Starting with dfs(0, len(s) - 1, '')
works perfectly because an empty string as the initial "previous character" ensures we can pick any character for our first pair without violating the consecutive character constraint.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses memoization search (dynamic programming with recursion and caching) to find the longest good palindromic subsequence.
We define a recursive function dfs(i, j, x)
that computes the length of the longest good palindromic subsequence in the substring s[i:j+1]
, where x
is the character used in the previous outer layer of the palindrome (to prevent consecutive duplicates).
Base Case:
- When
i >= j
, we've exhausted the string range, so we return0
Recursive Cases:
-
When
s[i] == s[j]
ands[i] != x
:- We can use these matching characters as a new outer layer of our palindrome
- They contribute
2
to the length (one from each end) - We recursively solve for the inner substring
[i+1, j-1]
withs[i]
as the new previous character - Formula:
dfs(i, j, x) = dfs(i + 1, j - 1, s[i]) + 2
-
When
s[i] != s[j]
ors[i] == x
:- We can't use both characters as a pair (either they don't match or would create consecutive duplicates)
- We try skipping either the left character or the right character
- Take the maximum of both options
- Formula:
dfs(i, j, x) = max(dfs(i + 1, j, x), dfs(i, j - 1, x))
Implementation Details:
- The
@cache
decorator automatically memoizes the function results, preventing redundant calculations for the same(i, j, x)
combinations - We start with
dfs(0, len(s) - 1, '')
where:0
andlen(s) - 1
represent the full string range- Empty string
''
as initialx
ensures any character can be chosen for the first pair
- After getting the result,
dfs.cache_clear()
is called to free memory (useful when the function might be called multiple times with different inputs)
Time Complexity: O(n² × k)
where n
is the length of string and k
is the number of unique characters (at most 26 for lowercase letters)
Space Complexity: O(n² × k)
for the memoization cache
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the solution with s = "aabaa"
.
We start with dfs(0, 4, '')
, examining the full string with no previous character.
Step 1: dfs(0, 4, '')
s[0] = 'a'
,s[4] = 'a'
, and'a' ≠ ''
- Characters match and differ from previous, so we can use them as outer layer
- Return:
2 + dfs(1, 3, 'a')
Step 2: dfs(1, 3, 'a')
s[1] = 'a'
,s[3] = 'a'
, but'a' == 'a'
(same as previous)- Can't use both as they would create consecutive duplicates
- Try both options:
dfs(2, 3, 'a')
: Skip left characterdfs(1, 2, 'a')
: Skip right character
Step 3a: dfs(2, 3, 'a')
s[2] = 'b'
,s[3] = 'a'
, they don't match- Try both:
max(dfs(3, 3, 'a'), dfs(2, 2, 'a'))
- Both return 0 (base case:
i >= j
) - Result: 0
Step 3b: dfs(1, 2, 'a')
s[1] = 'a'
,s[2] = 'b'
, they don't match- Try both:
max(dfs(2, 2, 'a'), dfs(1, 1, 'a'))
- Both return 0 (base case:
i >= j
) - Result: 0
Step 2 Result: max(0, 0) = 0
Final Result: 2 + 0 = 2
The longest good palindromic subsequence is "aa" with length 2. Notice how the algorithm correctly avoided creating "aaaa" (which would have consecutive 'a's that aren't in the middle) by checking the previous character constraint.
Solution Implementation
1from functools import cache
2
3class Solution:
4 def longestPalindromeSubseq(self, s: str) -> int:
5 """
6 Find the length of the longest palindromic subsequence where no two
7 consecutive characters in the palindrome are the same.
8
9 Args:
10 s: Input string
11
12 Returns:
13 Length of the longest palindromic subsequence with no consecutive duplicates
14 """
15
16 @cache
17 def find_longest_palindrome(left: int, right: int, prev_char: str) -> int:
18 """
19 Recursively find the longest palindromic subsequence in s[left:right+1]
20 where the previous character used in the palindrome is prev_char.
21
22 Args:
23 left: Left boundary index (inclusive)
24 right: Right boundary index (inclusive)
25 prev_char: The character previously added to the palindrome
26
27 Returns:
28 Length of the longest valid palindromic subsequence
29 """
30 # Base case: invalid range or single character
31 if left >= right:
32 return 0
33
34 # If characters at both ends match and are different from previous character
35 # we can include both in our palindrome
36 if s[left] == s[right] and s[left] != prev_char:
37 return find_longest_palindrome(left + 1, right - 1, s[left]) + 2
38
39 # Otherwise, try excluding either the left or right character
40 # and take the maximum
41 exclude_left = find_longest_palindrome(left + 1, right, prev_char)
42 exclude_right = find_longest_palindrome(left, right - 1, prev_char)
43 return max(exclude_left, exclude_right)
44
45 # Start with the entire string and no previous character
46 result = find_longest_palindrome(0, len(s) - 1, '')
47
48 # Clear the cache to free memory
49 find_longest_palindrome.cache_clear()
50
51 return result
52
1class Solution {
2 // Memoization table: dp[left][right][previousChar]
3 // Stores the length of longest palindromic subsequence from index left to right
4 // where previousChar represents the last character used (26 means no previous char)
5 private int[][][] dp;
6 private String inputString;
7
8 public int longestPalindromeSubseq(String s) {
9 int length = s.length();
10 this.inputString = s;
11
12 // Initialize memoization table with -1 (unvisited state)
13 // Third dimension size is 27: 26 for letters a-z, plus 1 for initial state (no previous char)
14 dp = new int[length][length][27];
15 for (int[][] row : dp) {
16 for (int[] subRow : row) {
17 Arrays.fill(subRow, -1);
18 }
19 }
20
21 // Start DFS from the entire string range with no previous character (represented by 26)
22 return dfs(0, length - 1, 26);
23 }
24
25 /**
26 * Recursively finds the longest palindromic subsequence in the given range
27 * @param left - left boundary of the current substring
28 * @param right - right boundary of the current substring
29 * @param previousChar - the character index used in the previous step (0-25 for 'a'-'z', 26 for none)
30 * @return length of the longest palindromic subsequence
31 */
32 private int dfs(int left, int right, int previousChar) {
33 // Base case: invalid range or single character
34 if (left >= right) {
35 return 0;
36 }
37
38 // Return memoized result if already computed
39 if (dp[left][right][previousChar] != -1) {
40 return dp[left][right][previousChar];
41 }
42
43 int maxLength = 0;
44
45 // Check if characters at both ends match and are different from previous character
46 // This ensures no consecutive identical characters in the palindrome
47 if (inputString.charAt(left) == inputString.charAt(right) &&
48 inputString.charAt(left) - 'a' != previousChar) {
49 // Include both matching characters and continue with inner substring
50 // Update previousChar to current character index
51 maxLength = dfs(left + 1, right - 1, inputString.charAt(left) - 'a') + 2;
52 } else {
53 // Characters don't match or would create consecutive identical chars
54 // Try excluding either the left or right character
55 maxLength = Math.max(
56 dfs(left + 1, right, previousChar), // Skip left character
57 dfs(left, right - 1, previousChar) // Skip right character
58 );
59 }
60
61 // Memoize and return the result
62 dp[left][right][previousChar] = maxLength;
63 return maxLength;
64 }
65}
66
1class Solution {
2public:
3 // dp[i][j][prev]: memoization array for longest palindrome subsequence
4 // i: left boundary index
5 // j: right boundary index
6 // prev: previous character used (0-25 for 'a'-'z', 26 for no previous character)
7 int dp[251][251][27];
8
9 int longestPalindromeSubseq(string s) {
10 int n = s.size();
11
12 // Initialize memoization array with -1 (unvisited state)
13 memset(dp, -1, sizeof(dp));
14
15 // Define recursive function with memoization
16 // Returns the length of longest palindrome subsequence in s[i...j]
17 // where prev is the character previously added to the palindrome
18 function<int(int, int, int)> dfs = [&](int left, int right, int prevChar) -> int {
19 // Base case: invalid range or single character
20 if (left >= right) {
21 return 0;
22 }
23
24 // Return memoized result if already computed
25 if (dp[left][right][prevChar] != -1) {
26 return dp[left][right][prevChar];
27 }
28
29 int result = 0;
30
31 // If characters at both ends match and are different from previous character
32 // (ensures no consecutive same characters in palindrome)
33 if (s[left] == s[right] && s[left] - 'a' != prevChar) {
34 // Include both characters and recursively solve for inner substring
35 result = dfs(left + 1, right - 1, s[left] - 'a') + 2;
36 } else {
37 // Try excluding either left or right character
38 result = max(dfs(left + 1, right, prevChar),
39 dfs(left, right - 1, prevChar));
40 }
41
42 // Store and return the result
43 dp[left][right][prevChar] = result;
44 return result;
45 };
46
47 // Start with full string and no previous character (26)
48 return dfs(0, n - 1, 26);
49 }
50};
51
1// dp[i][j][prev]: memoization array for longest palindrome subsequence
2// i: left boundary index
3// j: right boundary index
4// prev: previous character used (0-25 for 'a'-'z', 26 for no previous character)
5let dp: number[][][];
6
7function longestPalindromeSubseq(s: string): number {
8 const n = s.length;
9
10 // Initialize memoization array with -1 (unvisited state)
11 // Create 3D array: 251 x 251 x 27
12 dp = Array(251).fill(null).map(() =>
13 Array(251).fill(null).map(() =>
14 Array(27).fill(-1)
15 )
16 );
17
18 // Define recursive function with memoization
19 // Returns the length of longest palindrome subsequence in s[left...right]
20 // where prevChar is the character previously added to the palindrome
21 const dfs = (left: number, right: number, prevChar: number): number => {
22 // Base case: invalid range (left >= right means no valid subsequence)
23 if (left >= right) {
24 return 0;
25 }
26
27 // Return memoized result if already computed
28 if (dp[left][right][prevChar] !== -1) {
29 return dp[left][right][prevChar];
30 }
31
32 let result = 0;
33
34 // If characters at both ends match and are different from previous character
35 // (ensures no consecutive same characters in palindrome)
36 if (s[left] === s[right] && s.charCodeAt(left) - 'a'.charCodeAt(0) !== prevChar) {
37 // Include both characters and recursively solve for inner substring
38 // Add 2 for the matching pair and pass current character as new prevChar
39 result = dfs(left + 1, right - 1, s.charCodeAt(left) - 'a'.charCodeAt(0)) + 2;
40 } else {
41 // Characters don't match or would create consecutive same characters
42 // Try excluding either left or right character and take maximum
43 result = Math.max(
44 dfs(left + 1, right, prevChar), // Skip left character
45 dfs(left, right - 1, prevChar) // Skip right character
46 );
47 }
48
49 // Store and return the result for memoization
50 dp[left][right][prevChar] = result;
51 return result;
52 };
53
54 // Start with full string (0 to n-1) and no previous character (26)
55 return dfs(0, n - 1, 26);
56}
57
Time and Space Complexity
Time Complexity: O(n² × C)
where n
is the length of string s
and C
is the size of the character set (26 for lowercase English letters).
The recursive function dfs(i, j, x)
has three parameters:
i
andj
represent the left and right boundaries of the substring being considered, giving usO(n²)
possible combinationsx
represents the last character used in the palindrome, which can be any character from the string plus an empty string marker, giving us at mostC + 1
distinct values
Each unique combination of (i, j, x)
is computed only once due to memoization with @cache
. For each state, the function performs constant time operations: comparing characters and making at most two recursive calls (which are either cached or will be cached). Therefore, the total number of unique states is O(n² × C)
, and each state takes O(1)
time to compute, resulting in O(n² × C)
time complexity.
Space Complexity: O(n² × C)
for the memoization cache storing all possible states of (i, j, x)
, plus O(n)
for the recursion call stack depth in the worst case. The overall space complexity is O(n² × C)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the "No Consecutive Duplicates" Rule
The problem states that no two consecutive characters can be the same except for the two middle characters. A common mistake is implementing the constraint too strictly, preventing ANY consecutive duplicates including in the middle.
Incorrect Understanding:
# This would reject "abba" because b==b in the middle
def dfs(i, j, prev):
if s[i] == s[j] and s[i] != prev:
# Wrong: This prevents middle characters from being the same
return dfs(i + 1, j - 1, s[i]) + 2
Correct Implementation:
The current solution actually handles this correctly! When we use characters at positions i
and j
as a pair, they become the outermost layer. As we recurse inward, eventually when i >= j
, we've reached the middle. The base case returns 0, effectively allowing the last pair added (which becomes the middle) to have identical characters.
2. Incorrect Base Case Handling
Common Mistake:
def dfs(i, j, prev):
if i > j: # Only checking i > j
return 0
# Missing the i == j case consideration
Issue: When i == j
, we have a single character in the middle, which would make the total length odd. Since we need even-length palindromes, we should return 0 for both i >= j
.
Correct Approach:
if i >= j: # Handles both i > j and i == j return 0
3. Using Character Index Instead of Character Value
Common Mistake:
def dfs(i, j, prev_idx):
if s[i] == s[j] and i != prev_idx: # Using index comparison
return dfs(i + 1, j - 1, i) + 2 # Passing index instead of character
Issue: This compares indices instead of character values, leading to incorrect duplicate detection.
Correct Approach:
def dfs(i, j, prev_char):
if s[i] == s[j] and s[i] != prev_char: # Compare character values
return dfs(i + 1, j - 1, s[i]) + 2 # Pass the character, not index
4. Memory Issues with Large Inputs
Common Mistake: Forgetting to clear the cache after computation, especially in scenarios where the function might be called multiple times (like in a testing environment).
# Without cache clearing
result = dfs(0, len(s) - 1, '')
return result # Cache keeps growing with each test case
Correct Approach:
result = dfs(0, len(s) - 1, '')
dfs.cache_clear() # Free memory after computation
return result
5. Initial Previous Character Choice
Common Mistake:
# Starting with a character that might exist in the string
result = dfs(0, len(s) - 1, 'a') # What if s[0] == 'a'?
Issue: This could incorrectly prevent valid first character choices.
Correct Approach:
# Use empty string or a character guaranteed not to be in the input
result = dfs(0, len(s) - 1, '') # Empty string won't match any character
# Or alternatively:
result = dfs(0, len(s) - 1, '#') # If '#' is guaranteed not in input
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!