893. Groups of Special-Equivalent Strings
Problem Description
You have an array of strings where all strings have the same length. You can perform moves on these strings with a specific rule: in one move, you can swap any two characters at even positions (0, 2, 4, ...) with each other, OR swap any two characters at odd positions (1, 3, 5, ...) with each other.
Two strings are considered special-equivalent if you can transform one into the other by performing any number of these allowed moves. For example, "zzxy"
and "xyzz"
are special-equivalent because:
- Start with
"zzxy"
- Swap characters at positions 0 and 2 (both even):
"zzxy"
โ"xzzy"
- Swap characters at positions 1 and 3 (both odd):
"xzzy"
โ"xyzz"
The problem asks you to group all the strings into groups of special-equivalent strings. Each group must satisfy:
- Every pair of strings within the group are special-equivalent to each other
- The group is maximal (you cannot add any other string from the array that would be special-equivalent to all strings in the group)
Your task is to return the total number of such groups.
The key insight is that two strings are special-equivalent if and only if:
- The characters at even positions in both strings can be rearranged to match each other
- The characters at odd positions in both strings can be rearranged to match each other
This means strings are special-equivalent when they have the same set of characters at even positions (regardless of order) and the same set of characters at odd positions (regardless of order).
Intuition
Since we can swap any two characters at even positions with each other, and any two characters at odd positions with each other, we can rearrange the even-positioned characters in any order we want, and similarly for odd-positioned characters. However, we cannot move a character from an even position to an odd position or vice versa.
This means two strings are special-equivalent if and only if:
- They have the exact same collection of characters at even positions (just possibly in different order)
- They have the exact same collection of characters at odd positions (just possibly in different order)
For example, consider "abcd"
and "cdab"
:
- Even positions (0, 2):
"abcd"
has['a', 'c']
and"cdab"
has['c', 'a']
- same characters! - Odd positions (1, 3):
"abcd"
has['b', 'd']
and"cdab"
has['d', 'b']
- same characters!
So these strings are special-equivalent.
To identify which strings belong to the same group, we need a way to create a unique "signature" for each group. Since the order doesn't matter within even positions and odd positions separately, we can:
- Extract all characters at even positions and sort them
- Extract all characters at odd positions and sort them
- Concatenate these two sorted strings to create a canonical form
All strings that are special-equivalent will produce the same canonical form. For instance:
"abcd"
โ even:"ac"
โ sorted:"ac"
, odd:"bd"
โ sorted:"bd"
, canonical:"acbd"
"cdab"
โ even:"ca"
โ sorted:"ac"
, odd:"db"
โ sorted:"bd"
, canonical:"acbd"
By creating these canonical forms for all strings and counting the unique ones, we get the number of special-equivalent groups. We can use a set to automatically handle the uniqueness, making the solution elegant and efficient.
Learn more about Sorting patterns.
Solution Approach
The solution uses a set comprehension to elegantly solve this problem in a single line. Let's break down how it works:
-
Iterate through each word: For each
word
in thewords
array, we need to create its canonical form. -
Extract and sort characters at even positions: Using Python's slice notation
word[::2]
, we extract all characters at even indices (0, 2, 4, ...). Then we sort these characters usingsorted(word[::2])
. -
Extract and sort characters at odd positions: Similarly,
word[1::2]
extracts all characters at odd indices (1, 3, 5, ...), and we sort them withsorted(word[1::2])
. -
Create the canonical form: We concatenate the two sorted lists using
sorted(word[::2]) + sorted(word[1::2])
, then join them into a single string with''.join()
. This creates a unique signature for each special-equivalent group. -
Use a set to count unique groups: By placing all canonical forms in a set
s
, duplicate canonical forms are automatically eliminated. Each unique canonical form represents one group of special-equivalent strings. -
Return the count: The number of unique canonical forms in the set equals the number of special-equivalent groups, so we return
len(s)
.
Example walkthrough:
If words = ["abcd", "cdab", "cbad", "xyzz", "zzxy", "zzyx"]
:
"abcd"
โ even:"ac"
, odd:"bd"
โ canonical:"acbd"
"cdab"
โ even:"ca"
โ"ac"
, odd:"db"
โ"bd"
โ canonical:"acbd"
"cbad"
โ even:"ca"
โ"ac"
, odd:"bd"
โ canonical:"acbd"
"xyzz"
โ even:"xz"
, odd:"yz"
โ canonical:"xzyz"
"zzxy"
โ even:"zx"
โ"xz"
, odd:"zy"
โ"yz"
โ canonical:"xzyz"
"zzyx"
โ even:"zy"
โ"yz"
, odd:"zx"
โ"xz"
โ canonical:"yzxz"
The set would contain: {"acbd", "xzyz", "yzxz"}
, giving us 3 groups.
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 a small example with words = ["abc", "bca", "bac", "cba"]
.
For each string, we'll extract characters at even and odd positions, sort them separately, then combine them to create a canonical form:
String: "abc"
- Even positions (indices 0, 2): 'a' and 'c' โ sorted: "ac"
- Odd positions (index 1): 'b' โ sorted: "b"
- Canonical form: "ac" + "b" = "acb"
String: "bca"
- Even positions (indices 0, 2): 'b' and 'a' โ sorted: "ab"
- Odd positions (index 1): 'c' โ sorted: "c"
- Canonical form: "ab" + "c" = "abc"
String: "bac"
- Even positions (indices 0, 2): 'b' and 'c' โ sorted: "bc"
- Odd positions (index 1): 'a' โ sorted: "a"
- Canonical form: "bc" + "a" = "bca"
String: "cba"
- Even positions (indices 0, 2): 'c' and 'a' โ sorted: "ac"
- Odd positions (index 1): 'b' โ sorted: "b"
- Canonical form: "ac" + "b" = "acb"
Now we collect all unique canonical forms:
- "abc" โ "acb"
- "bca" โ "abc"
- "bac" โ "bca"
- "cba" โ "acb" (duplicate of first)
Unique canonical forms: {"acb", "abc", "bca"}
Therefore, we have 3 groups of special-equivalent strings:
- Group 1: ["abc", "cba"] (both have canonical form "acb")
- Group 2: ["bca"] (canonical form "abc")
- Group 3: ["bac"] (canonical form "bca")
The strings "abc" and "cba" are in the same group because we can transform "abc" to "cba" by swapping the characters at even positions 0 and 2.
Solution Implementation
1class Solution:
2 def numSpecialEquivGroups(self, words: List[str]) -> int:
3 # Use a set to store unique special-equivalent group signatures
4 unique_groups = {
5 # For each word, create a signature by:
6 # 1. Sorting characters at even indices (word[::2])
7 # 2. Sorting characters at odd indices (word[1::2])
8 # 3. Concatenating the two sorted strings
9 # This signature will be identical for special-equivalent strings
10 ''.join(sorted(word[::2]) + sorted(word[1::2]))
11 for word in words
12 }
13
14 # Return the count of unique special-equivalent groups
15 return len(unique_groups)
16
1class Solution {
2 /**
3 * Counts the number of groups of special-equivalent strings.
4 * Two strings are special-equivalent if we can swap any two characters at even indices,
5 * or swap any two characters at odd indices, to make them equal.
6 *
7 * @param words Array of strings to group by special-equivalence
8 * @return Number of distinct special-equivalent groups
9 */
10 public int numSpecialEquivGroups(String[] words) {
11 // Use a HashSet to store unique normalized representations of strings
12 Set<String> uniqueGroups = new HashSet<>();
13
14 // Process each word and add its normalized form to the set
15 for (String word : words) {
16 uniqueGroups.add(convert(word));
17 }
18
19 // The size of the set represents the number of distinct groups
20 return uniqueGroups.size();
21 }
22
23 /**
24 * Converts a string to its normalized form for special-equivalence comparison.
25 * The normalized form is created by sorting characters at even positions and odd positions separately,
26 * then concatenating them. This ensures that special-equivalent strings have the same normalized form.
27 *
28 * @param word The input string to normalize
29 * @return The normalized string representation
30 */
31 private String convert(String word) {
32 // List to store characters at even indices (0, 2, 4, ...)
33 List<Character> evenChars = new ArrayList<>();
34 // List to store characters at odd indices (1, 3, 5, ...)
35 List<Character> oddChars = new ArrayList<>();
36
37 // Separate characters based on their position (even or odd index)
38 for (int i = 0; i < word.length(); i++) {
39 char currentChar = word.charAt(i);
40 if (i % 2 == 0) {
41 evenChars.add(currentChar);
42 } else {
43 oddChars.add(currentChar);
44 }
45 }
46
47 // Sort both lists to create a canonical representation
48 Collections.sort(evenChars);
49 Collections.sort(oddChars);
50
51 // Build the normalized string by concatenating sorted even and odd characters
52 StringBuilder normalizedString = new StringBuilder();
53
54 // Append all sorted even-position characters
55 for (char c : evenChars) {
56 normalizedString.append(c);
57 }
58
59 // Append all sorted odd-position characters
60 for (char c : oddChars) {
61 normalizedString.append(c);
62 }
63
64 return normalizedString.toString();
65 }
66}
67
1class Solution {
2public:
3 int numSpecialEquivGroups(vector<string>& words) {
4 // Use a set to store unique representations of special-equivalent groups
5 unordered_set<string> uniqueGroups;
6
7 // Process each word in the input array
8 for (auto& word : words) {
9 // Separate characters at odd and even indices
10 string oddChars = "";
11 string evenChars = "";
12
13 // Iterate through each character in the word
14 for (int i = 0; i < word.size(); ++i) {
15 if (i & 1) {
16 // Character at odd index (1, 3, 5, ...)
17 oddChars += word[i];
18 } else {
19 // Character at even index (0, 2, 4, ...)
20 evenChars += word[i];
21 }
22 }
23
24 // Sort characters at odd and even positions separately
25 // This creates a canonical form for special-equivalent strings
26 sort(oddChars.begin(), oddChars.end());
27 sort(evenChars.begin(), evenChars.end());
28
29 // Concatenate sorted odd and even characters to form a unique key
30 // Words that are special-equivalent will have the same key
31 uniqueGroups.insert(evenChars + oddChars);
32 }
33
34 // Return the number of unique special-equivalent groups
35 return uniqueGroups.size();
36 }
37};
38
1function numSpecialEquivGroups(words: string[]): number {
2 // Use a Set to store unique representations of special-equivalent groups
3 const uniqueGroups: Set<string> = new Set();
4
5 // Process each word in the input array
6 for (const word of words) {
7 // Separate characters at odd and even indices
8 let oddChars: string = "";
9 let evenChars: string = "";
10
11 // Iterate through each character in the word
12 for (let i = 0; i < word.length; i++) {
13 if (i & 1) {
14 // Character at odd index (1, 3, 5, ...)
15 oddChars += word[i];
16 } else {
17 // Character at even index (0, 2, 4, ...)
18 evenChars += word[i];
19 }
20 }
21
22 // Sort characters at odd and even positions separately
23 // This creates a canonical form for special-equivalent strings
24 const sortedOddChars: string = oddChars.split('').sort().join('');
25 const sortedEvenChars: string = evenChars.split('').sort().join('');
26
27 // Concatenate sorted even and odd characters to form a unique key
28 // Words that are special-equivalent will have the same key
29 uniqueGroups.add(sortedEvenChars + sortedOddChars);
30 }
31
32 // Return the number of unique special-equivalent groups
33 return uniqueGroups.size;
34}
35
Time and Space Complexity
Time Complexity: O(n * m * log(m))
where n
is the number of words and m
is the average length of each word.
- Iterating through all words:
O(n)
- For each word:
- Slicing even indices
word[::2]
:O(m)
- Slicing odd indices
word[1::2]
:O(m)
- Sorting even characters:
O(m/2 * log(m/2))
โO(m * log(m))
- Sorting odd characters:
O(m/2 * log(m/2))
โO(m * log(m))
- Joining the sorted strings:
O(m)
- Adding to set:
O(m)
for string hashing
- Slicing even indices
- Overall:
O(n * m * log(m))
Space Complexity: O(n * m)
where n
is the number of words and m
is the average length of each word.
- Set storage: In worst case, all words produce unique signatures, storing up to
n
strings, each of lengthm
:O(n * m)
- Temporary space for slicing and sorting:
O(m)
for each word operation - Overall:
O(n * m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Concatenation of Sorted Characters
The Problem: A common mistake is concatenating the sorted strings incorrectly. Some developers might write:
''.join(sorted(word[::2]) + sorted(word[1::2]))
This looks correct, but remember that sorted()
returns a list of characters, not a string. When you use +
to concatenate two lists, you get a single list containing all characters. Then ''.join()
converts this combined list into a string.
However, developers might mistakenly try:
sorted(word[::2]) + sorted(word[1::2]) # Without join
# or
''.join(sorted(word[::2])) + ''.join(sorted(word[1::2])) # Double join
The first attempt would store a list in the set (causing a TypeError since lists are unhashable), while the second would work but is unnecessarily verbose.
The Solution:
Stick with the original approach: ''.join(sorted(word[::2]) + sorted(word[1::2]))
. This correctly:
- Creates two sorted lists of characters
- Concatenates the lists with
+
- Joins all characters into a single string
Pitfall 2: Forgetting to Sort the Characters
The Problem: Some might try to create the signature without sorting:
''.join(word[::2] + word[1::2])
This would simply reconstruct a rearranged version of the original string but wouldn't create a canonical form. For example, "abcd" would become "acbd" and "cdab" would become "cadb" - these are different, even though the strings are special-equivalent.
The Solution: Always sort the characters at even and odd positions separately. The sorting ensures that all special-equivalent strings map to the same canonical form.
Pitfall 3: Mixing Up Even and Odd Indices
The Problem: Python's slicing syntax can be confusing. Some might accidentally use:
word[0::2]
instead ofword[::2]
(though these are equivalent)word[2::2]
thinking it starts at position 2 (it actually starts at index 2, missing indices 0)word[::1]
andword[1::1]
thinking these separate even/odd positions
The Solution:
Remember the slice notation [start:stop:step]
:
word[::2]
means start at index 0, go to the end, step by 2 (gets indices 0, 2, 4, ...)word[1::2]
means start at index 1, go to the end, step by 2 (gets indices 1, 3, 5, ...)
Pitfall 4: Using a List Instead of a Set
The Problem: Using a list to collect canonical forms and then counting unique elements:
groups = [''.join(sorted(word[::2]) + sorted(word[1::2])) for word in words]
return len(set(groups)) # Extra conversion step
While this works, it's less efficient as it stores duplicates before converting to a set.
The Solution:
Use a set comprehension directly with {}
instead of a list comprehension with []
. This automatically handles duplicates during creation, making the solution more efficient.
Which technique can we use to find the middle of a linked list?
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!