Maximum Product of the Length of Two Palindromic Subsequences
MedPrompt
Given a string s, find two disjoint palindromic subsequences of s such that the product of their lengths is maximized. The two subsequences are disjoint if they do not both pick a character at the same index.
Return the maximum possible product of the lengths of the two palindromic subsequences.
A subsequence is a string that can be derived from another string by deleting some or no characters without changing the order of the remaining characters. A string is palindromic if it reads the same forward and backward.
Example 1:
Input: s = "leetcodecom"
Output: 9
Explanation: An optimal solution is to choose "ete" for the 1st subsequence and "cdc" for the 2nd subsequence.
The product of their lengths is: 3 * 3 = 9.Example 2:
Input: s = "bb"
Output: 1
Explanation: An optimal solution is to choose "b" (the first character) for the 1st subsequence and "b" (the second character) for the 2nd subsequence.
The product of their lengths is: 1 * 1 = 1.Example 3:
Input: s = "accbcaxxcxx"
Output: 25
Explanation: An optimal solution is to choose "accca" for the 1st subsequence and "xxcxx" for the 2nd subsequence.
The product of their lengths is: 5 * 5 = 25.
Constraints:
2 <= s.length <= 12sconsists of lowercase English letters only.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach involves generating all possible subsequences of the input string s, checking which ones are palindromic, and then finding the best pair of disjoint palindromic subsequences by checking all pairs.
Algorithm
- Initialize
n = s.length(). - Create a map
palindromesto store(mask, length)for palindromic subsequences. - Iterate
maskfrom1to(1 << n) - 1:- Construct the subsequence
subfromsusingmask. - If
subis a palindrome, put(mask, sub.length())intopalindromes.
- Construct the subsequence
- Initialize
maxProduct = 0. - Convert the keys of
palindromesinto a listpMasks. - Iterate through all pairs
(mask1, mask2)frompMasks:- If
(mask1 & mask2) == 0(the subsequences are disjoint):product = palindromes.get(mask1) * palindromes.get(mask2).maxProduct = max(maxProduct, product).
- If
- Return
maxProduct.
Walkthrough
- Generate all subsequences: We can represent each subsequence using a bitmask of length
n(wherenis the length ofs). A '1' at thei-th position in the mask means the characters.charAt(i)is included in the subsequence. We iterate through all2^npossible masks. - Identify palindromic subsequences: For each mask, we construct the corresponding subsequence string. We then check if this string is a palindrome.
- Store palindromes: We store the masks of all palindromic subsequences and their lengths, for example, in a hash map where the key is the mask and the value is the length.
- Find the best pair: After identifying all palindromic subsequences, we iterate through all possible pairs of them. For each pair, we check if their corresponding masks are disjoint (i.e., their bitwise AND is zero). If they are disjoint, we calculate the product of their lengths and update our maximum product found so far.
class Solution { public int maxProduct(String s) { int n = s.length(); Map<Integer, Integer> palindromeLengths = new HashMap<>(); for (int mask = 1; mask < (1 << n); mask++) { StringBuilder sub = new StringBuilder(); for (int i = 0; i < n; i++) { if ((mask & (1 << i)) != 0) { sub.append(s.charAt(i)); } } if (isPalindrome(sub.toString())) { palindromeLengths.put(mask, sub.length()); } } int maxProd = 0; List<Integer> masks = new ArrayList<>(palindromeLengths.keySet()); for (int i = 0; i < masks.size(); i++) { for (int j = i; j < masks.size(); j++) { int mask1 = masks.get(i); int mask2 = masks.get(j); if ((mask1 & mask2) == 0) { int product = palindromeLengths.get(mask1) * palindromeLengths.get(mask2); maxProd = Math.max(maxProd, product); } } } return maxProd; } private boolean isPalindrome(String str) { int left = 0, right = str.length() - 1; while (left < right) { if (str.charAt(left) != str.charAt(right)) { return false; } left++; right--; } return true; }}Complexity
Time
O(n * 2^n + P^2), where P is the number of palindromic subsequences. In the worst case, P can be close to 2^n, making the complexity O(n * 2^n + 4^n). For n=12, this is computationally expensive.
Space
O(P), where P is the number of palindromic subsequences. In the worst case, this can be O(2^n) to store the masks and lengths.
Trade-offs
Pros
Conceptually straightforward extension of generating all subsequences.
Cons
The pairwise check can be very slow if there are many palindromic subsequences, leading to a high time complexity that might not pass for larger constraints.
This approach uses recursion (backtracking) to explore all possible ways to partition the characters of the string s into three groups: one for the first subsequence, one for the second, and one for unused characters.
Algorithm
- Initialize a global variable
maxProduct = 0. - Define a recursive function
backtrack(index, s1, s2):- Base Case: If
index == s.length():- If
s1is a palindrome ands2is a palindrome:maxProduct = max(maxProduct, s1.length() * s2.length()).
- Return.
- If
- Recursive Step:
- Choice 1 (add to s1):
backtrack(index + 1, s1 + s.charAt(index), s2). - Choice 2 (add to s2):
backtrack(index + 1, s1, s2 + s.charAt(index)). - Choice 3 (ignore):
backtrack(index + 1, s1, s2).
- Choice 1 (add to s1):
- Base Case: If
- Start the process by calling
backtrack(0, "", ""). - Return
maxProduct. - Using
StringBuilderinstead of string concatenation is recommended for better performance.
Walkthrough
We define a recursive function, say backtrack(index, s1, s2), that tries to build two disjoint subsequences, s1 and s2. The index parameter tracks the current character in s being considered.
For each character s.charAt(index), we have three choices:
- Append it to
s1. - Append it to
s2. - Ignore it (don't add to either
s1ors2). The recursion proceeds by making one of these three choices and then calling itself for the next index,index + 1. The base case for the recursion is whenindexreaches the end of the strings. At this point, we have formed two complete (though possibly empty) subsequences,s1ands2. We check if both are palindromes. If they are, we calculate the product of their lengths and update a global maximum.
class Solution { int maxProd = 0; public int maxProduct(String s) { backtrack(0, s, new StringBuilder(), new StringBuilder()); return maxProd; } private void backtrack(int index, String s, StringBuilder s1, StringBuilder s2) { if (index == s.length()) { if (isPalindrome(s1) && isPalindrome(s2)) { maxProd = Math.max(maxProd, s1.length() * s2.length()); } return; } char c = s.charAt(index); // Choice 1: Add to s1 s1.append(c); backtrack(index + 1, s, s1, s2); s1.deleteCharAt(s1.length() - 1); // backtrack // Choice 2: Add to s2 s2.append(c); backtrack(index + 1, s, s1, s2); s2.deleteCharAt(s2.length() - 1); // backtrack // Choice 3: Ignore backtrack(index + 1, s, s1, s2); } private boolean isPalindrome(StringBuilder sb) { if (sb.length() == 0) return true; int left = 0, right = sb.length() - 1; while (left < right) { if (sb.charAt(left) != sb.charAt(right)) { return false; } left++; right--; } return true; }}Complexity
Time
O(n * 3^n). There are `3^n` possible partitions. For each partition, we perform two palindrome checks, which take `O(n)` time in total.
Space
O(n). The recursion depth is `n`, and the `StringBuilder`s also take `O(n)` space.
Trade-offs
Pros
Relatively simple to implement and understand.
It directly maps to the problem's combinatorial nature.
Cons
Can be slow for larger
n, though it's acceptable forn <= 12.
This is the most efficient approach. It leverages bitmasking to represent subsequences and dynamic programming to avoid redundant computations. The core idea is to first precompute the lengths of all palindromic subsequences and then efficiently find the best pair.
Algorithm
- Initialize
n = s.length(). - Create an array
dpof size(1 << n). - Iterate
maskfrom1to(1 << n) - 1:- Construct subsequence
subfromsusingmask. - If
subis a palindrome, setdp[mask] = sub.length().
- Construct subsequence
- For
ifrom0ton-1:- For
maskfrom1to(1 << n) - 1:- If the
i-th bit ofmaskis set ((mask >> i) & 1 == 1):dp[mask] = max(dp[mask], dp[mask ^ (1 << i)]).
- If the
- For
- Initialize
maxProduct = 0. - Iterate
mask1from1to(1 << n) - 1:mask2 = ((1 << n) - 1) ^ mask1.product = dp[mask1] * dp[mask2].maxProduct = max(maxProduct, product).
- Return
maxProduct.
Walkthrough
The approach consists of three main steps:
- Precompute Palindrome Lengths: We iterate through all
2^nmasks. For each mask, we generate the corresponding subsequence and check if it's a palindrome. We store the length of the subsequence if it is a palindrome, and 0 otherwise, in an arraydpof size2^n.dp[mask]will hold the length. - DP on Subsets: We want to find, for each mask, the length of the longest palindromic subsequence that is a submask of it. We can compute this using a DP technique. Let
dp[mask]be the length of the longest palindromic subsequence that can be formed using a subset of characters represented bymask. We can compute thisdparray as follows:- Initialize
dp[mask]with the length if the subsequence formaskis a palindrome, 0 otherwise. - Then, for each bit
ifrom0ton-1, we iterate through all masks. If thei-th bit is set in amask, it means the characters.charAt(i)is included. The longest palindrome for thismaskis either one that includess.charAt(i)or one that doesn't. The latter isdp[mask ^ (1 << i)]. So, we updatedp[mask] = max(dp[mask], dp[mask ^ (1 << i)]). This propagates the maximum lengths from submasks to their supermasks.
- Initialize
- Find Maximum Product: After computing the
dparray,dp[mask]contains the length of the longest palindromic subsequence for any submask ofmask. We can now iterate through all possible masksmask1from1to(1 << n) - 1. For eachmask1, we consider it for the first subsequence. The remaining characters are represented by the complement mask,mask2 = ((1 << n) - 1) ^ mask1. The length of the longest palindromic subsequence frommask1isdp[mask1], and frommask2isdp[mask2]. We calculate their product and update the overall maximum.
class Solution { public int maxProduct(String s) { int n = s.length(); int N = 1 << n; int[] dp = new int[N]; for (int mask = 1; mask < N; mask++) { StringBuilder sub = new StringBuilder(); for (int i = 0; i < n; i++) { if ((mask & (1 << i)) != 0) { sub.append(s.charAt(i)); } } if (isPalindrome(sub.toString())) { dp[mask] = sub.length(); } } for (int i = 0; i < n; i++) { for (int mask = 1; mask < N; mask++) { if ((mask & (1 << i)) != 0) { dp[mask] = Math.max(dp[mask], dp[mask ^ (1 << i)]); } } } int maxProd = 0; for (int mask1 = 1; mask1 < N; mask1++) { int mask2 = (N - 1) ^ mask1; maxProd = Math.max(maxProd, dp[mask1] * dp[mask2]); } return maxProd; } private boolean isPalindrome(String str) { int left = 0, right = str.length() - 1; while (left < right) { if (str.charAt(left) != str.charAt(right)) { return false; } left++; right--; } return true; }}Complexity
Time
O(n * 2^n). Precomputing palindrome lengths takes O(n * 2^n). The DP on subsets step takes O(n * 2^n). The final loop takes O(2^n). The total is dominated by O(n * 2^n).
Space
O(2^n) for the `dp` array.
Trade-offs
Pros
Highly efficient and guaranteed to pass within time limits for the given constraints.
Cons
More complex to understand and implement, requiring knowledge of bitmasking and dynamic programming on subsets.
Solutions
Solution
class Solution {public int maxProduct(String s) { int n = s.length(); boolean[] p = new boolean[1 << n]; Arrays.fill(p, true); for (int k = 1; k < 1 << n; ++k) { for (int i = 0, j = n - 1; i < n; ++i, --j) { while (i < j && (k >> i & 1) == 0) { ++i; } while (i < j && (k >> j & 1) == 0) { --j; } if (i < j && s.charAt(i) != s.charAt(j)) { p[k] = false; break; } } } int ans = 0; for (int i = 1; i < 1 << n; ++i) { if (p[i]) { int a = Integer.bitCount(i); int mx = ((1 << n) - 1) ^ i; for (int j = mx; j > 0; j = (j - 1) & mx) { if (p[j]) { int b = Integer.bitCount(j); ans = Math.max(ans, a * b); } } } } return ans; }}class Solution {public: int maxProduct(string s) { int n = s.size(); vector<bool> p(1 << n, true); for (int k = 1; k < 1 << n; ++k) { for (int i = 0, j = n - 1; i < j; ++i, --j) { while (i < j && !(k >> i & 1)) { ++i; } while (i < j && !(k >> j & 1)) { --j; } if (i < j && s[i] != s[j]) { p[k] = false; break; } } } int ans = 0; for (int i = 1; i < 1 << n; ++i) { if (p[i]) { int a = __builtin_popcount(i); int mx = ((1 << n) - 1) ^ i; for (int j = mx; j; j = (j - 1) & mx) { if (p[j]) { int b = __builtin_popcount(j); ans = max(ans, a * b); } } } } return ans; }};class Solution: def maxProduct(self, s: str) -> int: n = len(s) p = [True] * (1 << n) for k in range(1, 1 << n): i, j = 0, n - 1 while i < j: while i < j and (k >> i & 1) == 0: i += 1 while i < j and (k >> j & 1) == 0: j -= 1 if i < j and s[i] != s[j]: p[k] = False break i, j = i + 1, j - 1 ans = 0 for i in range(1, 1 << n): if p[i]: mx = ((1 << n) - 1) ^ i j = mx a = i . bit_count() while j: if p[j]: b = j . bit_count() ans = max(ans, a * b) j = (j - 1) & mx return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.