2131. Longest Palindrome by Concatenating Two Letter Words
Problem Description
You have an array of strings called words
, where each string contains exactly two lowercase English letters.
Your task is to create the longest possible palindrome by:
- Selecting some strings from
words
- Concatenating them in any order you choose
- Using each string at most once
Return the length of the longest palindrome you can create. If no palindrome can be formed, return 0
.
A palindrome reads the same forward and backward.
For example, if words = ["ab", "ba", "cc", "cc"]
, you could form:
- "abccccba" (length 8) by using "ab", "cc", "cc", "ba"
- The two "cc" strings form the middle "cccc", while "ab" and "ba" form the outer parts
The key insight is that:
- Strings with identical letters (like "aa", "cc") can be placed in pairs around the palindrome, with potentially one extra in the middle
- Strings with different letters (like "ab") need their reverse counterpart (like "ba") to maintain palindrome symmetry
Intuition
To build a palindrome, we need to think about what makes a valid palindrome structure. Since we're concatenating 2-letter strings, let's consider what types of strings can contribute to a palindrome:
-
Strings with identical letters (like "aa", "bb", "cc"): These are already palindromes themselves. We can place them anywhere in our final palindrome. Specifically, we can use pairs of such strings symmetrically around the center. If we have an odd number of any such string, we can place one in the middle.
-
Strings with different letters (like "ab", "cd"): For these to contribute to a palindrome, we need their reverse counterpart. For example, if we have "ab", we need "ba" to maintain symmetry. We place "ab" on one side and "ba" on the corresponding position on the other side.
The greedy approach emerges from maximizing the use of available strings:
- For identical-letter strings: Use as many pairs as possible (
v // 2 * 2
gives us the even count). Each pair contributes 4 to the total length (2 letters per string Γ 2 strings). - For different-letter strings: Use
min(v, cnt[reversed])
pairs, ensuring we don't exceed what's available for either the string or its reverse. Each pair contributes 4 to the length. - Finally, if we have any leftover identical-letter strings (tracked by
x
), we can place exactly one in the center of the palindrome, adding 2 more to the length.
This approach ensures we use the maximum number of strings while maintaining the palindrome property.
Learn more about Greedy patterns.
Solution Approach
We use a greedy approach with a hash table to count occurrences and build the maximum palindrome.
Step 1: Count Word Occurrences
cnt = Counter(words)
Create a hash table to store the frequency of each word. This allows us to quickly access how many times each word appears.
Step 2: Initialize Variables
ans = x = 0
ans
: tracks the total length of the palindromex
: counts words with identical letters that appear an odd number of times
Step 3: Process Each Word Type
for k, v in cnt.items():
Iterate through each unique word k
and its count v
.
Case 1: Words with Identical Letters (k[0] == k[1]
)
if k[0] == k[1]: x += v & 1 ans += v // 2 * 2 * 2
v & 1
: checks if count is odd (bitwise AND with 1). If odd, incrementx
v // 2 * 2
: gives us the even count (number of words we can use in pairs)- Multiply by 2 again because each word has 2 letters
- Example: If we have 5 "aa" strings, we use 4 of them (2 pairs), contributing 8 to length
Case 2: Words with Different Letters (k[0] != k[1]
)
else:
ans += min(v, cnt[k[::-1]]) * 2
k[::-1]
: reverses the word (e.g., "ab" becomes "ba")min(v, cnt[k[::-1]])
: finds how many matching pairs we can form- Multiply by 2 because each word has 2 letters
- Example: If we have 3 "ab" and 2 "ba", we can form 2 pairs, contributing 8 to length
Step 4: Add Center Word if Possible
ans += 2 if x else 0
If we have any word with identical letters appearing an odd number of times (x > 0
), we can place one such word in the center of the palindrome, adding 2 more to the length.
Time Complexity: O(n)
where n
is the number of words
Space Complexity: O(n)
for the hash table
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 words = ["ab", "ba", "aa", "aa", "cc"]
.
Step 1: Count Word Occurrences Create a frequency map:
- "ab": 1
- "ba": 1
- "aa": 2
- "cc": 1
Step 2: Initialize Variables
ans = 0
(total palindrome length)x = 0
(count of identical-letter words with odd frequency)
Step 3: Process Each Word
Processing "ab" (count = 1):
- Since "a" β "b", this is a different-letter word
- Check for reverse: "ba" has count = 1
- Can form
min(1, 1) = 1
pair - Add to answer:
ans += 1 * 2 = 2
(each word has 2 letters) - Running total:
ans = 2
Processing "ba" (count = 1):
- Since "b" β "a", this is a different-letter word
- Check for reverse: "ab" has count = 1
- Can form
min(1, 1) = 1
pair - Add to answer:
ans += 1 * 2 = 2
- Running total:
ans = 4
Processing "aa" (count = 2):
- Since "a" = "a", this is an identical-letter word
- Check if count is odd:
2 & 1 = 0
(even), sox
stays 0 - Use in pairs:
2 // 2 * 2 = 2
words can be used - Add to answer:
ans += 2 * 2 = 4
(2 words Γ 2 letters each) - Running total:
ans = 8
Processing "cc" (count = 1):
- Since "c" = "c", this is an identical-letter word
- Check if count is odd:
1 & 1 = 1
(odd), sox = 1
- Use in pairs:
1 // 2 * 2 = 0
words can be used in pairs - Add to answer:
ans += 0 * 2 = 0
- Running total:
ans = 8
Step 4: Add Center Word
Since x = 1
(we have an odd-count identical-letter word "cc"), we can place it in the center:
ans += 2
- Final answer:
ans = 10
Resulting Palindrome: One possible arrangement is "abaaccaaba" (length 10)
- "ab" + "aa" + "cc" + "aa" + "ba"
- The pair of "aa" and the pair "ab"/"ba" are placed symmetrically, with "cc" in the center
Solution Implementation
1class Solution:
2 def longestPalindrome(self, words: List[str]) -> int:
3 from collections import Counter
4
5 # Count frequency of each word
6 word_count = Counter(words)
7
8 # Initialize result and counter for palindromic words with odd frequency
9 result = 0
10 odd_palindrome_exists = 0
11
12 # Iterate through each unique word and its frequency
13 for word, frequency in word_count.items():
14 if word[0] == word[1]:
15 # Case 1: Word is a palindrome itself (e.g., "aa", "bb")
16 # Track if there's any palindromic word with odd frequency (for center placement)
17 odd_palindrome_exists += frequency & 1 # Add 1 if frequency is odd
18 # Add pairs of palindromic words (each pair contributes 4 characters)
19 result += (frequency // 2) * 2 * 2
20 else:
21 # Case 2: Word is not a palindrome (e.g., "ab", "ba")
22 # Find minimum count between word and its reverse to form valid pairs
23 # Each pair contributes 2 characters (divided by 2 to avoid double counting)
24 result += min(frequency, word_count[word[::-1]]) * 2
25
26 # If there's at least one palindromic word with odd frequency,
27 # we can place one in the center (adds 2 characters)
28 result += 2 if odd_palindrome_exists else 0
29
30 return result
31
1class Solution {
2 public int longestPalindrome(String[] words) {
3 // Count frequency of each word
4 Map<String, Integer> wordFrequency = new HashMap<>();
5 for (String word : words) {
6 wordFrequency.merge(word, 1, Integer::sum);
7 }
8
9 int totalLength = 0;
10 int oddPalindromicWords = 0;
11
12 // Process each unique word
13 for (Map.Entry<String, Integer> entry : wordFrequency.entrySet()) {
14 String word = entry.getKey();
15 String reversedWord = new StringBuilder(word).reverse().toString();
16 int frequency = entry.getValue();
17
18 // Check if word is palindromic (both characters are same)
19 if (word.charAt(0) == word.charAt(1)) {
20 // Track if there's an odd frequency (can be used as center)
21 oddPalindromicWords += frequency & 1;
22 // Use pairs of palindromic words (each pair contributes 4 characters)
23 totalLength += (frequency / 2) * 2 * 2;
24 } else {
25 // For non-palindromic words, use minimum of word and its reverse
26 // Each pair contributes 2 characters
27 totalLength += Math.min(frequency, wordFrequency.getOrDefault(reversedWord, 0)) * 2;
28 }
29 }
30
31 // If there's any odd palindromic word, we can use one as center (adds 2 characters)
32 totalLength += oddPalindromicWords > 0 ? 2 : 0;
33
34 return totalLength;
35 }
36}
37
1class Solution {
2public:
3 int longestPalindrome(vector<string>& words) {
4 // Count frequency of each word
5 unordered_map<string, int> wordFrequency;
6 for (const auto& word : words) {
7 wordFrequency[word]++;
8 }
9
10 int palindromeLength = 0;
11 int symmetricWordsWithOddCount = 0;
12
13 for (const auto& [word, frequency] : wordFrequency) {
14 // Create reversed version of current word
15 string reversedWord = word;
16 reverse(reversedWord.begin(), reversedWord.end());
17
18 if (word[0] == word[1]) {
19 // Handle symmetric words (like "aa", "bb")
20 // Track if we have odd occurrences (for potential center piece)
21 symmetricWordsWithOddCount += frequency & 1;
22 // Use pairs of symmetric words (each pair contributes 4 characters)
23 palindromeLength += (frequency / 2) * 2 * 2;
24 } else if (wordFrequency.count(reversedWord)) {
25 // Handle non-symmetric words that have their reverse present
26 // Use minimum count between word and its reverse
27 // Each matching pair contributes 2 characters
28 palindromeLength += min(frequency, wordFrequency[reversedWord]) * 2;
29 }
30 }
31
32 // If we have any symmetric word with odd count, we can place one in the center
33 if (symmetricWordsWithOddCount > 0) {
34 palindromeLength += 2;
35 }
36
37 return palindromeLength;
38 }
39};
40
1/**
2 * Finds the length of the longest palindrome that can be formed using given 2-letter words
3 * @param words - Array of 2-letter strings
4 * @returns Length of the longest possible palindrome
5 */
6function longestPalindrome(words: string[]): number {
7 // Map to store frequency count of each word
8 const wordFrequency = new Map<string, number>();
9
10 // Count occurrences of each word
11 for (const word of words) {
12 wordFrequency.set(word, (wordFrequency.get(word) || 0) + 1);
13 }
14
15 let palindromeLength = 0;
16 let hasOddSymmetricWord = 0;
17
18 // Process each unique word and its frequency
19 for (const [word, frequency] of wordFrequency.entries()) {
20 if (word[0] === word[1]) {
21 // Handle symmetric words (e.g., "aa", "bb")
22 // Track if there's an odd count for potential center piece
23 hasOddSymmetricWord += frequency & 1;
24 // Add pairs of symmetric words (each pair contributes 4 characters)
25 palindromeLength += Math.floor(frequency / 2) * 2 * 2;
26 } else {
27 // Handle asymmetric words (e.g., "ab", "cd")
28 // Find matching pairs with reversed word (e.g., "ab" with "ba")
29 const reversedWord = word[1] + word[0];
30 const reversedFrequency = wordFrequency.get(reversedWord) || 0;
31 // Each matching pair contributes 2 characters
32 palindromeLength += Math.min(frequency, reversedFrequency) * 2;
33 }
34 }
35
36 // If there's any symmetric word with odd count, we can use one as center
37 palindromeLength += hasOddSymmetricWord ? 2 : 0;
38
39 return palindromeLength;
40}
41
Time and Space Complexity
The time complexity is O(n)
, where n
is the number of words in the input list. This is because:
- Creating the Counter takes
O(n)
time as it needs to iterate through all words once - The subsequent loop iterates through the unique words in the Counter, which is at most
n
iterations - Each operation inside the loop (string comparison, reversal, arithmetic operations, and dictionary lookups) takes
O(1)
time since each word has fixed length of 2 - Therefore, the overall time complexity is
O(n) + O(n) = O(n)
The space complexity is O(n)
, where n
is the number of words. This is because:
- The Counter
cnt
stores at mostn
unique words as keys with their frequencies as values, requiringO(n)
space in the worst case when all words are unique - Other variables (
ans
,x
,k
,v
) use constant spaceO(1)
- The string reversal
k[::-1]
creates a temporary string of size 2, which isO(1)
- Therefore, the overall space complexity is
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Double Counting Non-Palindromic Pairs
The Problem: When processing non-palindromic words like "ab" and "ba", the current implementation counts each pair twice:
- When processing "ab", it counts pairs with "ba"
- When processing "ba", it counts the same pairs with "ab" again
This leads to an incorrect result that's double the actual palindrome length.
Example:
words = ["ab", "ba", "cd", "dc"] # Current code would incorrectly calculate: # - When processing "ab": min(1, 1) * 2 = 2 # - When processing "ba": min(1, 1) * 2 = 2 (double counting!) # - When processing "cd": min(1, 1) * 2 = 2 # - When processing "dc": min(1, 1) * 2 = 2 (double counting!) # Total: 8 (incorrect, should be 4)
Solution: Mark processed pairs or only count each pair once by checking if we've already processed the reverse:
class Solution:
def longestPalindrome(self, words: List[str]) -> int:
from collections import Counter
word_count = Counter(words)
result = 0
odd_palindrome_exists = False
processed = set()
for word, frequency in word_count.items():
if word in processed:
continue
if word[0] == word[1]:
# Palindromic word
if frequency % 2 == 1:
odd_palindrome_exists = True
result += (frequency // 2) * 4
else:
# Non-palindromic word
reversed_word = word[::-1]
if reversed_word in word_count:
pairs = min(frequency, word_count[reversed_word])
result += pairs * 4
processed.add(reversed_word) # Mark reverse as processed
processed.add(word)
# Add center palindrome if exists
if odd_palindrome_exists:
result += 2
return result
Pitfall 2: Incorrect Handling of Self-Palindromic Words
The Problem:
The variable x
(or odd_palindrome_exists
) accumulates the count of all palindromic words with odd frequencies, but we only need to know if at least one exists.
Example:
words = ["aa", "aa", "aa", "bb", "bb", "bb"] # Current: x = 2 (both "aa" and "bb" have odd count) # This doesn't affect correctness but is unnecessary
Better Approach: Use a boolean flag instead:
odd_palindrome_exists = False for word, frequency in word_count.items(): if word[0] == word[1]: if frequency % 2 == 1: odd_palindrome_exists = True # Just set flag, don't accumulate result += (frequency // 2) * 4
Pitfall 3: Edge Case with Single Self-Palindromic Word
The Problem: Not handling the case where we only have one self-palindromic word correctly.
Example:
words = ["aa"] # Should return 2 (can place "aa" in center) # Make sure the logic handles this case
Verification: Ensure your solution correctly handles:
- Single palindromic word β length 2
- Multiple identical palindromic words β use pairs + one center if odd count
- Mix of palindromic and non-palindromic words β combine both strategies
How does quick sort divide the problem into subproblems?
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!