Facebook Pixel

2744. Find Maximum Number of String Pairs

EasyArrayHash TableStringSimulation
Leetcode Link

Problem Description

You are given a 0-indexed array words containing distinct strings. Your task is to find the maximum number of pairs that can be formed from this array.

Two strings can form a pair if they meet these conditions:

  • One string is exactly the reverse of the other string
  • They are at different positions in the array where the first position index is less than the second position index (0 <= i < j < words.length)
  • Each string can be used in at most one pair

For example, if you have words = ["cd", "ac", "dc", "ca", "zz"], you can form 2 pairs:

  • "cd" (index 0) pairs with "dc" (index 2) because "cd" reversed is "dc"
  • "ac" (index 1) pairs with "ca" (index 3) because "ac" reversed is "ca"
  • "zz" cannot form a pair because there's no other "zz" in the array (its reverse is itself)

The solution uses a hash table to count occurrences. As we iterate through each word w:

  1. We check if the reversed version of w (written as w[::-1]) already exists in our counter
  2. If it does, we can form a pair, so we add that count to our answer
  3. We then add the current word w to our counter for future potential pairs

This approach ensures we only count valid pairs where i < j since we're checking for reversed strings that appeared before the current position.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to find pairs where one string is the reverse of another, and we want to maximize the number of such pairs. Since each string can only be used once, we need an efficient way to match strings with their reverses.

Think about what happens when we traverse the array from left to right. For each string we encounter, we want to know: "Has the reverse of this string appeared before?" If it has, we can form a pair immediately.

This naturally leads us to use a hash table approach. As we go through the array:

  • For the current string, check if its reverse has been seen before
  • If yes, we can form pairs with all previous occurrences of the reversed string
  • Then, record the current string for future potential matches

Why does this work? Consider the constraint i < j. When we're at position j, we only need to look backwards for potential pairs at positions i where i < j. By maintaining a counter of all strings we've seen so far, we automatically satisfy this ordering constraint.

The beauty of this approach is that we handle the pairing greedily - whenever we find a match, we count it immediately. Since the problem asks for the maximum number of pairs and each string can only be used once, this greedy approach is optimal. We don't need to worry about "saving" a string for a better match later because any valid pairing contributes equally to our count.

The time complexity is O(n) where n is the number of words, making this solution very efficient compared to checking all possible pairs which would be O(n²).

Solution Approach

The solution uses a hash table to efficiently track and match strings with their reverses. Here's how the implementation works:

Data Structure: We use a Counter object (hash table) called cnt to store the frequency of each string we've encountered so far.

Algorithm Steps:

  1. Initialize variables:

    • cnt = Counter() - An empty hash table to track string occurrences
    • ans = 0 - Counter for the number of pairs formed
  2. Iterate through each word in the words array:

    for w in words:
  3. Check for reverse match:

    ans += cnt[w[::-1]]
    • w[::-1] generates the reversed string of w
    • cnt[w[::-1]] returns the count of how many times the reversed string has appeared before
    • We add this count to ans because each previous occurrence of the reversed string can form a pair with the current string
  4. Update the counter:

    cnt[w] += 1
    • Record the current string in our hash table for future potential matches
    • This happens after checking for pairs to ensure we don't pair a string with itself

Why this works:

  • The order of operations is crucial: we check for matches before adding the current string
  • This ensures we only count pairs where i < j (the reversed string appeared at an earlier index)
  • Each string is counted exactly once in a pair because we only look backwards

Example walkthrough with words = ["cd", "ac", "dc", "ca"]:

  • Process "cd": cnt["dc"] = 0, no pair formed, update cnt["cd"] = 1
  • Process "ac": cnt["ca"] = 0, no pair formed, update cnt["ac"] = 1
  • Process "dc": cnt["cd"] = 1, one pair formed! ans = 1, update cnt["dc"] = 1
  • Process "ca": cnt["ac"] = 1, one pair formed! ans = 2, update cnt["ca"] = 1
  • Final answer: 2 pairs

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Example: words = ["ab", "ba", "cc", "ba", "ab", "ba"]

We'll trace through the algorithm step by step:

Initial State:

  • cnt = {} (empty counter)
  • ans = 0 (no pairs yet)

Step 1 - Process "ab" (index 0):

  • Check if reverse "ba" exists in cnt: No (count = 0)
  • Add 0 to ans: ans = 0
  • Update counter: cnt = {"ab": 1}

Step 2 - Process "ba" (index 1):

  • Check if reverse "ab" exists in cnt: Yes! (count = 1)
  • Add 1 to ans: ans = 1 (formed pair: "ab"[0] with "ba"[1])
  • Update counter: cnt = {"ab": 1, "ba": 1}

Step 3 - Process "cc" (index 2):

  • Check if reverse "cc" exists in cnt: No (count = 0)
  • Note: "cc" reversed is still "cc", but there's no previous "cc"
  • Add 0 to ans: ans = 1
  • Update counter: cnt = {"ab": 1, "ba": 1, "cc": 1}

Step 4 - Process "ba" (index 3):

  • Check if reverse "ab" exists in cnt: Yes! (count = 1)
  • Add 1 to ans: ans = 2 (formed pair: "ab"[0] with "ba"[3])
  • Note: The "ab" at index 0 hasn't been used yet, so it can pair
  • Update counter: cnt = {"ab": 1, "ba": 2}

Step 5 - Process "ab" (index 4):

  • Check if reverse "ba" exists in cnt: Yes! (count = 2)
  • Add 2 to ans: ans = 4 (can form pairs with both "ba"[1] and "ba"[3])
  • Update counter: cnt = {"ab": 2, "ba": 2}

Step 6 - Process "ba" (index 5):

  • Check if reverse "ab" exists in cnt: Yes! (count = 2)
  • Add 2 to ans: ans = 6 (can form pairs with "ab"[0] and "ab"[4])
  • Update counter: cnt = {"ab": 2, "ba": 3}

Final Answer: 6 pairs

The key insight is that when we encounter a string, we can form as many pairs as there are previous occurrences of its reverse. This greedy approach maximizes the number of pairs while respecting the constraint that each string position can only be used once.

Solution Implementation

1class Solution:
2    def maximumNumberOfStringPairs(self, words: List[str]) -> int:
3        # Dictionary to count occurrences of each word
4        word_count = Counter()
5        # Variable to store the number of valid pairs
6        pair_count = 0
7      
8        # Iterate through each word in the list
9        for word in words:
10            # Check if the reverse of current word exists in our counter
11            # If it exists, it means we can form a pair
12            pair_count += word_count[word[::-1]]
13          
14            # Add the current word to our counter for future pairing
15            word_count[word] += 1
16      
17        return pair_count
18
1class Solution {
2    public int maximumNumberOfStringPairs(String[] words) {
3        // Map to store encoded string representations and their counts
4        // Key: encoded value of string (first char << 5 | second char)
5        // Value: count of occurrences
6        Map<Integer, Integer> encodedStringCount = new HashMap<>();
7        int pairCount = 0;
8      
9        // Iterate through each word in the array
10        for (String word : words) {
11            // Extract the first and second characters as 0-based indices (a=0, b=1, etc.)
12            int firstChar = word.charAt(0) - 'a';
13            int secondChar = word.charAt(1) - 'a';
14          
15            // Check if the reverse of current word exists in the map
16            // Reverse is encoded as: secondChar << 5 | firstChar
17            // If found, increment pair count by the number of reverses found
18            int reverseEncoding = (secondChar << 5) | firstChar;
19            pairCount += encodedStringCount.getOrDefault(reverseEncoding, 0);
20          
21            // Add current word to the map with its encoding: firstChar << 5 | secondChar
22            // The << 5 operation ensures unique encoding for each two-character combination
23            // since we have at most 26 letters (needs 5 bits: 2^5 = 32 > 26)
24            int currentEncoding = (firstChar << 5) | secondChar;
25            encodedStringCount.merge(currentEncoding, 1, Integer::sum);
26        }
27      
28        return pairCount;
29    }
30}
31
1class Solution {
2public:
3    int maximumNumberOfStringPairs(vector<string>& words) {
4        // Hash map to store the count of each string pattern
5        // Key: encoded value of the string (first_char << 5 | second_char)
6        unordered_map<int, int> stringPatternCount;
7      
8        int pairCount = 0;
9      
10        // Iterate through each word in the array
11        for (auto& word : words) {
12            // Extract the two characters and convert to 0-based index
13            int firstChar = word[0] - 'a';
14            int secondChar = word[1] - 'a';
15          
16            // Check if the reverse pattern exists in the map
17            // Reverse pattern: secondChar in high bits, firstChar in low bits
18            int reversePattern = (secondChar << 5) | firstChar;
19            pairCount += stringPatternCount[reversePattern];
20          
21            // Store the current pattern in the map
22            // Current pattern: firstChar in high bits, secondChar in low bits
23            int currentPattern = (firstChar << 5) | secondChar;
24            stringPatternCount[currentPattern]++;
25        }
26      
27        return pairCount;
28    }
29};
30
1/**
2 * Counts the maximum number of string pairs where one string is the reverse of another
3 * @param words - Array of strings to check for reverse pairs
4 * @returns The number of valid string pairs found
5 */
6function maximumNumberOfStringPairs(words: string[]): number {
7    // Map to store encoded character pairs as keys and their counts as values
8    // Using number index signature for better TypeScript syntax
9    const pairCount: Record<number, number> = {};
10  
11    // Counter for the total number of valid pairs
12    let totalPairs = 0;
13  
14    // Iterate through each word in the array
15    for (const word of words) {
16        // Extract first and last characters and convert to 0-based indices (a=0, b=1, ..., z=25)
17        const firstCharIndex = word.charCodeAt(0) - 97;
18        const lastCharIndex = word.charCodeAt(word.length - 1) - 97;
19      
20        // Check if the reverse pair exists in the map
21        // Encode as: (lastChar << 5) | firstChar to create unique key for reverse
22        const reversePairKey = (lastCharIndex << 5) | firstCharIndex;
23        totalPairs += pairCount[reversePairKey] || 0;
24      
25        // Store current pair in the map for future matching
26        // Encode as: (firstChar << 5) | lastChar to create unique key
27        const currentPairKey = (firstCharIndex << 5) | lastCharIndex;
28        pairCount[currentPairKey] = (pairCount[currentPairKey] || 0) + 1;
29    }
30  
31    return totalPairs;
32}
33

Time and Space Complexity

The time complexity is O(n × m), where n is the length of the array words and m is the average length of each word string. This is because:

  • The algorithm iterates through all n words once
  • For each word w, it performs w[::-1] which takes O(m) time to reverse the string
  • The hash table operations (lookup and insertion) take O(m) time since strings are used as keys and comparing/hashing strings requires examining their characters

The space complexity is O(n × m). This is because:

  • The Counter dictionary cnt can store up to n unique words as keys
  • Each key (word) has length m on average, so storing all keys requires O(n × m) space
  • The reversed string w[::-1] creates a temporary string of length m, but this doesn't affect the overall space complexity

Note: The reference answer simplifies this to O(n) by likely assuming constant-length strings or treating string operations as O(1), which is a common simplification in some contexts where string length is bounded by a small constant.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Attempting to Pair a Palindrome with Itself

The Pitfall: When encountering palindromes (strings that read the same forwards and backwards, like "aa", "aba"), developers might incorrectly count them as pairs since the string equals its own reverse.

Why it happens: The condition cnt[w[::-1]] will find the palindrome itself if we're not careful about the order of operations.

Example of incorrect approach:

# WRONG: Adding to counter before checking
for word in words:
    word_count[word] += 1  # Adding first (WRONG!)
    pair_count += word_count[word[::-1]]

With words = ["aa", "bb"], this would incorrectly count "aa" as forming a pair with itself.

The Solution: The correct implementation checks for the reverse BEFORE adding the current word to the counter. This ensures a palindrome won't match with itself:

# CORRECT: Check first, then add
for word in words:
    pair_count += word_count[word[::-1]]  # Check first
    word_count[word] += 1  # Then add

2. Double-Counting Pairs

The Pitfall: Trying to count all possible matches at once without considering the constraint that each string can only be used once.

Example of incorrect approach:

# WRONG: This might count the same pair multiple times
for i in range(len(words)):
    for j in range(i+1, len(words)):
        if words[i] == words[j][::-1]:
            pair_count += 1

The Solution: The hash table approach naturally prevents double-counting because once we've found a match for a string with its reverse, we only increment the counter by the exact number of available reverses seen so far.

3. Handling Duplicate Strings Incorrectly

The Pitfall: Although the problem states the array contains "distinct strings", if there were duplicates, a naive approach might pair a string with another instance of the same string at a different index.

Example scenario: If words = ["ab", "ba", "ab"], we should form 2 pairs, not 1.

The Solution: The counter-based approach handles this correctly by counting occurrences. Each "ab" can pair with each previously seen "ba", giving us the correct count automatically through pair_count += word_count[word[::-1]].

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More