2002. Maximum Product of The Length of Two Palindromic Subsequences - Explanation
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
- Use recursion with three branches per character: skip, add to
seq1, or add toseq2. - At the end of the string, check if both subsequences are palindromes.
- If both are palindromes, update the result with their length product.
- 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 resTime & Space Complexity
- Time complexity:
- Space complexity:
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
- Enumerate all bitmasks from
1to . - For each mask, extract the corresponding subsequence and check if it is a palindrome.
- Store palindromic masks with their lengths in a map.
- For each pair of palindromic masks, if they are disjoint (AND equals
0), compute the product. - 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 resTime & Space Complexity
- Time complexity:
- Space complexity:
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
- Enumerate all bitmasks from
1to . - For each mask, check if the corresponding subsequence is a palindrome.
- If it is, compute the LPS of the remaining characters (those not in the mask) using dynamic programming.
- Update the result with the product of the palindrome length and the LPS length.
- 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 TrueTime & Space Complexity
- Time complexity:
- Space complexity:
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
- For each bitmask, use a two-pointer approach to check if the selected characters form a palindrome and compute its length.
- If it is a palindrome, run the LPS algorithm on the original string, but skip indices that are set in the mask.
- Reset the DP array appropriately before each LPS computation.
- 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 resTime & Space Complexity
- Time complexity:
- Space complexity:
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.