Groups of Special-Equivalent Strings
MedPrompt
You are given an array of strings of the same length words.
In one move, you can swap any two even indexed characters or any two odd indexed characters of a string words[i].
Two strings words[i] and words[j] are special-equivalent if after any number of moves, words[i] == words[j].
- For example,
words[i] = "zzxy"andwords[j] = "xyzz"are special-equivalent because we may make the moves"zzxy" -> "xzzy" -> "xyzz".
A group of special-equivalent strings from words is a non-empty subset of words such that:
- Every pair of strings in the group are special equivalent, and
- The group is the largest size possible (i.e., there is not a string
words[i]not in the group such thatwords[i]is special-equivalent to every string in the group).
Return the number of groups of special-equivalent strings from words.
Example 1:
Input: words = ["abcd","cdab","cbad","xyzz","zzxy","zzyx"]
Output: 3
Explanation:
One group is ["abcd", "cdab", "cbad"], since they are all pairwise special equivalent, and none of the other strings is all pairwise special equivalent to these.
The other two groups are ["xyzz", "zzxy"] and ["zzyx"].
Note that in particular, "zzxy" is not special equivalent to "zzyx".Example 2:
Input: words = ["abc","acb","bac","bca","cab","cba"]
Output: 3
Constraints:
1 <= words.length <= 10001 <= words[i].length <= 20words[i]consist of lowercase English letters.- All the strings are of the same length.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach identifies special-equivalent strings by creating a canonical form for each string. Two strings are special-equivalent if and only if the multiset of their even-indexed characters are the same, and the multiset of their odd-indexed characters are the same. We can represent these multisets canonically by sorting them. For each word, we separate its characters into two lists based on their index (even or odd), sort both lists, and then concatenate them. This new string serves as a unique identifier for its equivalence group. We use a HashSet to count the number of unique canonical forms, which corresponds to the number of groups.
Algorithm
- Initialize a
HashSet<String>to store the unique canonical representations. - Iterate through each
wordin the input arraywords. - For each
word, create two character arrays: one for characters at even indices and one for characters at odd indices. - Populate these two arrays by iterating through the
word. - Sort both character arrays alphabetically.
- Create a new string by concatenating the sorted even-indexed characters and the sorted odd-indexed characters.
- Add this canonical string to the
HashSet. - After processing all words, the size of the
HashSetis the number of special-equivalent groups.
Walkthrough
The core idea is that allowing swaps of any two even-indexed characters (or odd-indexed characters) means we can achieve any permutation of those characters. Therefore, two strings are special-equivalent if they have the same characters at even positions and the same characters at odd positions, irrespective of their order. To check this, we follow the algorithm to create a canonical representation for each string and count the unique ones.
import java.util.Arrays;import java.util.HashSet;import java.util.Set; class Solution { public int numSpecialEquivGroups(String[] words) { Set<String> seen = new HashSet<>(); for (String word : words) { int n = word.length(); char[] evenChars = new char[(n + 1) / 2]; char[] oddChars = new char[n / 2]; for (int i = 0; i < n; i++) { if (i % 2 == 0) { evenChars[i / 2] = word.charAt(i); } else { oddChars[i / 2] = word.charAt(i); } } Arrays.sort(evenChars); Arrays.sort(oddChars); String canonical = new String(evenChars) + new String(oddChars); seen.add(canonical); } return seen.size(); }}Complexity
Time
O(N * L log L), where N is the number of words and L is the length of each word. For each of the N words, we iterate through its L characters (O(L)), sort two arrays of size approximately L/2 (2 * O(L/2 log(L/2)) which simplifies to O(L log L)), and create a new string (O(L)). The dominant step is sorting, leading to O(L log L) per word.
Space
O(N * L). The `HashSet` can store up to N unique canonical strings, each of length L. In the worst case (all words are in different groups), this requires O(N * L) space. Additionally, O(L) temporary space is used for the character arrays for each word.
Trade-offs
Pros
Conceptually straightforward and easy to implement.
Directly models the problem definition where order doesn't matter within even/odd positions.
Cons
Less efficient than a counting-based approach, especially as the length of the strings (L) increases, due to the O(L log L) sorting step.
This approach improves upon the sorting method by using frequency counting, which is more efficient for a fixed alphabet. Instead of sorting the even and odd indexed characters, we count the occurrences of each character ('a' through 'z') for both sets. The canonical representation is then formed by serializing these two frequency counts into a single string. For example, we can concatenate the 26 counts for even-indexed characters followed by the 26 counts for odd-indexed characters. This unique string key represents the equivalence group. We use a HashSet to count the number of unique keys.
Algorithm
- Initialize a
HashSet<String>to store the unique canonical keys. - Iterate through each
wordin the input arraywords. - For each
word, create two integer arrays of size 26,evenCountsandoddCounts, initialized to zero. - Iterate through the
wordwith indexiand characterc:- If
iis even, incrementevenCounts[c - 'a']. - If
iis odd, incrementoddCounts[c - 'a'].
- If
- Serialize the two count arrays into a single, unique string key. A simple way is to convert the arrays to strings and concatenate them.
- Add this key to the
HashSet. - After processing all words, the size of the
HashSetis the answer.
Walkthrough
Since the characters are limited to lowercase English letters, we can use an array of size 26 as a frequency map instead of sorting. This avoids the O(L log L) complexity of sorting. The canonical representation is built from the character counts, which uniquely define the multiset of characters for both even and odd positions. We then count the number of unique representations to find the number of groups.
import java.util.Arrays;import java.util.HashSet;import java.util.Set; class Solution { public int numSpecialEquivGroups(String[] words) { Set<String> seen = new HashSet<>(); for (String word : words) { int[] evenCounts = new int[26]; int[] oddCounts = new int[26]; for (int i = 0; i < word.length(); i++) { if (i % 2 == 0) { evenCounts[word.charAt(i) - 'a']++; } else { oddCounts[word.charAt(i) - 'a']++; } } // Serialize the count arrays into a canonical string key. // Arrays.toString() provides a consistent string representation. String key = Arrays.toString(evenCounts) + Arrays.toString(oddCounts); seen.add(key); } return seen.size(); }}Complexity
Time
O(N * L), where N is the number of words and L is the length of each word. For each of the N words, we iterate through its L characters to populate the count arrays (O(L)). Creating the key from the count arrays takes constant time, as the alphabet size is fixed at 26 (O(1)). Thus, the total time is dominated by iterating through the words.
Space
O(N * C), where N is the number of words and C is the length of the canonical key. The key length C is constant (related to the alphabet size, 26). In the worst case, the `HashSet` stores N distinct keys, leading to O(N) space. The temporary space for the count arrays for each word is O(1) (2 * 26 integers).
Trade-offs
Pros
More efficient than the sorting approach, with a linear time complexity.
Scales better with the length of the strings (L).
Cons
The construction of the canonical key might seem slightly less direct than sorting, but is a standard technique for anagram-style problems.
Solutions
Solution
class Solution { public int numSpecialEquivGroups ( String [] words ) { Set < String > s = new HashSet <>(); for ( String word : words ) { s . add ( convert ( word )); } return s . size (); } private String convert ( String word ) { List < Character > a = new ArrayList <>(); List < Character > b = new ArrayList <>(); for ( int i = 0 ; i < word . length (); ++ i ) { char ch = word . charAt ( i ); if ( i % 2 == 0 ) { a . add ( ch ); } else { b . add ( ch ); } } Collections . sort ( a ); Collections . sort ( b ); StringBuilder sb = new StringBuilder (); for ( char c : a ) { sb . append ( c ); } for ( char c : b ) { sb . append ( c ); } return sb . toString (); } }class Solution { public: int numSpecialEquivGroups ( vector < string >& words ) { unordered_set < string > s ; for ( auto & word : words ) { string a = "" , b = "" ; for ( int i = 0 ; i < word . size (); ++ i ) { if ( i & 1 ) a += word [ i ]; else b += word [ i ]; } sort ( a . begin (), a . end ()); sort ( b . begin (), b . end ()); s . insert ( a + b ); } return s . size (); } };class Solution : def numSpecialEquivGroups ( self , words : List [ str ]) -> int : s = { '' . join ( sorted ( word [:: 2 ]) + sorted ( word [ 1 :: 2 ])) for word in words } return len ( s )Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.