2002. Maximum Product of the Length of Two Palindromic Subsequences
Problem Description
You are given a string s
and need to find two disjoint palindromic subsequences from it such that the product of their lengths is maximized.
A subsequence is a string formed by deleting some (or no) characters from the original string without changing the order of the remaining characters. For example, "ace" is a subsequence of "abcde".
A string is palindromic if it reads the same forwards and backwards. For example, "aba" and "racecar" are palindromes.
Two subsequences are disjoint if they don't share any character at the same position in the original string. In other words, for any index i
in string s
, the character at position i
can belong to at most one of the two subsequences.
Your task is to find two such disjoint palindromic subsequences where the product of their lengths is as large as possible, and return this maximum product.
For example, if s = "leetcodecom"
, one possible solution could be finding two palindromic subsequences like "ete" (length 3) and "cdc" (length 3), giving a product of 9. The actual maximum might be different, but this illustrates the concept of finding disjoint palindromic subsequences and calculating their length product.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves finding subsequences in a string, not traversing nodes and edges in a graph structure.
Need to solve for kth smallest/largest?
- No: We're looking for the maximum product of lengths, not the kth smallest or largest element.
Involves Linked Lists?
- No: The problem works with strings and subsequences, not linked list data structures.
Does the problem have small constraints?
- Yes: The string length is at most 12 characters (as mentioned in the solution approach). With such small constraints, we can enumerate all possible subsequences (2^12 = 4096 possibilities).
Brute force / Backtracking?
- Yes: Given the small constraints, we can use brute force to enumerate all possible ways to split the string into two disjoint subsequences. We need to:
- Generate all possible subsequences (using binary enumeration)
- Check which ones are palindromes
- For each palindromic subsequence, find all disjoint palindromic subsequences
- Calculate the product of their lengths and track the maximum
Conclusion: The flowchart correctly leads us to a brute force/backtracking approach. The small constraint (string length ≤ 12) makes it feasible to enumerate all 2^n possible subsequences using binary representation, where each bit indicates whether a character is included in the subsequence or not. This aligns perfectly with the provided solution that uses binary enumeration and bit manipulation to find all palindromic subsequences and their disjoint pairs.
Intuition
The key insight comes from recognizing that with a string length of at most 12, we have a manageable search space. Each character can either be included or excluded from a subsequence, giving us 2^12 = 4096
possible subsequences - small enough to check exhaustively.
We can represent each subsequence as a binary number where bit i
indicates whether character i
is included. For example, with string "abc", the binary number 101
represents the subsequence "ac" (taking characters at positions 0 and 2).
The challenge is finding two disjoint palindromic subsequences. Two subsequences are disjoint if they don't share any character at the same position. In binary terms, if subsequence i
has certain bits set to 1, then any disjoint subsequence j
must have those bits set to 0. This means i & j = 0
(no overlapping bits).
Here's the clever part: once we identify a palindromic subsequence with bitmask i
, we know that any disjoint subsequence must come from the complement of i
. The complement is (2^n - 1) ^ i
, which gives us all the bits that are NOT set in i
. Any valid disjoint subsequence j
must be a subset of this complement.
So our approach becomes:
- Pre-compute which subsequences are palindromes by checking each bitmask
- For each palindromic subsequence
i
, enumerate all possible subsequences from its complement - Check if those complement subsequences are also palindromes
- If both are palindromes and disjoint, calculate the product of their lengths (number of 1-bits in each mask)
- Track the maximum product found
This binary enumeration approach elegantly handles the constraint checking and ensures we explore all valid pairs of disjoint palindromic subsequences.
Learn more about Dynamic Programming, Backtracking and Bitmask patterns.
Solution Approach
The implementation uses binary enumeration to explore all possible subsequences efficiently.
Step 1: Pre-compute Palindromic Subsequences
First, we create a boolean array p
of size 2^n
where p[k]
indicates whether the subsequence represented by bitmask k
is a palindrome.
For each bitmask k
from 1 to 2^n - 1
:
- Use two pointers
i
(starting at 0) andj
(starting atn-1
) - Skip positions where the bit is 0 (character not included in subsequence)
- Check if characters at included positions form a palindrome by comparing
s[i]
ands[j]
- If any mismatch is found, mark
p[k] = False
for k in range(1, 1 << n):
i, j = 0, n - 1
while i < j:
# Skip unselected characters from left
while i < j and (k >> i & 1) == 0:
i += 1
# Skip unselected characters from right
while i < j and (k >> j & 1) == 0:
j -= 1
# Check palindrome property
if i < j and s[i] != s[j]:
p[k] = False
break
i, j = i + 1, j - 1
Step 2: Find Maximum Product
For each palindromic subsequence i
:
- Calculate its complement:
mx = ((1 << n) - 1) ^ i
- This gives all positions not used by subsequence
i
- This gives all positions not used by subsequence
- Enumerate all subsets
j
of the complementmx
- Use the technique
j = (j - 1) & mx
to iterate through subsets
- Use the technique
- If
j
is also palindromic, calculate the product of lengths- Length is the bit count (number of 1s) in the bitmask
- Use
bit_count()
method to count 1s efficiently
for i in range(1, 1 << n):
if p[i]: # If i is palindromic
mx = ((1 << n) - 1) ^ i # Complement of i
j = mx
a = i.bit_count() # Length of first palindrome
while j:
if p[j]: # If j is also palindromic
b = j.bit_count() # Length of second palindrome
ans = max(ans, a * b) # Update maximum product
j = (j - 1) & mx # Next subset of mx
The clever bit manipulation j = (j - 1) & mx
ensures we only iterate through valid subsets of mx
, guaranteeing that subsequences i
and j
remain disjoint throughout the enumeration.
Time Complexity: O(3^n)
- For each of the 2^n
subsequences, we potentially check 2^k
disjoint subsequences where k
is the number of remaining positions.
Space Complexity: O(2^n)
- To store the palindrome status of all subsequences.
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 walk through the solution with s = "abab"
(n = 4).
Step 1: Pre-compute which subsequences are palindromes
We'll check all 15 non-empty subsequences (bitmasks 1 to 15):
- Bitmask
0001
(binary) = 1: subsequence "b" (position 3) → palindrome ✓ - Bitmask
0010
(binary) = 2: subsequence "a" (position 2) → palindrome ✓ - Bitmask
0011
(binary) = 3: subsequence "ab" (positions 2,3) → NOT palindrome ✗ - Bitmask
0100
(binary) = 4: subsequence "b" (position 1) → palindrome ✓ - Bitmask
0101
(binary) = 5: subsequence "bb" (positions 1,3) → palindrome ✓ - Bitmask
0110
(binary) = 6: subsequence "ba" (positions 1,2) → NOT palindrome ✗ - Bitmask
0111
(binary) = 7: subsequence "bab" (positions 1,2,3) → palindrome ✓ - Bitmask
1000
(binary) = 8: subsequence "a" (position 0) → palindrome ✓ - Bitmask
1001
(binary) = 9: subsequence "ab" (positions 0,3) → NOT palindrome ✗ - Bitmask
1010
(binary) = 10: subsequence "aa" (positions 0,2) → palindrome ✓ - Bitmask
1011
(binary) = 11: subsequence "aab" (positions 0,2,3) → NOT palindrome ✗ - Bitmask
1100
(binary) = 12: subsequence "ab" (positions 0,1) → NOT palindrome ✗ - Bitmask
1101
(binary) = 13: subsequence "abb" (positions 0,1,3) → NOT palindrome ✗ - Bitmask
1110
(binary) = 14: subsequence "aba" (positions 0,1,2) → palindrome ✓ - Bitmask
1111
(binary) = 15: subsequence "abab" (all positions) → NOT palindrome ✗
So palindromic subsequences are: {1, 2, 4, 5, 7, 8, 10, 14}
Step 2: Find disjoint palindromic pairs
Let's trace through finding disjoint pairs for bitmask i = 10
(binary 1010
, subsequence "aa"):
-
Calculate complement:
mx = 15 ^ 10 = 5
(binary0101
)- This represents positions 1 and 3 (the "b" characters)
-
Enumerate subsets of
mx = 5
:j = 5
(binary0101
, "bb"): Is palindrome? Yes!- Product = 2 × 2 = 4
j = 4
(binary0100
, "b" at position 1): Is palindrome? Yes!- Product = 2 × 1 = 2
j = 1
(binary0001
, "b" at position 3): Is palindrome? Yes!- Product = 2 × 1 = 2
j = 0
: Stop (empty subsequence)
Let's check another example with i = 7
(binary 0111
, subsequence "bab"):
-
Calculate complement:
mx = 15 ^ 7 = 8
(binary1000
)- This represents position 0 (the first "a")
-
Enumerate subsets of
mx = 8
:j = 8
(binary1000
, "a"): Is palindrome? Yes!- Product = 3 × 1 = 3
Step 3: Find maximum product
After checking all pairs:
- "aa" (10) and "bb" (5): product = 2 × 2 = 4 ← Maximum!
- "bab" (7) and "a" (8): product = 3 × 1 = 3
- "aba" (14) and "b" (1): product = 3 × 1 = 3
- And other combinations...
The maximum product is 4, achieved by selecting the disjoint palindromic subsequences "aa" and "bb".
Solution Implementation
1class Solution:
2 def maxProduct(self, s: str) -> int:
3 n = len(s)
4
5 # Precompute which subsequences are palindromes
6 # Each bitmask represents a subsequence (bit i = 1 means s[i] is included)
7 is_palindrome = [True] * (1 << n)
8
9 # Check each possible subsequence (represented by bitmask)
10 for mask in range(1, 1 << n):
11 # Two-pointer approach to check if subsequence is palindrome
12 left, right = 0, n - 1
13
14 while left < right:
15 # Move left pointer to next included character
16 while left < right and (mask >> left & 1) == 0:
17 left += 1
18
19 # Move right pointer to next included character
20 while left < right and (mask >> right & 1) == 0:
21 right -= 1
22
23 # Check if characters match
24 if left < right and s[left] != s[right]:
25 is_palindrome[mask] = False
26 break
27
28 # Move pointers inward
29 left += 1
30 right -= 1
31
32 max_product = 0
33
34 # Try all palindromic subsequences as first subsequence
35 for first_mask in range(1, 1 << n):
36 if not is_palindrome[first_mask]:
37 continue
38
39 # Get complement mask (all bits not in first_mask)
40 complement = ((1 << n) - 1) ^ first_mask
41
42 # Iterate through all submasks of complement as second subsequence
43 second_mask = complement
44 first_length = first_mask.bit_count()
45
46 while second_mask:
47 if is_palindrome[second_mask]:
48 second_length = second_mask.bit_count()
49 max_product = max(max_product, first_length * second_length)
50
51 # Get next submask of complement
52 second_mask = (second_mask - 1) & complement
53
54 return max_product
55
1class Solution {
2 public int maxProduct(String s) {
3 int stringLength = s.length();
4
5 // Array to store whether each bitmask represents a palindromic subsequence
6 // Index represents a bitmask where bit i indicates if character at position i is included
7 boolean[] isPalindrome = new boolean[1 << stringLength];
8 Arrays.fill(isPalindrome, true);
9
10 // Check all possible subsequences (represented by bitmasks)
11 for (int mask = 1; mask < (1 << stringLength); ++mask) {
12 // Use two pointers to check if the subsequence is a palindrome
13 for (int left = 0, right = stringLength - 1; left < stringLength; ++left, --right) {
14 // Skip characters not included in current subsequence from left side
15 while (left < right && (mask >> left & 1) == 0) {
16 ++left;
17 }
18
19 // Skip characters not included in current subsequence from right side
20 while (left < right && (mask >> right & 1) == 0) {
21 --right;
22 }
23
24 // If characters at left and right positions don't match, not a palindrome
25 if (left < right && s.charAt(left) != s.charAt(right)) {
26 isPalindrome[mask] = false;
27 break;
28 }
29 }
30 }
31
32 int maxProductResult = 0;
33
34 // Try all palindromic subsequences as the first subsequence
35 for (int firstMask = 1; firstMask < (1 << stringLength); ++firstMask) {
36 if (isPalindrome[firstMask]) {
37 int firstLength = Integer.bitCount(firstMask);
38
39 // Get complement mask (all bits not in firstMask)
40 int complementMask = ((1 << stringLength) - 1) ^ firstMask;
41
42 // Iterate through all subsets of the complement mask
43 // This ensures the second subsequence is disjoint from the first
44 for (int secondMask = complementMask; secondMask > 0; secondMask = (secondMask - 1) & complementMask) {
45 if (isPalindrome[secondMask]) {
46 int secondLength = Integer.bitCount(secondMask);
47 maxProductResult = Math.max(maxProductResult, firstLength * secondLength);
48 }
49 }
50 }
51 }
52
53 return maxProductResult;
54 }
55}
56
1class Solution {
2public:
3 int maxProduct(string s) {
4 int n = s.size();
5
6 // Store whether each subset (represented as bitmask) forms a palindrome
7 // Bitmask: bit i = 1 means character at index i is included in the subsequence
8 vector<bool> isPalindrome(1 << n, true);
9
10 // Check if each possible subsequence is a palindrome
11 for (int mask = 1; mask < (1 << n); ++mask) {
12 // Use two pointers to check if the subsequence is a palindrome
13 int left = 0;
14 int right = n - 1;
15
16 while (left < right) {
17 // Move left pointer to the next included character
18 while (left < right && !(mask >> left & 1)) {
19 ++left;
20 }
21
22 // Move right pointer to the previous included character
23 while (left < right && !(mask >> right & 1)) {
24 --right;
25 }
26
27 // Check if characters at both ends match
28 if (left < right && s[left] != s[right]) {
29 isPalindrome[mask] = false;
30 break;
31 }
32
33 ++left;
34 --right;
35 }
36 }
37
38 int maxProductValue = 0;
39
40 // Try all palindromic subsequences as the first subsequence
41 for (int firstMask = 1; firstMask < (1 << n); ++firstMask) {
42 if (isPalindrome[firstMask]) {
43 // Count bits in first subsequence (length of first palindrome)
44 int firstLength = __builtin_popcount(firstMask);
45
46 // Get complement mask (all positions not in first subsequence)
47 int complementMask = ((1 << n) - 1) ^ firstMask;
48
49 // Iterate through all subsets of the complement as second subsequence
50 for (int secondMask = complementMask; secondMask > 0; secondMask = (secondMask - 1) & complementMask) {
51 if (isPalindrome[secondMask]) {
52 // Count bits in second subsequence (length of second palindrome)
53 int secondLength = __builtin_popcount(secondMask);
54
55 // Update maximum product
56 maxProductValue = max(maxProductValue, firstLength * secondLength);
57 }
58 }
59 }
60 }
61
62 return maxProductValue;
63 }
64};
65
1function maxProduct(s: string): number {
2 const n = s.length;
3
4 // Store whether each subset (represented as bitmask) forms a palindrome
5 // Bitmask: bit i = 1 means character at index i is included in the subsequence
6 const isPalindrome: boolean[] = new Array(1 << n).fill(true);
7
8 // Check if each possible subsequence is a palindrome
9 for (let mask = 1; mask < (1 << n); mask++) {
10 // Use two pointers to check if the subsequence is a palindrome
11 let left = 0;
12 let right = n - 1;
13
14 while (left < right) {
15 // Move left pointer to the next included character
16 while (left < right && !((mask >> left) & 1)) {
17 left++;
18 }
19
20 // Move right pointer to the previous included character
21 while (left < right && !((mask >> right) & 1)) {
22 right--;
23 }
24
25 // Check if characters at both ends match
26 if (left < right && s[left] !== s[right]) {
27 isPalindrome[mask] = false;
28 break;
29 }
30
31 left++;
32 right--;
33 }
34 }
35
36 let maxProductValue = 0;
37
38 // Try all palindromic subsequences as the first subsequence
39 for (let firstMask = 1; firstMask < (1 << n); firstMask++) {
40 if (isPalindrome[firstMask]) {
41 // Count bits in first subsequence (length of first palindrome)
42 const firstLength = countBits(firstMask);
43
44 // Get complement mask (all positions not in first subsequence)
45 const complementMask = ((1 << n) - 1) ^ firstMask;
46
47 // Iterate through all subsets of the complement as second subsequence
48 for (let secondMask = complementMask; secondMask > 0; secondMask = (secondMask - 1) & complementMask) {
49 if (isPalindrome[secondMask]) {
50 // Count bits in second subsequence (length of second palindrome)
51 const secondLength = countBits(secondMask);
52
53 // Update maximum product
54 maxProductValue = Math.max(maxProductValue, firstLength * secondLength);
55 }
56 }
57 }
58 }
59
60 return maxProductValue;
61}
62
63// Helper function to count the number of set bits in a number
64function countBits(n: number): number {
65 let count = 0;
66 while (n > 0) {
67 count += n & 1;
68 n >>= 1;
69 }
70 return count;
71}
72
Time and Space Complexity
Time Complexity: O(2^n × n + 3^n)
The time complexity consists of two main parts:
-
First loop (preprocessing palindrome check):
O(2^n × n)
- The outer loop iterates through all
2^n - 1
possible subsequences (from1
to2^n - 1
) - For each subsequence
k
, the inner while loops check if it forms a palindrome by comparing characters at positionsi
andj
- In the worst case, checking each subsequence takes
O(n)
time as we traverse through the string positions - Total:
O(2^n × n)
- The outer loop iterates through all
-
Second loop (finding maximum product):
O(3^n)
- The outer loop iterates through all
2^n - 1
possible subsequences - For each palindromic subsequence
i
, we iterate through all subsequencesj
that don't overlap withi
- The key insight: for each bit position in the
n
-bit representation, we have 3 choices:- The bit belongs to subsequence
i
(fixed by outer loop) - The bit belongs to subsequence
j
(iterated in inner loop) - The bit belongs to neither subsequence
- The bit belongs to subsequence
- This gives us
3^n
total combinations across all iterations - The operations inside (bit counting and max comparison) are
O(1)
orO(log n)
which doesn't affect the dominant term
- The outer loop iterates through all
Space Complexity: O(2^n)
The space is dominated by the boolean array p
which stores whether each of the 2^n
possible subsequences is a palindrome. All other variables use constant space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Palindrome Checking for Subsequences
A critical pitfall is incorrectly checking whether a subsequence forms a palindrome. The naive approach of extracting characters and then checking palindrome property creates unnecessary overhead and potential errors.
Incorrect approach:
# Wrong: Building string then checking
chars = []
for i in range(n):
if mask >> i & 1:
chars.append(s[i])
# Then checking if chars forms palindrome
Correct approach: Use two pointers directly on the original string while skipping unselected positions:
left, right = 0, n - 1 while left < right: # Skip unselected positions while left < right and (mask >> left & 1) == 0: left += 1 while left < right and (mask >> right & 1) == 0: right -= 1 # Check palindrome property if left < right and s[left] != s[right]: is_palindrome[mask] = False break left += 1 right -= 1
2. Missing Edge Case: Empty Subsequence
The code starts iteration from mask = 1, correctly skipping the empty subsequence (mask = 0). However, when iterating through submasks, forgetting to handle the case where second_mask
becomes 0 could lead to issues.
Solution:
The while loop while second_mask:
naturally handles this by terminating when second_mask
reaches 0.
3. Inefficient Submask Enumeration
Using a naive approach to find disjoint subsequences by checking all pairs would be O(4^n):
# Inefficient: Checking all pairs
for mask1 in range(1, 1 << n):
for mask2 in range(1, 1 << n):
if mask1 & mask2 == 0: # Check if disjoint
# Process...
Correct approach: Use complement and submask enumeration to guarantee disjoint subsequences:
complement = ((1 << n) - 1) ^ first_mask second_mask = complement while second_mask: # Process second_mask (guaranteed disjoint with first_mask) second_mask = (second_mask - 1) & complement
4. Bit Manipulation Errors
Common mistakes:
- Using
mask >> i
instead ofmask >> i & 1
to check if bit i is set - Incorrectly calculating complement as
~first_mask
instead of((1 << n) - 1) ^ first_mask
- The
~
operator in Python produces negative numbers for positive inputs, which causes issues
Solution:
Always use ((1 << n) - 1) ^ mask
for complement within n bits, and mask >> i & 1
to check individual bits.
5. Off-by-One Errors in Pointer Movement
When checking palindromes, forgetting to increment/decrement pointers after comparison:
# Wrong: Infinite loop if pointers don't move if left < right and s[left] != s[right]: is_palindrome[mask] = False break # Missing: left += 1, right -= 1
Solution: Always move pointers after each comparison to avoid infinite loops and ensure all character pairs are checked.
Which type of traversal does breadth first search do?
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
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!