2002. Maximum Product of The Length of Two Palindromic Subsequences - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Recursion and Backtracking - Used to explore all possible ways to partition characters into two subsequences
  • Palindrome Checking - Understanding how to verify if a string reads the same forwards and backwards using two pointers
  • Bitmask/Bit Manipulation - Representing subsets of characters as binary numbers for efficient enumeration
  • Dynamic Programming (Longest Palindromic Subsequence) - Computing the LPS of a string using DP to optimize the solution

1. Recursion (Backtracking)

Intuition

We need two disjoint palindromic subsequences with maximum product of their lengths. For each character, we have three choices: add it to the first subsequence, add it to the second subsequence, or skip it entirely. At the end, if both subsequences are palindromes, we compute their product.

Algorithm

  1. Use recursion with three branches per character: skip, add to seq1, or add to seq2.
  2. At the end of the string, check if both subsequences are palindromes.
  3. If both are palindromes, update the result with their length product.
  4. Return the maximum product found.
class Solution:
    def maxProduct(self, s: str) -> int:
        def isPal(s):
            i, j = 0, len(s) - 1
            while i < j:
                if s[i] != s[j]:
                    return False
                i += 1
                j -= 1
            return True

        res = 0
        def rec(i, seq1, seq2):
            nonlocal res
            if i == len(s):
                if isPal(seq1) and isPal(seq2):
                    res = max(res, len(seq1) * len(seq2))
                return

            rec(i + 1, seq1, seq2)
            rec(i + 1, seq1 + s[i], seq2)
            rec(i + 1, seq1, seq2 + s[i])

        rec(0, "", "")
        return res

Time & Space Complexity

  • Time complexity: O(n3n)O(n * 3 ^ n)
  • Space complexity: O(n)O(n)

2. Bit Mask

Intuition

Since the string length is small (up to 12), we can represent each subsequence as a bitmask. We first enumerate all possible subsequences, check which ones are palindromes, and store them. Then we check all pairs of palindromic subsequences. Two subsequences are disjoint if their masks have no overlapping bits (bitwise AND equals 0).

Algorithm

  1. Enumerate all bitmasks from 1 to 2n12^n - 1.
  2. For each mask, extract the corresponding subsequence and check if it is a palindrome.
  3. Store palindromic masks with their lengths in a map.
  4. For each pair of palindromic masks, if they are disjoint (AND equals 0), compute the product.
  5. Return the maximum product.
class Solution:
    def maxProduct(self, s: str) -> int:
        def isPal(s):
            i, j = 0, len(s) - 1
            while i < j:
                if s[i] != s[j]:
                    return False
                i += 1
                j -= 1
            return True

        N, pali = len(s), {}

        for mask in range(1, 1 << N):
            subseq = ""
            for i in range(N):
                if mask & (1 << i):
                    subseq += s[i]

            if isPal(subseq):
                pali[mask] = len(subseq)

        res = 0
        for m1 in pali:
            for m2 in pali:
                if m1 & m2 == 0:
                    res = max(res, pali[m1] * pali[m2])

        return res

Time & Space Complexity

  • Time complexity: O(4n)O(4 ^ n)
  • Space complexity: O(2n)O(2 ^ n)

3. Bit Mask + Longest Palindromic Subsequence

Intuition

Instead of checking all pairs of palindromic masks, we can optimize by iterating through palindromic masks for the first subsequence and computing the longest palindromic subsequence (LPS) in the remaining characters. This avoids redundant pair comparisons while still finding the optimal solution.

Algorithm

  1. Enumerate all bitmasks from 1 to 2n12^n - 1.
  2. For each mask, check if the corresponding subsequence is a palindrome.
  3. If it is, compute the LPS of the remaining characters (those not in the mask) using dynamic programming.
  4. Update the result with the product of the palindrome length and the LPS length.
  5. Return the maximum product.
class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        if n == 0:
            return 0

        dp = [1] * n
        for i in range(n - 1, -1, -1):
            prev = 0
            for j in range(i + 1, n):
                tmp = dp[j]
                if s[i] == s[j]:
                    dp[j] = 2 + prev
                else:
                    dp[j] = max(dp[j - 1], dp[j])
                prev = tmp
        return dp[n - 1]

    def maxProduct(self, s: str) -> int:
        n = len(s)
        res = 0

        for i in range(1, 1 << n):
            seq1, seq2 = [], []

            for j in range(n):
                if (i & (1 << j)) != 0:
                    seq1.append(s[j])
                else:
                    seq2.append(s[j])

            if not self.isPal(seq1):
                continue

            lps = self.longestPalindromeSubseq(''.join(seq2))
            res = max(res, len(seq1) * lps)

        return res

    def isPal(self, s: str) -> bool:
        i, j = 0, len(s) - 1
        while i < j:
            if s[i] != s[j]:
                return False
            i += 1
            j -= 1
        return True

Time & Space Complexity

  • Time complexity: O(n22n)O(n ^ 2 * 2 ^ n)
  • Space complexity: O(n)O(n)

4. Bit Mask + LPS (Optimal)

Intuition

We can further optimize by computing the LPS directly on the original string while using the mask to skip characters that belong to the first subsequence. This avoids creating new strings and reduces overhead. We also validate palindromes efficiently using two pointers with mask-based skipping.

Algorithm

  1. For each bitmask, use a two-pointer approach to check if the selected characters form a palindrome and compute its length.
  2. If it is a palindrome, run the LPS algorithm on the original string, but skip indices that are set in the mask.
  3. Reset the DP array appropriately before each LPS computation.
  4. Track and return the maximum product.
class Solution:
    def longestPalindromeSubseq(self, s: str, mask: int, dp: List[int]) -> int:
        n = len(s)
        for i in range(n - 1, -1, -1):
            if (mask & (1 << i)) != 0:
                continue

            prev = 0
            for j in range(i + 1, n):
                tmp = dp[j]
                if (mask & (1 << j)) == 0 and s[i] == s[j]:
                    dp[j] = 2 + prev
                else:
                    dp[j] = max(dp[j - 1], dp[j])
                prev = tmp
        return dp[-1]

    def maxProduct(self, s: str) -> int:
        n = len(s)
        res = 0
        dp = [1] * n

        for i in range(1, 1 << n):
            m1 = self.palsize(s, i)
            if m1 == 0:
                continue

            for j in range(n):
                if (i & (1 << j)) == 0:
                    dp[j] = 1
                else:
                    dp[j] = 0

            m2 = self.longestPalindromeSubseq(s, i, dp)
            res = max(res, m1 * m2)

        return res

    def palsize(self, s: str, mask: int) -> int:
        i, j = 0, len(s) - 1
        res = 0
        while i <= j:
            if (mask & (1 << i)) == 0:
                i += 1
            elif (mask & (1 << j)) == 0:
                j -= 1
            else:
                if s[i] != s[j]:
                    return 0
                res += 1 if i == j else 2
                i += 1
                j -= 1
        return res

Time & Space Complexity

  • Time complexity: O(n22n)O(n ^ 2 * 2 ^ n)
  • Space complexity: O(n)O(n)

Common Pitfalls

Allowing Overlapping Characters Between Subsequences

The two palindromic subsequences must be disjoint, meaning they cannot share any character. When using bitmasks, ensure that (mask1 & mask2) == 0 before considering a pair. Forgetting this check leads to invalid solutions where the same character is counted in both subsequences.

Not Validating Palindrome Before Computing Product

When iterating through bitmasks, some masks represent subsequences that are not palindromes. Computing the product of lengths without first verifying that both subsequences are actually palindromes leads to incorrect maximum values.

Inefficient Palindrome Checking in Tight Loops

Constructing new strings for each subsequence and then checking if they are palindromes is expensive. For optimal solutions, use two-pointer validation directly on the original string with mask-based index skipping to avoid repeated string allocations inside the exponential loop.