Problem

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 themaximum number of groups words can be divided into, and
  • ans[1] is thesize of the largest group.

Examples

Example 1

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
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 * 10^4
  • 1 <= words[i].length <= 26
  • words[i] consists of lowercase English letters only.
  • No letter occurs more than once in words[i].

Solution

Method 1 – Bitmasking and Union-Find

Intuition

Each string can be represented as a bitmask of its letters. Two strings are connected if their bitmasks differ by adding, removing, or replacing one bit. We can use a hash map to map bitmasks to indices and union-find to group connected strings.

Approach

  1. Convert each word to a bitmask representing its set of letters.
  2. Use a hash map to map each bitmask to the list of indices with that bitmask.
  3. Initialize a union-find (DSU) structure for all indices.
  4. For each word:
    • Try toggling each bit (add/remove one letter) and union with any word having that bitmask.
    • Try toggling each bit off and another bit on (replace one letter) and union with any word having that bitmask.
  5. Count the number of unique groups and the size of the largest group.

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
public:
    vector<int> groupStrings(vector<string>& words) {
        int n = words.size();
        unordered_map<int, vector<int>> mask_to_indices;
        vector<int> p(n);
        iota(p.begin(), p.end(), 0);
        function<int(int)> find = [&](int x) { return p[x] == x ? x : p[x] = find(p[x]); };
        vector<int> masks(n);
        for (int i = 0; i < n; ++i) {
            int mask = 0;
            for (char c : words[i]) mask |= 1 << (c - 'a');
            masks[i] = mask;
            mask_to_indices[mask].push_back(i);
        }
        for (int i = 0; i < n; ++i) {
            int mask = masks[i];
            // Add/remove one letter
            for (int b = 0; b < 26; ++b) {
                int m2 = mask ^ (1 << b);
                if (mask_to_indices.count(m2)) {
                    for (int j : mask_to_indices[m2]) p[find(i)] = find(j);
                }
            }
            // Replace one letter
            for (int b1 = 0; b1 < 26; ++b1) {
                if (!(mask & (1 << b1))) continue;
                for (int b2 = 0; b2 < 26; ++b2) {
                    if (b1 == b2 || (mask & (1 << b2))) continue;
                    int m2 = (mask ^ (1 << b1)) | (1 << b2);
                    if (mask_to_indices.count(m2)) {
                        for (int j : mask_to_indices[m2]) p[find(i)] = find(j);
                    }
                }
            }
        }
        unordered_map<int, int> group_size;
        for (int i = 0; i < n; ++i) group_size[find(i)]++;
        int max_size = 0;
        for (auto& [_, sz] : group_size) max_size = max(max_size, sz);
        return {(int)group_size.size(), max_size};
    }
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
func groupStrings(words []string) []int {
    n := len(words)
    masks := make([]int, n)
    maskToIdx := map[int][]int{}
    p := make([]int, n)
    for i := range p { p[i] = i }
    var find func(int) int
    find = func(x int) int {
        if p[x] != x { p[x] = find(p[x]) }
        return p[x]
    }
    for i, w := range words {
        mask := 0
        for _, c := range w {
            mask |= 1 << (c - 'a')
        }
        masks[i] = mask
        maskToIdx[mask] = append(maskToIdx[mask], i)
    }
    for i, mask := range masks {
        // Add/remove one letter
        for b := 0; b < 26; b++ {
            m2 := mask ^ (1 << b)
            if idxs, ok := maskToIdx[m2]; ok {
                for _, j := range idxs {
                    p[find(i)] = find(j)
                }
            }
        }
        // Replace one letter
        for b1 := 0; b1 < 26; b1++ {
            if mask&(1<<b1) == 0 { continue }
            for b2 := 0; b2 < 26; b2++ {
                if b1 == b2 || mask&(1<<b2) != 0 { continue }
                m2 := (mask ^ (1 << b1)) | (1 << b2)
                if idxs, ok := maskToIdx[m2]; ok {
                    for _, j := range idxs {
                        p[find(i)] = find(j)
                    }
                }
            }
        }
    }
    groupSize := map[int]int{}
    for i := 0; i < n; i++ {
        groupSize[find(i)]++
    }
    maxSize := 0
    for _, sz := range groupSize {
        if sz > maxSize { maxSize = sz }
    }
    return []int{len(groupSize), maxSize}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Solution {
    public int[] groupStrings(String[] words) {
        int n = words.length;
        Map<Integer, List<Integer>> maskToIdx = new HashMap<>();
        int[] p = new int[n];
        for (int i = 0; i < n; i++) p[i] = i;
        int[] masks = new int[n];
        for (int i = 0; i < n; i++) {
            int mask = 0;
            for (char c : words[i].toCharArray()) mask |= 1 << (c - 'a');
            masks[i] = mask;
            maskToIdx.computeIfAbsent(mask, k -> new ArrayList<>()).add(i);
        }
        java.util.function.IntUnaryOperator find = new java.util.function.IntUnaryOperator() {
            public int applyAsInt(int x) { return p[x] == x ? x : (p[x] = applyAsInt(p[x])); }
        };
        for (int i = 0; i < n; i++) {
            int mask = masks[i];
            for (int b = 0; b < 26; b++) {
                int m2 = mask ^ (1 << b);
                if (maskToIdx.containsKey(m2)) {
                    for (int j : maskToIdx.get(m2)) p[find.applyAsInt(i)] = find.applyAsInt(j);
                }
            }
            for (int b1 = 0; b1 < 26; b1++) {
                if ((mask & (1 << b1)) == 0) continue;
                for (int b2 = 0; b2 < 26; b2++) {
                    if (b1 == b2 || (mask & (1 << b2)) != 0) continue;
                    int m2 = (mask ^ (1 << b1)) | (1 << b2);
                    if (maskToIdx.containsKey(m2)) {
                        for (int j : maskToIdx.get(m2)) p[find.applyAsInt(i)] = find.applyAsInt(j);
                    }
                }
            }
        }
        Map<Integer, Integer> groupSize = new HashMap<>();
        for (int i = 0; i < n; i++) groupSize.put(find.applyAsInt(i), groupSize.getOrDefault(find.applyAsInt(i), 0) + 1);
        int maxSize = 0;
        for (int sz : groupSize.values()) maxSize = Math.max(maxSize, sz);
        return new int[]{groupSize.size(), maxSize};
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
    fun groupStrings(words: Array<String>): IntArray {
        val n = words.size
        val maskToIdx = mutableMapOf<Int, MutableList<Int>>()
        val p = IntArray(n) { it }
        val masks = IntArray(n)
        fun find(x: Int): Int = if (p[x] == x) x else { p[x] = find(p[x]); p[x] }
        for (i in 0 until n) {
            var mask = 0
            for (c in words[i]) mask = mask or (1 shl (c - 'a'))
            masks[i] = mask
            maskToIdx.getOrPut(mask) { mutableListOf() }.add(i)
        }
        for (i in 0 until n) {
            val mask = masks[i]
            for (b in 0 until 26) {
                val m2 = mask xor (1 shl b)
                maskToIdx[m2]?.forEach { j -> p[find(i)] = find(j) }
            }
            for (b1 in 0 until 26) {
                if (mask and (1 shl b1) == 0) continue
                for (b2 in 0 until 26) {
                    if (b1 == b2 || mask and (1 shl b2) != 0) continue
                    val m2 = (mask xor (1 shl b1)) or (1 shl b2)
                    maskToIdx[m2]?.forEach { j -> p[find(i)] = find(j) }
                }
            }
        }
        val groupSize = mutableMapOf<Int, Int>()
        for (i in 0 until n) groupSize[find(i)] = groupSize.getOrDefault(find(i), 0) + 1
        val maxSize = groupSize.values.maxOrNull() ?: 0
        return intArrayOf(groupSize.size, maxSize)
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution:
    def groupStrings(self, words: list[str]) -> list[int]:
        n = len(words)
        masks = []
        mask_to_idx = dict()
        for i, w in enumerate(words):
            mask = 0
            for c in w:
                mask |= 1 << (ord(c) - ord('a'))
            masks.append(mask)
            mask_to_idx.setdefault(mask, []).append(i)
        p = list(range(n))
        def find(x):
            if p[x] != x:
                p[x] = find(p[x])
            return p[x]
        for i, mask in enumerate(masks):
            # Add/remove one letter
            for b in range(26):
                m2 = mask ^ (1 << b)
                if m2 in mask_to_idx:
                    for j in mask_to_idx[m2]:
                        p[find(i)] = find(j)
            # Replace one letter
            for b1 in range(26):
                if not (mask & (1 << b1)):
                    continue
                for b2 in range(26):
                    if b1 == b2 or (mask & (1 << b2)):
                        continue
                    m2 = (mask ^ (1 << b1)) | (1 << b2)
                    if m2 in mask_to_idx:
                        for j in mask_to_idx[m2]:
                            p[find(i)] = find(j)
        group_size = dict()
        for i in range(n):
            root = find(i)
            group_size[root] = group_size.get(root, 0) + 1
        max_size = max(group_size.values())
        return [len(group_size), max_size]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
impl Solution {
    pub fn group_strings(words: Vec<String>) -> Vec<i32> {
        use std::collections::HashMap;
        let n = words.len();
        let mut mask_to_idx: HashMap<i32, Vec<usize>> = HashMap::new();
        let mut masks = vec![0; n];
        for (i, w) in words.iter().enumerate() {
            let mut mask = 0;
            for b in w.bytes() {
                mask |= 1 << (b - b'a');
            }
            masks[i] = mask;
            mask_to_idx.entry(mask).or_default().push(i);
        }
        let mut p: Vec<usize> = (0..n).collect();
        fn find(p: &mut Vec<usize>, x: usize) -> usize {
            if p[x] != x { p[x] = find(p, p[x]); }
            p[x]
        }
        for i in 0..n {
            let mask = masks[i];
            for b in 0..26 {
                let m2 = mask ^ (1 << b);
                if let Some(idxs) = mask_to_idx.get(&m2) {
                    for &j in idxs {
                        let ri = find(&mut p, i);
                        let rj = find(&mut p, j);
                        p[ri] = rj;
                    }
                }
            }
            for b1 in 0..26 {
                if mask & (1 << b1) == 0 { continue; }
                for b2 in 0..26 {
                    if b1 == b2 || mask & (1 << b2) != 0 { continue; }
                    let m2 = (mask ^ (1 << b1)) | (1 << b2);
                    if let Some(idxs) = mask_to_idx.get(&m2) {
                        for &j in idxs {
                            let ri = find(&mut p, i);
                            let rj = find(&mut p, j);
                            p[ri] = rj;
                        }
                    }
                }
            }
        }
        let mut group_size = std::collections::HashMap::new();
        for i in 0..n {
            let root = find(&mut p, i);
            *group_size.entry(root).or_insert(0) += 1;
        }
        let max_size = *group_size.values().max().unwrap();
        vec![group_size.len() as i32, max_size]
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
    groupStrings(words: string[]): number[] {
        const n = words.length;
        const masks: number[] = [];
        const maskToIdx: Record<number, number[]> = {};
        for (let i = 0; i < n; i++) {
            let mask = 0;
            for (const c of words[i]) mask |= 1 << (c.charCodeAt(0) - 97);
            masks.push(mask);
            if (!maskToIdx[mask]) maskToIdx[mask] = [];
            maskToIdx[mask].push(i);
        }
        const p = Array.from({length: n}, (_, i) => i);
        const find = (x: number): number => p[x] === x ? x : (p[x] = find(p[x]));
        for (let i = 0; i < n; i++) {
            const mask = masks[i];
            for (let b = 0; b < 26; b++) {
                const m2 = mask ^ (1 << b);
                if (maskToIdx[m2]) {
                    for (const j of maskToIdx[m2]) p[find(i)] = find(j);
                }
            }
            for (let b1 = 0; b1 < 26; b1++) {
                if (!(mask & (1 << b1))) continue;
                for (let b2 = 0; b2 < 26; b2++) {
                    if (b1 === b2 || (mask & (1 << b2))) continue;
                    const m2 = (mask ^ (1 << b1)) | (1 << b2);
                    if (maskToIdx[m2]) {
                        for (const j of maskToIdx[m2]) p[find(i)] = find(j);
                    }
                }
            }
        }
        const groupSize: Record<number, number> = {};
        for (let i = 0; i < n; i++) {
            const root = find(i);
            groupSize[root] = (groupSize[root] || 0) + 1;
        }
        let maxSize = 0;
        for (const sz of Object.values(groupSize)) maxSize = Math.max(maxSize, sz);
        return [Object.keys(groupSize).length, maxSize];
    }
}

Complexity

  • ⏰ Time complexity: O(n * 26^2) — For each word, we try all add/remove/replace operations (constant for 26 letters).
  • 🧺 Space complexity: O(n) — For DSU and mask mappings.