3035. Maximum Palindromes After Operations
Problem Description
You have an array of strings called words
. You can perform a special operation as many times as you want (including zero times):
- Pick any two positions in the array (indices
i
andj
) - Pick any character position
x
inwords[i]
- Pick any character position
y
inwords[j]
- Swap the characters at these positions:
words[i][x]
andwords[j][y]
Note that you can swap characters between different words or within the same word (when i
equals j
).
Your goal is to maximize the number of palindromes in the array after performing these operations optimally. A palindrome is a string that reads the same forwards and backwards.
Example Understanding:
If you have words like ["abc", "def"]
, you could swap characters to potentially create ["aba", "def"]
where "aba"
is a palindrome. The key insight is that you have a pool of all characters from all words, and you can redistribute them however you want through swaps.
The problem asks you to return the maximum count of palindromic strings you can create in the array through optimal character swapping.
Key Points:
- You can swap any character from any position in any word with any other character
- The operation can be performed unlimited times
- You want to maximize the total number of palindromes (not the length of palindromes)
- The positions of words in the array don't change, only their characters can be rearranged through swaps
Intuition
Since we can swap any characters between any words, we essentially have a global pool of all characters that we can redistribute freely. This is the key insight - we're not bound by the original arrangement of characters in each word.
To form palindromes, we need to understand what makes a valid palindrome:
- For even-length strings: every character must appear an even number of times
- For odd-length strings: all characters except one must appear an even number of times
When we look at our total character pool, some characters will appear an odd number of times. These odd-count characters are "problematic" because they can't all be paired up. Each palindrome can accommodate at most one character with an odd count (placed in the middle for odd-length palindromes).
Here's the clever part: if we have k
characters with odd counts, we need at least k
odd-length positions across all our palindromes to place these unpaired characters. Any characters beyond what's needed for these middle positions can be paired up.
The strategy becomes:
- Count the total number of character pairs available (this is total characters minus the number of odd-count characters)
- Try to form palindromes starting with the shortest words first (greedy approach)
- For each word, we need
floor(length/2)
pairs to make it a palindrome - Keep forming palindromes until we run out of pairs
Why shortest first? Because shorter words require fewer pairs to become palindromes. By satisfying shorter words first, we maximize the total number of palindromes we can create with our limited pool of character pairs.
The bit manipulation in the solution (mask ^= 1 << (ord(c) - ord("a"))
) is just an efficient way to track which characters appear an odd number of times. XOR-ing a bit flips it, so characters appearing even times end up as 0, while odd times end up as 1.
Solution Approach
Let's walk through the implementation step by step:
Step 1: Count total characters and track odd-frequency characters
s = mask = 0
for w in words:
s += len(w)
for c in w:
mask ^= 1 << (ord(c) - ord("a"))
s
accumulates the total number of characters across all wordsmask
uses bit manipulation to track which characters appear an odd number of times- Each bit position (0-25) represents a letter ('a'-'z')
- XOR operation flips the bit: if a character appears even times, its bit becomes 0; odd times becomes 1
- For example, if 'a' appears 3 times, bit 0 will be 1; if 'b' appears 4 times, bit 1 will be 0
Step 2: Calculate available character pairs
s -= mask.bit_count()
mask.bit_count()
returns the number of 1s in the binary representation (number of characters with odd frequency)- These odd-frequency characters can't all be paired up, so we subtract them from the total
s
now represents the number of characters that can be paired (total usable pairs × 2)
Step 3: Greedy allocation starting with shortest words
words.sort(key=len)
ans = 0
for w in words:
s -= len(w) // 2 * 2
if s < 0:
break
ans += 1
- Sort words by length to prioritize shorter words (they need fewer pairs)
- For each word:
- Calculate pairs needed:
len(w) // 2 * 2
gives us the even number ≤ word length - This represents the number of paired characters needed (for odd-length words, the middle character can be any leftover)
- Subtract from available character count
s
- If we have enough characters (
s >= 0
), increment the palindrome count - If not enough characters remain (
s < 0
), stop - we can't form more palindromes
- Calculate pairs needed:
Why this works:
- By converting the problem to counting available pairs and greedily assigning them to shortest words first, we maximize the number of palindromes
- The algorithm correctly handles both even and odd-length palindromes by only requiring pairs for the even portion of each word's length
- Time complexity:
O(n * m + n log n)
wheren
is the number of words andm
is average word length - Space complexity:
O(1)
as we only use a fixed-size bitmask
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 an example with words = ["aa", "bc", "dd", "gg"]
.
Step 1: Count characters and track odd frequencies
- Total characters: 2 + 2 + 2 + 2 = 8
- Character frequencies: 'a' appears 2 times, 'b' appears 1 time, 'c' appears 1 time, 'd' appears 2 times, 'g' appears 2 times
- Using bit manipulation to track odd counts:
- Start with mask = 0
- Process "aa": 'a' appears twice, bit flips twice (0→1→0), mask stays 0
- Process "bc": 'b' once (bit 1 becomes 1), 'c' once (bit 2 becomes 1), mask = ...00110
- Process "dd": 'd' appears twice, bit flips twice, mask still = ...00110
- Process "gg": 'g' appears twice, bit flips twice, mask still = ...00110
- Final mask has 2 bits set (for 'b' and 'c')
Step 2: Calculate available pairs
- Total characters = 8
- Characters with odd frequency = 2 (b and c)
- Available paired characters = 8 - 2 = 6
Step 3: Greedy allocation
- Sort words by length: ["aa", "bc", "dd", "gg"] (already sorted, all length 2)
- Try to form palindromes:
- Word "aa" (length 2): needs 2 characters for pairs, we have 6, use them. Remaining = 4
- Word "bc" (length 2): needs 2 characters for pairs, we have 4, use them. Remaining = 2
- Word "dd" (length 2): needs 2 characters for pairs, we have 2, use them. Remaining = 0
- Word "gg" (length 2): needs 2 characters, we have 0, cannot form palindrome
Result: We can form 3 palindromes
What actually happens with the characters:
- We can rearrange to get: ["aa", "bb", "dd", "gg"] - but wait, we only have one 'b'!
- Actually, with our character pool (aa, b, c, dd, gg), we can form: ["aa", "dd", "gg", "bc"]
- The 'b' and 'c' (our odd-count characters) prevent us from making all 4 words palindromes
- Best we can do is 3 palindromes, for example: ["aa", "bb", "dd", "cg"] or ["aa", "cc", "dd", "bg"]
Let's try words = ["a", "abc", "defg", "hh"]
.
Step 1: Count characters and track odd frequencies
- Total characters: 1 + 3 + 4 + 2 = 10
- Character frequencies: a(2), b(1), c(1), d(1), e(1), f(1), g(1), h(2)
- Mask will have 6 bits set (for b, c, d, e, f, g - all appearing once)
Step 2: Calculate available pairs
- Total characters = 10
- Characters with odd frequency = 6
- Available paired characters = 10 - 6 = 4
Step 3: Greedy allocation
- Sort by length: ["a", "hh", "abc", "defg"]
- Try to form palindromes:
- Word "a" (length 1): needs 0 paired characters (odd length, middle can be any), we have 4. Remaining = 4
- Word "hh" (length 2): needs 2 paired characters, we have 4, use them. Remaining = 2
- Word "abc" (length 3): needs 2 paired characters (floor(3/2) * 2), we have 2, use them. Remaining = 0
- Word "defg" (length 4): needs 4 paired characters, we have 0, cannot form palindrome
Result: We can form 3 palindromes
The actual rearrangement could be: ["a", "hh", "aba", "defg"] where we've moved characters around to create 3 palindromes, using the unpaired characters wisely as middle characters in odd-length palindromes.
Solution Implementation
1class Solution:
2 def maxPalindromesAfterOperations(self, words: List[str]) -> int:
3 # Total number of characters across all words
4 total_chars = 0
5
6 # Bitmask to track odd/even frequency of each character
7 # Bit i is 1 if character (a+i) appears odd number of times
8 parity_mask = 0
9
10 # Count total characters and track character frequency parity
11 for word in words:
12 total_chars += len(word)
13 for char in word:
14 # Toggle bit corresponding to this character
15 # XOR flips bit: even->odd or odd->even
16 char_index = ord(char) - ord('a')
17 parity_mask ^= 1 << char_index
18
19 # Calculate number of character pairs available for palindromes
20 # Subtract odd frequency characters as they can't form pairs
21 available_pairs = total_chars - parity_mask.bit_count()
22
23 # Sort words by length to greedily fill shortest words first
24 # This maximizes the number of palindromes we can form
25 words.sort(key=len)
26
27 # Count how many words can be rearranged into palindromes
28 palindrome_count = 0
29
30 for word in words:
31 # Calculate pairs needed for this word
32 # For palindrome: even length needs len/2 pairs, odd needs (len-1)/2 pairs
33 pairs_needed = len(word) // 2 * 2
34
35 # Deduct required pairs from available pool
36 available_pairs -= pairs_needed
37
38 # If we don't have enough pairs, stop
39 if available_pairs < 0:
40 break
41
42 # Successfully formed a palindrome with this word
43 palindrome_count += 1
44
45 return palindrome_count
46
1class Solution {
2 public int maxPalindromesAfterOperations(String[] words) {
3 // Total number of characters available across all words
4 int totalCharacters = 0;
5
6 // Bitmask to track character frequencies (odd/even)
7 // Each bit represents whether a character appears odd times
8 int frequencyMask = 0;
9
10 // Count total characters and track character frequency parity
11 for (String word : words) {
12 totalCharacters += word.length();
13
14 // XOR toggle bits to track odd/even occurrences of each character
15 for (char character : word.toCharArray()) {
16 frequencyMask ^= 1 << (character - 'a');
17 }
18 }
19
20 // Subtract the number of characters with odd frequency
21 // These characters can only be used as middle characters in palindromes
22 totalCharacters -= Integer.bitCount(frequencyMask);
23
24 // Sort words by length in ascending order
25 // Greedy approach: try to form palindromes from shortest words first
26 Arrays.sort(words, (word1, word2) -> word1.length() - word2.length());
27
28 // Count how many palindromes can be formed
29 int palindromeCount = 0;
30
31 for (String word : words) {
32 // For each word, we need pairs of characters to form a palindrome
33 // word.length() / 2 * 2 gives us the even number of characters needed
34 int evenCharactersNeeded = word.length() / 2 * 2;
35 totalCharacters -= evenCharactersNeeded;
36
37 // If we don't have enough characters left, stop
38 if (totalCharacters < 0) {
39 break;
40 }
41
42 // Successfully formed a palindrome with this word
43 ++palindromeCount;
44 }
45
46 return palindromeCount;
47 }
48}
49
1class Solution {
2public:
3 int maxPalindromesAfterOperations(vector<string>& words) {
4 // Total number of characters across all words
5 int totalChars = 0;
6
7 // Bitmask to track parity of character frequencies
8 // If bit i is set, character ('a' + i) appears odd number of times
9 int parityMask = 0;
10
11 // Count total characters and track character frequency parity
12 for (const auto& word : words) {
13 totalChars += word.length();
14
15 for (char ch : word) {
16 // XOR toggles the bit - if character appears even times, bit becomes 0
17 parityMask ^= 1 << (ch - 'a');
18 }
19 }
20
21 // Calculate number of character pairs available for palindromes
22 // Subtract odd frequency characters as they can't form pairs
23 int availablePairs = totalChars - __builtin_popcount(parityMask);
24
25 // Sort words by length to greedily fill shorter words first
26 sort(words.begin(), words.end(),
27 [](const string& a, const string& b) {
28 return a.length() < b.length();
29 });
30
31 int palindromeCount = 0;
32
33 // Try to make each word a palindrome, starting with shortest
34 for (const auto& word : words) {
35 // Each word needs (length / 2) pairs to form a palindrome
36 // For odd length words, the middle character doesn't need a pair
37 int pairsNeeded = word.length() / 2 * 2;
38
39 availablePairs -= pairsNeeded;
40
41 // If we don't have enough pairs, we can't make more palindromes
42 if (availablePairs < 0) {
43 break;
44 }
45
46 ++palindromeCount;
47 }
48
49 return palindromeCount;
50 }
51};
52
1/**
2 * Calculates the maximum number of palindromes that can be formed after operations
3 * @param words - Array of strings to process
4 * @returns Maximum number of palindromes possible
5 */
6function maxPalindromesAfterOperations(words: string[]): number {
7 // Total length of all characters across all words
8 let totalCharacters: number = 0;
9
10 // Bitmask to track odd frequency characters
11 // Each bit position represents a character ('a' = bit 0, 'b' = bit 1, etc.)
12 let oddFrequencyMask: number = 0;
13
14 // Count total characters and track character frequencies using XOR
15 for (const word of words) {
16 totalCharacters += word.length;
17
18 // XOR toggles bit for each character occurrence
19 // After all XORs, bit is 1 if character appears odd times, 0 if even
20 for (const character of word) {
21 const charIndex: number = character.charCodeAt(0) - 'a'.charCodeAt(0);
22 oddFrequencyMask ^= 1 << charIndex;
23 }
24 }
25
26 // Count number of characters with odd frequency (number of 1s in bitmask)
27 const oddFrequencyCount: number = (oddFrequencyMask.toString(2).match(/1/g) || []).length;
28
29 // Subtract odd frequency characters as they can only be used as palindrome centers
30 totalCharacters -= oddFrequencyCount;
31
32 // Sort words by length in ascending order (greedy approach - fill smaller words first)
33 words.sort((a: string, b: string) => a.length - b.length);
34
35 let palindromeCount: number = 0;
36
37 // Try to form palindromes starting with shortest words
38 for (const word of words) {
39 // For each word, we need pairs of characters (floor division by 2, then multiply by 2)
40 const pairsNeeded: number = Math.floor(word.length / 2) * 2;
41 totalCharacters -= pairsNeeded;
42
43 // If we don't have enough character pairs left, stop
44 if (totalCharacters < 0) {
45 break;
46 }
47
48 // Successfully formed another palindrome
49 palindromeCount++;
50 }
51
52 return palindromeCount;
53}
54
Time and Space Complexity
Time Complexity: O(n * m + n * log n)
where n
is the number of words and m
is the average length of each word.
- Iterating through all words to calculate total length and XOR mask:
O(n * m)
- we visit each character in each word once - The XOR operation for each character:
O(1)
mask.bit_count()
:O(1)
since the mask has at most 26 bits (for 26 lowercase letters)- Sorting the words by length:
O(n * log n)
- Final iteration through sorted words:
O(n)
- Overall:
O(n * m + n * log n)
Space Complexity: O(1)
or O(n)
depending on whether we count the sorting space.
- Variables
s
,mask
, andans
:O(1)
- The mask uses a fixed 26-bit integer regardless of input size:
O(1)
- Sorting may require
O(n)
auxiliary space depending on the implementation (Timsort in Python) - If we don't count the sorting space (treating it as in-place):
O(1)
- If we count the sorting space:
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding Character Distribution
Issue: A common mistake is thinking you need to preserve the original characters of each word when forming palindromes. Developers might try to check if each individual word can be rearranged into a palindrome using only its own characters.
Wrong Approach:
def maxPalindromesAfterOperations(words):
count = 0
for word in words:
char_freq = {}
for c in word:
char_freq[c] = char_freq.get(c, 0) + 1
odd_count = sum(1 for freq in char_freq.values() if freq % 2 == 1)
if odd_count <= 1: # Can form palindrome
count += 1
return count
Why it's wrong: The problem allows swapping characters between ANY words, creating a global pool of characters that can be redistributed optimally.
Solution: Treat all characters from all words as a shared pool, then distribute them optimally to maximize palindrome count.
Pitfall 2: Incorrect Handling of Odd-Length Palindromes
Issue: Forgetting that odd-length palindromes can use any leftover single character as their center, not just characters that appear an odd number of times in the input.
Wrong Approach:
# Incorrectly assuming we need specific odd-frequency characters for odd-length words
odd_chars = mask.bit_count()
for word in words:
if len(word) % 2 == 1:
if odd_chars > 0:
odd_chars -= 1
# Form palindrome
else:
# Skip this word
Why it's wrong: Any single character (from odd-frequency characters OR from breaking a pair) can serve as the center of an odd-length palindrome.
Solution: Calculate the total available characters after accounting for pairs. For odd-length words, we only need pairs for the even portion (len(word) // 2 * 2), and any remaining character can be the center.
Pitfall 3: Not Sorting Words by Length
Issue: Trying to fill words in their original order or sorting in descending order by length.
Wrong Approach:
# Processing in original order
for word in words:
pairs_needed = len(word) // 2 * 2
if available_pairs >= pairs_needed:
available_pairs -= pairs_needed
count += 1
# OR sorting longest first
words.sort(key=len, reverse=True)
Why it's wrong: Larger words consume more character pairs. If we fill them first, we might exhaust our character pool and miss the opportunity to create more palindromes with shorter words.
Solution: Always sort words by length in ascending order. This greedy approach ensures we create the maximum number of palindromes by filling shorter words first.
Pitfall 4: Overcounting Available Characters
Issue: Not properly accounting for the fact that palindromes need character PAIRS (except for the potential center character in odd-length palindromes).
Wrong Approach:
total_chars = sum(len(w) for w in words)
# Using total_chars directly without considering pairing requirements
Why it's wrong: If you have 5 'a's, you can only use 4 of them in pairs (2 pairs), with 1 leftover that can only be used as a center character.
Solution: After counting total characters, subtract the number of characters with odd frequencies (these are the "leftover" characters that can't form pairs). The remaining count represents characters available for pairing.
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!