Maximum Palindromes After Operations
MedPrompt
You are given a 0-indexed string array words having length n and containing 0-indexed strings.
You are allowed to perform the following operation any number of times (including zero):
- Choose integers
i,j,x, andysuch that0 <= i, j < n,0 <= x < words[i].length,0 <= y < words[j].length, and swap the characterswords[i][x]andwords[j][y].
Return an integer denoting the maximum number of palindromes words can contain, after performing some operations.
Note: i and j may be equal during an operation.
Example 1:
Input: words = ["abbb","ba","aa"]
Output: 3
Explanation: In this example, one way to get the maximum number of palindromes is:
Choose i = 0, j = 1, x = 0, y = 0, so we swap words[0][0] and words[1][0]. words becomes ["bbbb","aa","aa"].
All strings in words are now palindromes.
Hence, the maximum number of palindromes achievable is 3.Example 2:
Input: words = ["abc","ab"]
Output: 2
Explanation: In this example, one way to get the maximum number of palindromes is:
Choose i = 0, j = 1, x = 1, y = 0, so we swap words[0][1] and words[1][0]. words becomes ["aac","bb"].
Choose i = 0, j = 0, x = 1, y = 2, so we swap words[0][1] and words[0][2]. words becomes ["aca","bb"].
Both strings are now palindromes.
Hence, the maximum number of palindromes achievable is 2.Example 3:
Input: words = ["cd","ef","a"]
Output: 1
Explanation: In this example, there is no need to perform any operation.
There is one palindrome in words "a".
It can be shown that it is not possible to get more than one palindrome after any number of operations.
Hence, the answer is 1.
Constraints:
1 <= words.length <= 10001 <= words[i].length <= 100words[i]consists only of lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach models the problem as a series of choices. For each word, we can either choose to form a palindrome with it or skip it. This creates a binary decision tree. By exploring all possible paths in this tree using recursion (a backtracking method), we can find the combination of choices that yields the maximum number of palindromes. This method exhaustively checks every subset of words.
Algorithm
- Calculate the total number of available character pairs (
total_pairs) by counting character frequencies across all words. - Create a list of word lengths.
- Define a recursive function,
solve(index, pairs_left), that computes the maximum palindromes fromwords[index...]withpairs_leftavailable. - In
solve(index, pairs_left):- Base Case: If
indexis out of bounds, return 0. - Choice 1 (Skip Word): Recursively call
solve(index + 1, pairs_left). - Choice 2 (Take Word): If
pairs_leftis sufficient forwords[index](i.e.,pairs_left >= lengths[index] / 2), recursively call1 + solve(index + 1, pairs_left - lengths[index] / 2). - Return the maximum of the results from the possible choices.
- Base Case: If
- The initial call is
solve(0, total_pairs).
Walkthrough
The brute-force backtracking approach systematically explores every possible subset of words that could be turned into palindromes. First, we pre-calculate the total number of available character pairs by counting all characters across all words. Then, we define a recursive function, say solve(index, remaining_pairs), which represents the maximum number of palindromes we can form from the words starting at index with a given number of remaining_pairs.
For each word at index, the function makes two decisions:
- Skip: Don't make the current word a palindrome. We move to the next word (
index + 1) with the same number ofremaining_pairs. - Take: Try to make the current word a palindrome. This is only possible if we have enough pairs for it. If so, we add 1 to our count and move to the next word with the
remaining_pairsreduced by the amount used.
The function returns the maximum result from these two choices. The base case for the recursion is when we have considered all words (index reaches the end of the list), at which point we can't form any more palindromes, so we return 0.
class Solution { private int[] lengths; public int maxPalindromesAfterOperations(String[] words) { int[] counts = new int[26]; this.lengths = new int[words.length]; int i = 0; for (String word : words) { lengths[i++] = word.length(); for (char c : word.toCharArray()) { counts[c - 'a']++; } } int totalPairs = 0; for (int count : counts) { totalPairs += count / 2; } return solve(0, totalPairs); } private int solve(int index, int pairsLeft) { if (index == lengths.length) { return 0; } // Option 1: Skip this word int resSkip = solve(index + 1, pairsLeft); // Option 2: Take this word if possible int pairsNeeded = lengths[index] / 2; int resTake = 0; // Cannot take by default if (pairsLeft >= pairsNeeded) { resTake = 1 + solve(index + 1, pairsLeft - pairsNeeded); } return Math.max(resSkip, resTake); }}Complexity
Time
O(2^N), where N is the number of words. In the worst case, we explore two branches for each word, leading to an exponential number of calls.
Space
O(N), where N is the number of words. This space is used by the recursion stack.
Trade-offs
Pros
Conceptually straightforward, as it directly translates the problem's choices into code.
Guaranteed to find the correct answer, albeit very slowly.
Cons
Extremely inefficient due to its exponential time complexity.
Infeasible for the given constraints as it will lead to a 'Time Limit Exceeded' error.
Redundantly re-computes solutions for the same subproblems.
This approach improves upon the brute-force method by using dynamic programming with memoization. The problem can be identified as a variation of the 0/1 Knapsack problem. The 'knapsack' has a capacity equal to the total available character pairs. Each word is an 'item' with a 'weight' equal to the number of pairs it requires (length / 2) and a 'value' of 1. The goal is to maximize the total value (number of words) without exceeding the knapsack's capacity. Memoization avoids re-computing results for the same state (index, pairs_left), drastically reducing the number of calculations.
Algorithm
- Calculate
total_pairsand a list oflengthsas in the previous approach. - Create a 2D memoization table,
memo[N][total_pairs + 1], to store the results of subproblems. Initialize it with a sentinel value (e.g.,nullor -1). - Use the same recursive function
solve(index, pairs_left). - Before any computation within
solve, check ifmemo[index][pairs_left]already contains a result. If so, return it immediately. - After computing the result for
(index, pairs_left), store it inmemo[index][pairs_left]before returning.
Walkthrough
The key observation is that the brute-force backtracking approach solves the same subproblems multiple times. For example, the result for solve(5, 10) might be needed through different paths of choices for the first four words. We can cache the results to avoid this redundant work.
A 2D array, memo, can be used for this, where memo[i][j] stores the result for solve(i, j). When the function is called, it first checks the memoization table. If a result exists, it's returned instantly. Otherwise, the result is computed as in the backtracking approach, and then stored in the table before being returned. This ensures that each unique subproblem (index, pairs_left) is solved only once.
class Solution { private int[] lengths; private Integer[][] memo; public int maxPalindromesAfterOperations(String[] words) { int[] counts = new int[26]; this.lengths = new int[words.length]; int i = 0; for (String word : words) { lengths[i++] = word.length(); for (char c : word.toCharArray()) { counts[c - 'a']++; } } int totalPairs = 0; for (int count : counts) { totalPairs += count / 2; } memo = new Integer[words.length][totalPairs + 1]; return solve(0, totalPairs); } private int solve(int index, int pairsLeft) { if (index == lengths.length) { return 0; } if (memo[index][pairsLeft] != null) { return memo[index][pairsLeft]; } // Option 1: Skip this word int resSkip = solve(index + 1, pairsLeft); // Option 2: Take this word if possible int pairsNeeded = lengths[index] / 2; int resTake = 0; if (pairsLeft >= pairsNeeded) { resTake = 1 + solve(index + 1, pairsLeft - pairsNeeded); } return memo[index][pairsLeft] = Math.max(resSkip, resTake); }}Complexity
Time
O(N * P), where N is the number of words and P is the total number of available pairs. Each state `(index, pairsLeft)` is computed once.
Space
O(N * P), where N is the number of words and P is the total number of pairs. This is for the memoization table.
Trade-offs
Pros
Significantly more efficient than brute-force.
Guaranteed to find the optimal solution by exploring the problem space systematically.
Cons
The space complexity is O(N * P), which can be large if the total number of pairs
Pis high.The time complexity, while better than exponential, is still pseudo-polynomial and may be too slow if
Pis very large.
The most efficient solution is a greedy one. The problem is a special case of the 0/1 knapsack problem where all items have a value of 1. In this scenario, the optimal strategy to maximize the number of items is to always pick the items with the smallest 'weight' first. Here, the 'items' are the words, and their 'weight' is the number of character pairs they require (length / 2). By sorting the words by length (and thus by pairs required) and greedily fulfilling their needs, we can find the maximum number of palindromes efficiently.
Algorithm
- Count the frequency of all characters across all words.
- From the frequencies, calculate the
total_pairsavailable. For a character with frequencyf, it contributesf / 2pairs. - Create a list of the lengths of all words.
- Sort the list of lengths in ascending order.
- Initialize a palindrome counter to 0.
- Iterate through the sorted lengths:
- For each length
L, calculate the pairs needed:pairs_needed = L / 2. - If
total_pairs >= pairs_needed, it's possible to form this palindrome. Increment the counter and decreasetotal_pairsbypairs_needed. - If
total_pairs < pairs_needed, we don't have enough pairs. Since the lengths are sorted, we cannot satisfy any subsequent, longer words either. Break the loop.
- For each length
- Return the palindrome counter.
Walkthrough
This approach leverages a key insight: to maximize the number of palindromes, we should prioritize forming palindromes from shorter words because they consume the fewest resources (character pairs). The ability to swap any character means we only need to care about the total count of pairs, not which specific characters form them.
The algorithm is as follows: First, we compute the total number of character pairs available from the entire character pool of all words. Second, we get the lengths of all words and sort them in non-decreasing order. Finally, we iterate through the sorted lengths, and for each length, we check if we have enough pairs. If we do, we 'form' the palindrome, increment our count, and deduct the used pairs from our total. If we don't, we stop, as we won't be able to form any longer palindromes either.
This greedy strategy is optimal because by satisfying the cheapest requests first, we maximize the number of requests we can satisfy in total.
import java.util.Arrays; class Solution { public int maxPalindromesAfterOperations(String[] words) { int[] counts = new int[26]; int[] lengths = new int[words.length]; int i = 0; for (String word : words) { lengths[i++] = word.length(); for (char c : word.toCharArray()) { counts[c - 'a']++; } } int totalPairs = 0; for (int count : counts) { totalPairs += count / 2; } Arrays.sort(lengths); int palindromes = 0; for (int len : lengths) { int pairsNeeded = len / 2; if (totalPairs >= pairsNeeded) { totalPairs -= pairsNeeded; palindromes++; } else { break; } } return palindromes; }}Complexity
Time
O(M + N log N), where M is the total number of characters in all words (for frequency counting) and N is the number of words (for sorting). This is the most efficient solution.
Space
O(N) to store the word lengths for sorting. The frequency map requires O(1) space.
Trade-offs
Pros
Highly efficient with a polynomial time complexity, much better than DP.
Simple to implement and understand once the greedy logic is clear.
Low space complexity.
Cons
The correctness of the greedy choice is not immediately obvious and relies on an understanding of the underlying resource allocation problem.
Solutions
Solution
class Solution {public int maxPalindromesAfterOperations(String[] words) { int s = 0, mask = 0; for (var w : words) { s += w.length(); for (var c : w.toCharArray()) { mask ^= 1 << (c - 'a'); } } s -= Integer.bitCount(mask); Arrays.sort(words, (a, b)->a.length() - b.length()); int ans = 0; for (var w : words) { s -= w.length() / 2 * 2; if (s < 0) { break; } ++ans; } return ans; }}class Solution {public: int maxPalindromesAfterOperations(vector<string> &words) { int s = 0, mask = 0; for (const auto &w : words) { s += w.length(); for (char c : w) { mask ^= 1 << (c - 'a'); } } s -= __builtin_popcount(mask); sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.length() < b.length(); }); int ans = 0; for (const auto &w : words) { s -= w.length() / 2 * 2; if (s < 0) { break; } ++ans; } return ans; }};class Solution: def maxPalindromesAfterOperations(self, words: List[str]) -> int: s = mask = 0 for w in words: s += len(w) for c in w: mask ^= 1 << (ord(c) - ord("a")) s -= mask . bit_count() words . sort(key=len) ans = 0 for w in words: s -= len(w) // 2 * 2 if s < 0: break ans += 1 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.