Welcome to Subscribe On Youtube
Formatted question description: https://leetcode.ca/all/2157.html
2157. Groups of Strings (Hard)
You are given a 0-indexed array of strings words
. Each string consists of lowercase English letters only. No letter occurs more than once in any string of words
.
Two strings s1
and s2
are said to be connected if the set of letters of s2
can be obtained from the set of letters of s1
by any one of the following operations:
- Adding exactly one letter to the set of the letters of
s1
. - Deleting exactly one letter from the set of the letters of
s1
. - Replacing exactly one letter from the set of the letters of
s1
with any letter, including itself.
The array words
can be divided into one or more non-intersecting groups. A string belongs to a group if any one of the following is true:
- It is connected to at least one other string of the group.
- It is the only string present in the group.
Note that the strings in words
should be grouped in such a manner that a string belonging to a group cannot be connected to a string present in any other group. It can be proved that such an arrangement is always unique.
Return an array ans
of size 2
where:
ans[0]
is the total number of groupswords
can be divided into, andans[1]
is the size of the largest group.
Example 1:
Input: words = ["a","b","ab","cde"] Output: [2,3] Explanation: - words[0] can be used to obtain words[1] (by replacing 'a' with 'b'), and words[2] (by adding 'b'). So words[0] is connected to words[1] and words[2]. - words[1] can be used to obtain words[0] (by replacing 'b' with 'a'), and words[2] (by adding 'a'). So words[1] is connected to words[0] and words[2]. - words[2] can be used to obtain words[0] (by deleting 'b'), and words[1] (by deleting 'a'). So words[2] is connected to words[0] and words[1]. - words[3] is not connected to any string in words. Thus, words can be divided into 2 groups ["a","b","ab"] and ["cde"]. The size of the largest group is 3.
Example 2:
Input: words = ["a","ab","abc"] Output: [1,3] Explanation: - words[0] is connected to words[1]. - words[1] is connected to words[0] and words[2]. - words[2] is connected to words[1]. Since all strings are connected to each other, they should be grouped together. Thus, the size of the largest group is 3.
Constraints:
1 <= words.length <= 2 * 104
1 <= words[i].length <= 26
words[i]
consists of lowercase English letters only.- No letter occurs more than once in
words[i]
.
Similar Questions:
Solution 1. Union Find + Bitmask
Let m
be a map from a bitmask to the corresponding index in A
.
For each A[i]
:
- generate the correponding hash
h
- generate the addition, deletion and replacement variant of hash
h
. - Assume the variant’s hash is
t
, we connecti
withm[t]
using Union Find.
In the end, Union Find can tell us the number of groups and the size of all groups.
Note that this sometimes gets TLE because of the tight time constraint and the randomness of unordered_map
.
-
// OJ: https://leetcode.com/contest/weekly-contest-278/problems/groups-of-strings/ // Time: O(26 * 26 * NlogN) // Space: O(N) class UnionFind { vector<int> id, size; int cnt; public: UnionFind(int n) : id(n), size(n, 1), cnt(n) { iota(begin(id), end(id), 0); } int find(int a) { return id[a] == a ? a : (id[a] = find(id[a])); } void connect(int a, int b) { int x = find(a), y = find(b); if (x == y) return; id[x] = y; size[y] += size[x]; --cnt; } int getSize(int a) { return size[find(a)]; } int getCount() { return cnt; } }; class Solution { public: vector<int> groupStrings(vector<string>& A) { int N = A.size(); UnionFind uf(N); unordered_map<int, int> m; // map from hash to index m.reserve(N); for (int i = 0; i < N; ++i) { int h = 0; for (char c : A[i]) h |= 1 << (c - 'a'); // `h` is the bitmask representation of `A[i]` for (int j = 0; j < 26; ++j) { if (h >> j & 1) { // if `h`'s j-th bit is 1 int del = h ^ (1 << j); // `del` is the bitmask after deleting the `j`-th bit if (m.count(del)) uf.connect(i, m[del]); // Connect `A[i]` with its deletion variant for (int k = 0; k < 26; ++k) { // we replace `j`-th bit with `k`-th bit int rep = del | (1 << k); // `rep` is the bitmask after replacing `j`-th bit with `k`-th bit. if (rep != del && m.count(rep)) uf.connect(i, m[rep]); } } else { int add = h | (1 << j); // `add` is the bitmask after adding `j`-th bit if (m.count(add)) uf.connect(i, m[add]); } } m[h] = i; } int mx = 1; for (int i = 0; i < N; ++ i) mx = max(mx, uf.getSize(i)); return {uf.getCount(), mx}; } };
-
class Solution: def groupStrings(self, words: List[str]) -> List[int]: def find(x): if p[x] != x: p[x] = find(p[x]) return p[x] def union(a, b): nonlocal mx, n if b not in p: return pa, pb = find(a), find(b) if pa == pb: return p[pa] = pb size[pb] += size[pa] mx = max(mx, size[pb]) n -= 1 p = {} size = Counter() n = len(words) mx = 0 for word in words: x = 0 for c in word: x |= 1 << (ord(c) - ord('a')) p[x] = x size[x] += 1 mx = max(mx, size[x]) if size[x] > 1: n -= 1 for x in p.keys(): for i in range(26): union(x, x ^ (1 << i)) if (x >> i) & 1: for j in range(26): if ((x >> j) & 1) == 0: union(x, x ^ (1 << i) | (1 << j)) return [n, mx] ############ # 2157. Groups of Strings # https://leetcode.com/problems/groups-of-strings class UnionFind: def __init__(self): self._parent = {} self._size = {} def union(self, a, b): a, b = self.find(a), self.find(b) if a == b: return if self._size[a] < self._size[b]: a, b = b, a self._parent[b] = a self._size[a] += self._size[b] def find(self, x): if x not in self._parent: self._parent[x] = x self._size[x] = 1 while self._parent[x] != x: self._parent[x] = self._parent[self._parent[x]] x = self._parent[x] return x class Solution: def groupStrings(self, words: List[str]) -> List[int]: def getMask(word): mask = 0 for char in word: mask |= (1 << (ord(char) - ord('a'))) return mask uf = UnionFind() for word in words: mask = getMask(word) for i in range(26): if mask & (1 << i): uf.union(mask, mask ^ (1 << i)) count = Counter() for word in words: count[uf.find(getMask(word))] += 1 return [len(count), max(count.values())]
-
class Solution { private Map<Integer, Integer> p; private Map<Integer, Integer> size; private int mx; private int n; public int[] groupStrings(String[] words) { p = new HashMap<>(); size = new HashMap<>(); n = words.length; mx = 0; for (String word : words) { int x = 0; for (char c : word.toCharArray()) { x |= 1 << (c - 'a'); } p.put(x, x); size.put(x, size.getOrDefault(x, 0) + 1); mx = Math.max(mx, size.get(x)); if (size.get(x) > 1) { --n; } } for (int x : p.keySet()) { for (int i = 0; i < 26; ++i) { union(x, x ^ (1 << i)); if (((x >> i) & 1) != 0) { for (int j = 0; j < 26; ++j) { if (((x >> j) & 1) == 0) { union(x, x ^ (1 << i) | (1 << j)); } } } } } return new int[] {n, mx}; } private int find(int x) { if (p.get(x) != x) { p.put(x, find(p.get(x))); } return p.get(x); } private void union(int a, int b) { if (!p.containsKey(b)) { return; } int pa = find(a), pb = find(b); if (pa == pb) { return; } p.put(pa, pb); size.put(pb, size.get(pb) + size.get(pa)); mx = Math.max(mx, size.get(pb)); --n; } }
-
func groupStrings(words []string) []int { p := map[int]int{} size := map[int]int{} mx, n := 0, len(words) var find func(int) int find = func(x int) int { if p[x] != x { p[x] = find(p[x]) } return p[x] } union := func(a, b int) { if _, ok := p[b]; !ok { return } pa, pb := find(a), find(b) if pa == pb { return } p[pa] = pb size[pb] += size[pa] mx = max(mx, size[pb]) n-- } for _, word := range words { x := 0 for _, c := range word { x |= 1 << (c - 'a') } p[x] = x size[x]++ mx = max(mx, size[x]) if size[x] > 1 { n-- } } for x := range p { for i := 0; i < 26; i++ { union(x, x^(1<<i)) if ((x >> i) & 1) != 0 { for j := 0; j < 26; j++ { if ((x >> j) & 1) == 0 { union(x, x^(1<<i)|(1<<j)) } } } } } return []int{n, mx} } func max(a, b int) int { if a > b { return a } return b }
Solution 2. Optimization
- Added “union by rank” to the Union Find to reduce the time complexity of
find
fromO(logN)
toO(alpha(N))
wherealpha(N)
is the inverse function of Ackermann function. - For the replacement operation, reduced the time from
O(26 * 26)
toO(26)
by “meet-in-the-middle”. If two strings are connected after replacement operation, then they can be the same string after deleting one character. Example"abc"
and"abd"
are connected because they both become"ab"
after one deletion.
-
// OJ: https://leetcode.com/problems/groups-of-strings/ // Time: O(26 * N * alpha(N)) // Space: O(26 * N) class UnionFind { vector<int> id, rank, size; int cnt; public: UnionFind(int n) : id(n), rank(n, 0), size(n, 1), cnt(n) { iota(begin(id), end(id), 0); } int find(int a) { return id[a] == a ? a : (id[a] = find(id[a])); } void connect(int a, int b) { int x = find(a), y = find(b); if (x == y) return; if (rank[x] > rank[y]) { id[y] = x; size[x] += size[y]; } else { id[x] = y; size[y] += size[x]; if (rank[x] == rank[y]) rank[y]++; } --cnt; } int getSize(int a) { return size[find(a)]; } int getCount() { return cnt; } }; class Solution { public: vector<int> groupStrings(vector<string>& A) { int N = A.size(); UnionFind uf(N); unordered_map<int, int> m, delMap; m.reserve(N); for (int i = 0; i < N; ++i) { int h = 0; for (char c : A[i]) h |= 1 << (c - 'a'); // `h` is the bitmask representation of `A[i]` for (int j = 0; j < 26; ++j) { if (h >> j & 1) { // if `h`'s j-th bit is 1 int del = h ^ (1 << j); // `del` is the bitmask after deleting the `j`-th bit if (m.count(del)) uf.connect(i, m[del]); // Connect `A[i]` with its deletion variant if (delMap.count(del)) uf.connect(i, delMap[del]); else delMap[del] = i; } else { int add = h | (1 << j); // `add` is the bitmask after adding `j`-th bit if (m.count(add)) uf.connect(i, m[add]); } } m[h] = i; } int mx = 1; for (int i = 0; i < N; ++ i) mx = max(mx, uf.getSize(i)); return {uf.getCount(), mx}; } };
-
class Solution: def groupStrings(self, words: List[str]) -> List[int]: def find(x): if p[x] != x: p[x] = find(p[x]) return p[x] def union(a, b): nonlocal mx, n if b not in p: return pa, pb = find(a), find(b) if pa == pb: return p[pa] = pb size[pb] += size[pa] mx = max(mx, size[pb]) n -= 1 p = {} size = Counter() n = len(words) mx = 0 for word in words: x = 0 for c in word: x |= 1 << (ord(c) - ord('a')) p[x] = x size[x] += 1 mx = max(mx, size[x]) if size[x] > 1: n -= 1 for x in p.keys(): for i in range(26): union(x, x ^ (1 << i)) if (x >> i) & 1: for j in range(26): if ((x >> j) & 1) == 0: union(x, x ^ (1 << i) | (1 << j)) return [n, mx] ############ # 2157. Groups of Strings # https://leetcode.com/problems/groups-of-strings class UnionFind: def __init__(self): self._parent = {} self._size = {} def union(self, a, b): a, b = self.find(a), self.find(b) if a == b: return if self._size[a] < self._size[b]: a, b = b, a self._parent[b] = a self._size[a] += self._size[b] def find(self, x): if x not in self._parent: self._parent[x] = x self._size[x] = 1 while self._parent[x] != x: self._parent[x] = self._parent[self._parent[x]] x = self._parent[x] return x class Solution: def groupStrings(self, words: List[str]) -> List[int]: def getMask(word): mask = 0 for char in word: mask |= (1 << (ord(char) - ord('a'))) return mask uf = UnionFind() for word in words: mask = getMask(word) for i in range(26): if mask & (1 << i): uf.union(mask, mask ^ (1 << i)) count = Counter() for word in words: count[uf.find(getMask(word))] += 1 return [len(count), max(count.values())]
-
class Solution { private Map<Integer, Integer> p; private Map<Integer, Integer> size; private int mx; private int n; public int[] groupStrings(String[] words) { p = new HashMap<>(); size = new HashMap<>(); n = words.length; mx = 0; for (String word : words) { int x = 0; for (char c : word.toCharArray()) { x |= 1 << (c - 'a'); } p.put(x, x); size.put(x, size.getOrDefault(x, 0) + 1); mx = Math.max(mx, size.get(x)); if (size.get(x) > 1) { --n; } } for (int x : p.keySet()) { for (int i = 0; i < 26; ++i) { union(x, x ^ (1 << i)); if (((x >> i) & 1) != 0) { for (int j = 0; j < 26; ++j) { if (((x >> j) & 1) == 0) { union(x, x ^ (1 << i) | (1 << j)); } } } } } return new int[] {n, mx}; } private int find(int x) { if (p.get(x) != x) { p.put(x, find(p.get(x))); } return p.get(x); } private void union(int a, int b) { if (!p.containsKey(b)) { return; } int pa = find(a), pb = find(b); if (pa == pb) { return; } p.put(pa, pb); size.put(pb, size.get(pb) + size.get(pa)); mx = Math.max(mx, size.get(pb)); --n; } }
-
func groupStrings(words []string) []int { p := map[int]int{} size := map[int]int{} mx, n := 0, len(words) var find func(int) int find = func(x int) int { if p[x] != x { p[x] = find(p[x]) } return p[x] } union := func(a, b int) { if _, ok := p[b]; !ok { return } pa, pb := find(a), find(b) if pa == pb { return } p[pa] = pb size[pb] += size[pa] mx = max(mx, size[pb]) n-- } for _, word := range words { x := 0 for _, c := range word { x |= 1 << (c - 'a') } p[x] = x size[x]++ mx = max(mx, size[x]) if size[x] > 1 { n-- } } for x := range p { for i := 0; i < 26; i++ { union(x, x^(1<<i)) if ((x >> i) & 1) != 0 { for j := 0; j < 26; j++ { if ((x >> j) & 1) == 0 { union(x, x^(1<<i)|(1<<j)) } } } } } return []int{n, mx} } func max(a, b int) int { if a > b { return a } return b }
Discuss
https://leetcode.com/problems/all-divisions-with-the-highest-score-of-a-binary-array/discuss/1730117