Similar String Groups
Group strings where two differ by exactly one swap — DSU on pairwise similarity.
5 min read
union-find strings
Problem#
Two strings are “similar” if they are equal, or if you can swap exactly two positions in one to make them equal. Given a list of anagrams, return the number of similarity groups (transitively closed).
Examples#
strs = ["tars","rats","arts","star"]→2.strs = ["omv","ovm"]→1.
Constraints#
1 <= strs.length <= 300,1 <= strs[i].length <= 300.
Hints#
Hint
For each pair
(i, j), count mismatch positions — similar iff 0 or 2 mismatches. Approach#
DSU over string indices. For each pair (i, j), count differing positions; if 0 or 2, union. Answer is the count of distinct roots.
Solution#
class Solution { vector<int> parent; int find(int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; } void unite(int a, int b) { parent[find(a)] = find(b); }public: int numSimilarGroups(vector<string>& strs) { int n = strs.size(); parent.resize(n); iota(parent.begin(), parent.end(), 0); for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { if (find(i) == find(j)) continue; int diff = 0; for (int k = 0; k < (int)strs[i].size() && diff <= 2; ++k) if (strs[i][k] != strs[j][k]) ++diff; if (diff == 0 || diff == 2) unite(i, j); } } int groups = 0; for (int i = 0; i < n; ++i) if (find(i) == i) ++groups; return groups; }};func numSimilarGroups(strs []string) int { n := len(strs) parent := make([]int, n) for i := range parent { parent[i] = i } var find func(x int) int find = func(x int) int { for parent[x] != x { parent[x] = parent[parent[x]] x = parent[x] } return x } unite := func(a, b int) { parent[find(a)] = find(b) } for i := 0; i < n; i++ { for j := i + 1; j < n; j++ { if find(i) == find(j) { continue } diff := 0 for k := 0; k < len(strs[i]) && diff <= 2; k++ { if strs[i][k] != strs[j][k] { diff++ } } if diff == 0 || diff == 2 { unite(i, j) } } } groups := 0 for i := 0; i < n; i++ { if find(i) == i { groups++ } } return groups}from typing import List
class Solution: def numSimilarGroups(self, strs: List[str]) -> int: n = len(strs) parent = list(range(n))
def find(x: int) -> int: while parent[x] != x: parent[x] = parent[parent[x]] x = parent[x] return x
def unite(a: int, b: int) -> None: parent[find(a)] = find(b)
for i in range(n): for j in range(i + 1, n): if find(i) == find(j): continue diff = 0 for k in range(len(strs[i])): if strs[i][k] != strs[j][k]: diff += 1 if diff > 2: break if diff == 0 or diff == 2: unite(i, j) return sum(1 for i in range(n) if find(i) == i)function numSimilarGroups(strs) { const n = strs.length; const parent = Array.from({ length: n }, (_, i) => i); const find = (x) => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; }; const unite = (a, b) => { parent[find(a)] = find(b); }; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { if (find(i) === find(j)) continue; let diff = 0; const L = strs[i].length; for (let k = 0; k < L && diff <= 2; k++) { if (strs[i][k] !== strs[j][k]) diff++; } if (diff === 0 || diff === 2) unite(i, j); } } let groups = 0; for (let i = 0; i < n; i++) if (find(i) === i) groups++; return groups;}class Solution { private int[] parent;
private int find(int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; }
private void unite(int a, int b) { parent[find(a)] = find(b); }
public int numSimilarGroups(String[] strs) { int n = strs.length; parent = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (find(i) == find(j)) continue; int diff = 0; int L = strs[i].length(); for (int k = 0; k < L && diff <= 2; k++) { if (strs[i].charAt(k) != strs[j].charAt(k)) diff++; } if (diff == 0 || diff == 2) unite(i, j); } } int groups = 0; for (int i = 0; i < n; i++) if (find(i) == i) groups++; return groups; }}function numSimilarGroups(strs: string[]): number { const n = strs.length; const parent: number[] = Array.from({ length: n }, (_, i) => i); const find = (x: number): number => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; }; const unite = (a: number, b: number): void => { parent[find(a)] = find(b); }; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { if (find(i) === find(j)) continue; let diff = 0; const L = strs[i].length; for (let k = 0; k < L && diff <= 2; k++) { if (strs[i][k] !== strs[j][k]) diff++; } if (diff === 0 || diff === 2) unite(i, j); } } let groups = 0; for (let i = 0; i < n; i++) if (find(i) === i) groups++; return groups;}Editorial#
For each pair, we early-exit the character comparison once diff > 2 — strings are similar only at exactly 0 or 2 mismatches. The pre-union check find(i) == find(j) short-circuits already-unioned pairs. When N^2 * L < N * 26^L we compare pairs directly; otherwise generate all single-swap neighbors per string and look up in a hash map.
Complexity#
- Time: O(N^2 * L).
- Space: O(N).
Concept revision#
Related#