1657. Determine if Two Strings Are Close
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 alla
's with allb
'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:
- Both strings contain exactly the same set of unique characters
- 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)
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:
-
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}
andword2
has{a, b, d}
, they can never be close. -
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, thenword2
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:
- Both strings must have the same set of unique characters
- 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 EvaluatorExample Walkthrough
Let's walk through the solution with word1 = "cabbba"
and word2 = "abbccc"
.
Step 1: Count character frequencies
-
For
word1 = "cabbba"
:c
appears 1 timea
appears 2 timesb
appears 3 times- So
cnt1 = {c: 1, a: 2, b: 3}
-
For
word2 = "abbccc"
:a
appears 1 timeb
appears 2 timesc
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:
- Transform all
a
's toc
's and allc
's toa
's inword1
:"cabbba"
becomes"aabbbc"
(now a:1, b:3, c:2)
- Transform all
b
's toc
's and allc
's tob
's:"aabbbc"
becomes"aacccc"
(now a:1, b:2, c: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 ofword1
n
is the length ofword2
C
is the number of distinct letter types (at most 26 for lowercase English letters)
Breaking down the time complexity:
- Creating
Counter(word1)
takesO(m)
time - Creating
Counter(word2)
takesO(n)
time sorted(cnt1.values())
takesO(C Γ log C)
time to sort at mostC
frequency valuessorted(cnt2.values())
takesO(C Γ log C)
time to sort at mostC
frequency valuesset(cnt1.keys())
takesO(C)
time to create a set of at mostC
keysset(cnt2.keys())
takesO(C)
time to create a set of at mostC
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 mostC
key-value pairs:O(C)
cnt2
stores at mostC
key-value pairs:O(C)
sorted(cnt1.values())
creates a list of at mostC
elements:O(C)
sorted(cnt2.values())
creates a list of at mostC
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.
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
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
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!