Facebook Pixel

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:

  1. It is a subsequence of the original string (you can delete some characters but keep the relative order)
  2. It reads the same forwards and backwards (palindrome)
  3. It has an even length
  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. The current range we're examining (i to j)
  2. 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 return 0

Recursive Cases:

  1. When s[i] == s[j] and s[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] with s[i] as the new previous character
    • Formula: dfs(i, j, x) = dfs(i + 1, j - 1, s[i]) + 2
  2. When s[i] != s[j] or s[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 and len(s) - 1 represent the full string range
    • Empty string '' as initial x 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 Evaluator

Example 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 character
    • dfs(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 and j represent the left and right boundaries of the substring being considered, giving us O(n²) possible combinations
  • x represents the last character used in the palindrome, which can be any character from the string plus an empty string marker, giving us at most C + 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More