Facebook Pixel

1657. Determine if Two Strings Are Close

MediumHash TableStringCountingSorting
Leetcode Link

Problem Description

You are given two strings word1 and word2. Two strings are considered close if you can transform one into the other using these operations:

Operation 1: Swap any two existing characters in the string.

  • For example: "abcde" can become "aecdb" by swapping characters at positions 1 and 4.

Operation 2: Transform every occurrence of one existing character into another existing character, and simultaneously transform every occurrence of the second character into the first.

  • For example: "aacabb" can become "bbcbaa" by swapping all a's with all b's.

You can apply these operations any number of times on either string.

Your task is to determine if word1 and word2 are close. Return true if they are close, and false otherwise.

The key insight is that two strings are close if and only if:

  1. Both strings contain exactly the same set of unique characters
  2. The frequency counts of characters (when sorted) are identical between the two strings

For example:

  • "abc" and "bca" are close (can use Operation 1 to rearrange)
  • "cabbba" and "abbccc" are close (both have characters {a,b,c} and frequency pattern [1,2,3])
  • "cabbba" and "aabbss" are not close (different character sets)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Let's think about what each operation actually allows us to do:

Operation 1 (swapping characters) means we can rearrange the string in any order we want. This tells us that the actual positions of characters don't matter - only what characters we have and how many of each.

Operation 2 (transforming all occurrences of one character to another and vice versa) is more interesting. If we have 3 a's and 5 b's, we can transform them into 5 a's and 3 b's. Essentially, this operation lets us swap the frequencies between two characters.

With these observations, we can deduce:

  1. The character set must be identical: We can only work with existing characters. We can't create new character types or eliminate existing ones. So if word1 has characters {a, b, c} and word2 has {a, b, d}, they can never be close.

  2. The frequency distribution must match: Since Operation 2 lets us swap frequencies between characters, what matters is not which specific character has which frequency, but rather the collection of all frequencies. For example, if word1 has frequencies [2, 3, 5] for its characters, then word2 must also have some arrangement of frequencies [2, 3, 5] (though possibly assigned to different characters).

Therefore, two strings are close if and only if:

  • They contain the exact same set of unique characters
  • When we collect all the frequency counts and sort them, both strings yield the same sorted frequency list

This leads us directly to the solution: count character frequencies for both strings, check if they use the same characters, and verify that their sorted frequency lists match.

Learn more about Sorting patterns.

Solution Approach

Based on our intuition, we need to check two conditions:

  1. Both strings must have the same set of unique characters
  2. The sorted frequency counts must be identical

Here's how we implement this step by step:

Step 1: Count character frequencies We use Python's Counter from the collections module to count the occurrences of each character in both strings:

cnt1, cnt2 = Counter(word1), Counter(word2)

This gives us dictionaries where keys are characters and values are their frequencies.

Step 2: Check if character sets are identical We extract the keys (unique characters) from both counters and compare them as sets:

set(cnt1.keys()) == set(cnt2.keys())

If the character sets don't match, the strings cannot be close.

Step 3: Check if frequency distributions match We extract the frequency values from both counters, sort them, and compare:

sorted(cnt1.values()) == sorted(cnt2.values())

Sorting is crucial here because Operation 2 allows us to swap frequencies between any two characters. For example, if word1 has frequencies {a:3, b:2, c:1} and word2 has {a:1, b:3, c:2}, their sorted frequency lists are both [1, 2, 3], so they can be made close.

Step 4: Return the combined result The function returns True only if both conditions are satisfied:

return sorted(cnt1.values()) == sorted(cnt2.values()) and set(cnt1.keys()) == set(cnt2.keys())

Time Complexity: O(n log n) where n is the length of the strings, due to sorting the frequency values.

Space Complexity: O(1) since we're only storing character counts, and there are at most 26 lowercase English letters.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through the solution with word1 = "cabbba" and word2 = "abbccc".

Step 1: Count character frequencies

  • For word1 = "cabbba":

    • c appears 1 time
    • a appears 2 times
    • b appears 3 times
    • So cnt1 = {c: 1, a: 2, b: 3}
  • For word2 = "abbccc":

    • a appears 1 time
    • b appears 2 times
    • c appears 3 times
    • So cnt2 = {a: 1, b: 2, c: 3}

Step 2: Check if character sets are identical

  • Characters in word1: {a, b, c}
  • Characters in word2: {a, b, c}
  • Since both sets are {a, b, c}, this condition passes βœ“

Step 3: Check if frequency distributions match

  • Frequencies in word1: [1, 2, 3] (from c:1, a:2, b:3)
  • Frequencies in word2: [1, 2, 3] (from a:1, b:2, c:3)
  • After sorting both are [1, 2, 3], this condition passes βœ“

Step 4: Return the result Both conditions are satisfied, so the function returns True.

How could we transform one to the other? We can use Operation 2 to swap frequencies:

  1. Transform all a's to c's and all c's to a's in word1:
    • "cabbba" becomes "aabbbc" (now a:1, b:3, c:2)
  2. Transform all b's to c's and all c's to b's:
    • "aabbbc" becomes "aacccc" (now a:1, b:2, c:3)
  3. Use Operation 1 to rearrange to match word2:
    • "aacccc" can be rearranged to "abbccc"

Solution Implementation

1from collections import Counter
2from typing import List
3
4
5class Solution:
6    def closeStrings(self, word1: str, word2: str) -> bool:
7        # Count the frequency of each character in both words
8        frequency_map1 = Counter(word1)
9        frequency_map2 = Counter(word2)
10      
11        # Check two conditions for strings to be "close":
12        # 1. Both strings must contain the exact same set of unique characters
13        same_characters = set(frequency_map1.keys()) == set(frequency_map2.keys())
14      
15        # 2. The frequency values must match when sorted
16        # (characters can be swapped to rearrange frequencies)
17        same_frequency_distribution = sorted(frequency_map1.values()) == sorted(frequency_map2.values())
18      
19        # Both conditions must be true for the strings to be close
20        return same_characters and same_frequency_distribution
21
1class Solution {
2    public boolean closeStrings(String word1, String word2) {
3        // Create frequency arrays for both strings (26 letters in alphabet)
4        int[] frequencyArray1 = new int[26];
5        int[] frequencyArray2 = new int[26];
6      
7        // Count character frequencies in word1
8        for (int i = 0; i < word1.length(); i++) {
9            frequencyArray1[word1.charAt(i) - 'a']++;
10        }
11      
12        // Count character frequencies in word2
13        for (int i = 0; i < word2.length(); i++) {
14            frequencyArray2[word2.charAt(i) - 'a']++;
15        }
16      
17        // Check if both strings have the same set of unique characters
18        // If one string has a character that the other doesn't, they can't be close
19        for (int i = 0; i < 26; i++) {
20            if ((frequencyArray1[i] == 0) != (frequencyArray2[i] == 0)) {
21                return false;
22            }
23        }
24      
25        // Sort both frequency arrays to compare frequency distributions
26        Arrays.sort(frequencyArray1);
27        Arrays.sort(frequencyArray2);
28      
29        // Check if the sorted frequency arrays are equal
30        // This verifies that both strings have the same frequency distribution
31        return Arrays.equals(frequencyArray1, frequencyArray2);
32    }
33}
34
1class Solution {
2public:
3    bool closeStrings(string word1, string word2) {
4        // Initialize frequency arrays for both strings (26 letters in alphabet)
5        int frequency1[26]{};
6        int frequency2[26]{};
7      
8        // Count character frequencies in word1
9        for (char& c : word1) {
10            ++frequency1[c - 'a'];
11        }
12      
13        // Count character frequencies in word2
14        for (char& c : word2) {
15            ++frequency2[c - 'a'];
16        }
17      
18        // Check if both strings have the same set of characters
19        // If one string has a character that the other doesn't, they can't be close
20        for (int i = 0; i < 26; ++i) {
21            if ((frequency1[i] == 0) != (frequency2[i] == 0)) {
22                return false;
23            }
24        }
25      
26        // Sort frequency arrays to check if they have the same frequency distribution
27        sort(frequency1, frequency1 + 26);
28        sort(frequency2, frequency2 + 26);
29      
30        // Check if sorted frequency arrays are equal
31        // This ensures both strings have the same frequency values (just possibly for different characters)
32        return equal(frequency1, frequency1 + 26, frequency2);
33    }
34};
35
1/**
2 * Determines if two words are "close strings" based on character operations.
3 * Two strings are considered close if you can attain one from the other using:
4 * 1. Swap any two existing characters
5 * 2. Transform every occurrence of one existing character into another existing character
6 * 
7 * @param word1 - The first string to compare
8 * @param word2 - The second string to compare
9 * @returns true if the strings are close, false otherwise
10 */
11function closeStrings(word1: string, word2: string): boolean {
12    // Initialize frequency arrays for all 26 lowercase letters
13    const frequencyArray1: number[] = Array(26).fill(0);
14    const frequencyArray2: number[] = Array(26).fill(0);
15  
16    // Count character frequencies in word1
17    for (const character of word1) {
18        const alphabetIndex: number = character.charCodeAt(0) - 'a'.charCodeAt(0);
19        frequencyArray1[alphabetIndex]++;
20    }
21  
22    // Count character frequencies in word2
23    for (const character of word2) {
24        const alphabetIndex: number = character.charCodeAt(0) - 'a'.charCodeAt(0);
25        frequencyArray2[alphabetIndex]++;
26    }
27  
28    // Check if both strings have the same set of unique characters
29    // If one string has a character that the other doesn't, they can't be close
30    for (let i = 0; i < 26; i++) {
31        const char1Exists: boolean = frequencyArray1[i] !== 0;
32        const char2Exists: boolean = frequencyArray2[i] !== 0;
33      
34        if (char1Exists !== char2Exists) {
35            return false;
36        }
37    }
38  
39    // Sort frequency arrays to check if frequency distributions match
40    // This validates if we can transform characters to match frequencies
41    frequencyArray1.sort((a: number, b: number) => a - b);
42    frequencyArray2.sort((a: number, b: number) => a - b);
43  
44    // Compare sorted frequency distributions using string comparison
45    return frequencyArray1.join('.') === frequencyArray2.join('.');
46}
47

Time and Space Complexity

The time complexity is O(m + n + C Γ— log C), where:

  • m is the length of word1
  • n is the length of word2
  • C is the number of distinct letter types (at most 26 for lowercase English letters)

Breaking down the time complexity:

  • Creating Counter(word1) takes O(m) time
  • Creating Counter(word2) takes O(n) time
  • sorted(cnt1.values()) takes O(C Γ— log C) time to sort at most C frequency values
  • sorted(cnt2.values()) takes O(C Γ— log C) time to sort at most C frequency values
  • set(cnt1.keys()) takes O(C) time to create a set of at most C keys
  • set(cnt2.keys()) takes O(C) time to create a set of at most C keys
  • Comparing two sorted lists takes O(C) time
  • Comparing two sets takes O(C) time

The dominant operations are counting the characters O(m + n) and sorting the frequency values O(C Γ— log C), giving us a total time complexity of O(m + n + C Γ— log C).

The space complexity is O(C), where C is at most 26, because:

  • cnt1 stores at most C key-value pairs: O(C)
  • cnt2 stores at most C key-value pairs: O(C)
  • sorted(cnt1.values()) creates a list of at most C elements: O(C)
  • sorted(cnt2.values()) creates a list of at most C elements: O(C)
  • The sets created from keys each contain at most C elements: O(C)

Since we only store a constant number of data structures each with at most C elements, the total space complexity is O(C).

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

Common Pitfalls

Pitfall 1: Forgetting to Check Both Conditions

A common mistake is checking only one of the two required conditions. Some might think that having the same frequency distribution is sufficient:

Incorrect approach:

def closeStrings(self, word1: str, word2: str) -> bool:
    cnt1, cnt2 = Counter(word1), Counter(word2)
    # Only checking frequency distribution - WRONG!
    return sorted(cnt1.values()) == sorted(cnt2.values())

This would incorrectly return True for word1 = "abc" and word2 = "def" since both have frequency pattern [1, 1, 1], but they don't share any common characters.

Solution: Always check both conditions - same character set AND same frequency distribution.

Pitfall 2: Misunderstanding Operation 2

Some might think Operation 2 allows introducing new characters or changing the total count of characters:

Incorrect interpretation:

def closeStrings(self, word1: str, word2: str) -> bool:
    # Thinking we can add/remove characters - WRONG!
    return len(word1) == len(word2)

Operation 2 only swaps the roles of existing characters - it doesn't create new ones or change total counts.

Solution: Remember that Operation 2 swaps ALL occurrences of one character with ALL occurrences of another existing character.

Pitfall 3: Not Sorting the Frequency Values

Comparing frequency values without sorting them is a critical error:

Incorrect approach:

def closeStrings(self, word1: str, word2: str) -> bool:
    cnt1, cnt2 = Counter(word1), Counter(word2)
    # Comparing unsorted values - WRONG!
    return list(cnt1.values()) == list(cnt2.values()) and set(cnt1.keys()) == set(cnt2.keys())

This fails for cases like word1 = "aabbcc" (frequencies: a=2, b=2, c=2) and word2 = "bbaacc" (frequencies: b=2, a=2, c=2). The frequency lists [2, 2, 2] and [2, 2, 2] might appear in different orders based on dictionary iteration.

Solution: Always sort the frequency values before comparison since Operation 2 allows swapping frequencies between characters.

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

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

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

Load More