Union Find — Worked Examples
Scope — The worked-solution archive behind union_find.md: nineteen problems grouped by what a node represents — a vertex, a grid cell, a variable carrying a ratio, or a tree node — since that is the only thing that changes between them. See also: union_find.md — the parent sheet: the DSU template, the six patterns, the optimisations and the traps; graph.md — graph representation in general; topology_sorting.md — the directed counterpart; diff_toposort_quickunion.md — which of the two a problem wants; tree_lca_distance.md — LC 236 and the recursive-LCA framing of example 19).
LeetCode Problem Lists
Overview
This is the long tail of union_find.md. The parent keeps the template, the six patterns and the traps; this file keeps the problems that apply them.
Key Properties
- Complexity: every solution below is near-O(α(n)) per operation with path compression and union by rank — effectively O(1); where a solution is dominated by something else (a sort, a BFS) its own comment says so
- Core Idea: the DSU never changes. What changes is what a node is, and when two of them merge — which is why these are grouped by that rather than by difficulty
- When to Use: after the parent’s pattern table has told you which of the six shapes the problem is
Cycle Detection & Redundancy
1) Redundant Connection — LC 684
Add edges one by one; if two nodes are already connected, this edge is redundant.
// LC 684 - Redundant Connection
// IDEA: Union-Find — detect cycle; redundant edge connects already-connected nodes
// time = O(N * α(N)), space = O(N)
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
int[] parent = new int[n + 1];
for (int i = 0; i <= n; i++) parent[i] = i;
for (int[] edge : edges) {
if (find(parent, edge[0]) == find(parent, edge[1])) return edge;
union(parent, edge[0], edge[1]);
}
return new int[]{};
}
private int find(int[] parent, int x) {
if (parent[x] != x) parent[x] = find(parent, parent[x]); // path compression
return parent[x];
}
private void union(int[] parent, int x, int y) {
parent[find(parent, x)] = find(parent, y);
}
# LC 684 - Redundant Connection
# IDEA: Union-Find (dict-based, union by rank) — process edges; return first edge that forms a cycle
# time = O(N * α(N)), space = O(N)
class Solution(object):
def findRedundantConnection(self, edges):
uf = MyUF()
for a, b in edges:
if not uf.union(a, b):
return [a, b]
return []
class MyUF(object):
def __init__(self):
self.parent = {}
self.rank = {}
def get_parent(self, x):
if x not in self.parent: # lazy init: node becomes its own root on first seen
self.parent[x] = x
self.rank[x] = 0
if self.parent[x] != x:
self.parent[x] = self.get_parent(self.parent[x]) # path compression
return self.parent[x]
def union(self, a, b):
rootA, rootB = self.get_parent(a), self.get_parent(b)
if rootA == rootB:
return False # cycle detected — this edge is redundant
if self.rank[rootA] < self.rank[rootB]:
self.parent[rootA] = rootB
elif self.rank[rootA] > self.rank[rootB]:
self.parent[rootB] = rootA
else:
self.parent[rootB] = rootA
self.rank[rootA] += 1
return True
# LC 684 - Redundant Connection (array-based variant, matches Java approach)
# IDEA: Union-Find (1-indexed array, path compression only) — simpler when nodes are 1..n
# time = O(N * α(N)), space = O(N)
class Solution(object):
def findRedundantConnection(self, edges):
n = len(edges)
uf = UF(n)
for u, v in edges:
if not uf.union(u, v):
return [u, v]
return []
class UF(object):
def __init__(self, n):
self.parents = list(range(n + 1)) # 1-indexed; parents[i] = i initially
def find(self, a):
if self.parents[a] != a:
self.parents[a] = self.find(self.parents[a]) # path compression
return self.parents[a]
def union(self, a, b):
root_a, root_b = self.find(a), self.find(b)
if root_a == root_b:
return False # already connected → cycle
self.parents[root_a] = root_b
return True
2) Redundant Connection II — LC 685 — DSU on a directed graph Priority 4 of 5 — High value — a gap here costs you rounds
Twist on §2-1 (LC 684): in a directed rooted tree the broken invariant can be either (a) a node with two parents, or (b) a cycle, or both. Plain “union fails ⇒ answer” is no longer enough.
Key Idea — two-candidate elimination:
- Scan edges keeping
parent[v]. If somevalready has a parent, recordcand1 = (parent[v], v)(earlier edge) and remember the index of the later edgecand2. - Re-run a plain DSU over all edges skipping
cand2.- Cycle found and there was no two-parent node → return the edge that closed the cycle.
- Cycle found and a two-parent node exists →
cand2was innocent; returncand1. - No cycle → return
cand2.
// java
// LC 685 - Redundant Connection II
// IDEA: directed DSU — locate the two edges into a 2-parent node, drop the later one and
// re-test with union-find; whether a cycle remains tells you which candidate to remove
// time = O(N * α(N)), space = O(N)
public int[] findRedundantDirectedConnection(int[][] edges) {
int n = edges.length;
int[] parent = new int[n + 1]; // parent[v] = u for edge u->v (0 = none yet)
int[] cand1 = null;
int dup = -1; // index of the LATER of the two edges into the same node
for (int i = 0; i < n; i++) {
int u = edges[i][0], v = edges[i][1];
if (parent[v] != 0) {
cand1 = new int[]{parent[v], v}; // the earlier in-edge
dup = i; // the later in-edge (this one)
} else {
parent[v] = u;
}
}
int[] p = new int[n + 1];
int[] sz = new int[n + 1];
for (int i = 0; i <= n; i++) { p[i] = i; sz[i] = 1; }
for (int i = 0; i < n; i++) {
if (i == dup) continue; // pretend the later in-edge doesn't exist
int ru = find(p, edges[i][0]), rv = find(p, edges[i][1]);
if (ru == rv) {
// a cycle survives without cand2
return cand1 == null ? edges[i] // no 2-parent node → this edge closes the cycle
: cand1; // 2-parent node → the EARLIER edge is the culprit
}
// union by size — a plain `p[rv] = ru` can build an O(N) parent chain here
if (sz[ru] < sz[rv]) { int t = ru; ru = rv; rv = t; }
p[rv] = ru;
sz[ru] += sz[rv];
}
return edges[dup]; // no cycle → removing the later in-edge fixes it
}
private int find(int[] p, int x) { return p[x] == x ? x : (p[x] = find(p, p[x])); }
# python
# LC 685 - Redundant Connection II
# IDEA: find the 2-parent node's two in-edges, remove the later one, then DSU-test for a cycle
# time = O(N * α(N)), space = O(N)
class Solution(object):
def findRedundantDirectedConnection(self, edges):
n = len(edges)
par = [0] * (n + 1) # par[v] = u for edge u->v
cand1, dup = None, -1
for i, (u, v) in enumerate(edges):
if par[v] != 0:
cand1 = [par[v], v] # earlier in-edge
dup = i # later in-edge
else:
par[v] = u
parent = list(range(n + 1))
size = [1] * (n + 1)
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
for i, (u, v) in enumerate(edges):
if i == dup:
continue # skip the later in-edge
ru, rv = find(u), find(v)
if ru == rv: # cycle survives without it
return edges[i] if cand1 is None else cand1
# union by size — plain `parent[rv] = ru` can build an O(N) chain,
# which blows Python's recursion limit inside find() when N = 1000
if size[ru] < size[rv]:
ru, rv = rv, ru
parent[rv] = ru
size[ru] += size[rv]
return edges[dup] # no cycle → the later in-edge is redundant
Comparison — LC 684 vs LC 685
| LC 684 (undirected) | LC 685 (directed) | |
|---|---|---|
| Invariant broken | exactly one cycle | 2-parent node or cycle (or both) |
| Algorithm | union edges; first failure is the answer | find the 2 in-edges, drop the later, re-test with DSU |
| Answer when both cases apply | n/a | the earlier in-edge (cand1) |
| Passes over edges | 1 | 2 |
3) Satisfiability of Equality Equations — LC 990
Process ‘==’ edges first; then check ‘!=’ pairs for contradiction.
// LC 990 - Satisfiability of Equality Equations
// IDEA: Union-Find — union on ==, validate != pairs for contradiction
// time = O(N), space = O(26)
public boolean equationsPossible(String[] equations) {
int[] p = new int[26];
for (int i = 0; i < 26; i++) p[i] = i;
for (String eq : equations)
if (eq.charAt(1) == '=') union(p, eq.charAt(0)-'a', eq.charAt(3)-'a');
for (String eq : equations)
if (eq.charAt(1) == '!' && find(p, eq.charAt(0)-'a') == find(p, eq.charAt(3)-'a'))
return false;
return true;
}
private int find(int[] p, int x) { return p[x]==x ? x : (p[x]=find(p,p[x])); }
private void union(int[] p, int x, int y) { p[find(p,x)] = find(p,y); }
# python
# LC 990 - Satisfiability of Equality Equations
# IDEA: Union-Find — union all '==' pairs first, then verify no '!=' pair shares a root
# time = O(N), space = O(26)
class Solution(object):
def equationsPossible(self, equations):
parent = list(range(26))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # path compression
return parent[x]
# pass 1: union every equality
for eq in equations:
if eq[1] == '=':
parent[find(ord(eq[0]) - 97)] = find(ord(eq[3]) - 97)
# pass 2: any inequality inside one component => contradiction
for eq in equations:
if eq[1] == '!' and find(ord(eq[0]) - 97) == find(ord(eq[3]) - 97):
return False
return True
Component Counting & Connectivity
4) Number of Provinces — LC 547
Count the number of distinct roots after unioning all direct friendships.
// LC 547 - Number of Provinces
// IDEA: Union-Find — count distinct components (roots)
// time = O(N^2 * α(N)), space = O(N)
public int findCircleNum(int[][] isConnected) {
int n = isConnected.length;
int[] 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 (isConnected[i][j] == 1) union(parent, i, j);
int count = 0;
for (int i = 0; i < n; i++) if (find(parent, i) == i) count++;
return count;
}
private int find(int[] parent, int x) {
if (parent[x] != x) parent[x] = find(parent, parent[x]);
return parent[x];
}
private void union(int[] parent, int x, int y) {
parent[find(parent, x)] = find(parent, y);
}
# python
# LC 547 - Number of Provinces
# IDEA: Union-Find — union every direct friendship; `components` counter = answer
# time = O(N^2 * α(N)), space = O(N)
class Solution(object):
def findCircleNum(self, isConnected):
n = len(isConnected)
uf = UnionFind(n)
for i in range(n):
for j in range(i + 1, n):
if isConnected[i][j] == 1:
uf.union(i, j)
return uf.components # each successful union decrements the counter
# reuses the `UnionFind` (union by size) class from section 0-3
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.size = [1] * n
self.components = n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
rx, ry = self.find(x), self.find(y)
if rx == ry:
return False
if self.size[rx] < self.size[ry]:
rx, ry = ry, rx
self.parent[ry] = rx
self.size[rx] += self.size[ry]
self.components -= 1
return True
5) Graph Valid Tree — LC 261
Tree has exactly N-1 edges and no cycle; union each edge, return false on same-component edge.
// LC 261 - Graph Valid Tree
// IDEA: Union-Find — N-1 edges + no cycle = valid tree
// time = O(N * α(N)), space = O(N)
public boolean validTree(int n, int[][] edges) {
if (edges.length != n - 1) return false;
int[] p = new int[n];
for (int i = 0; i < n; i++) p[i] = i;
for (int[] e : edges) {
if (find(p, e[0]) == find(p, e[1])) return false;
p[find(p, e[0])] = find(p, e[1]);
}
return true;
}
private int find(int[] p, int x) { return p[x] == x ? x : (p[x] = find(p, p[x])); }
# python
# LC 261 - Graph Valid Tree
# IDEA: Union-Find — a valid tree has exactly N-1 edges AND no cycle
# time = O(N * α(N)), space = O(N)
class Solution(object):
def validTree(self, n, edges):
if len(edges) != n - 1: # tree must have exactly n-1 edges
return False
parent = list(range(n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # path compression
return parent[x]
for a, b in edges:
ra, rb = find(a), find(b)
if ra == rb: # both endpoints already connected => cycle
return False
parent[ra] = rb
return True
6) Number of Connected Components in an Undirected Graph — LC 323
Union each edge; count remaining distinct roots as connected components.
// LC 323 - Number of Connected Components in Undirected Graph
// IDEA: Union-Find — count distinct roots after unioning all edges
// time = O(N * α(N)), space = O(N)
public int countComponents(int n, int[][] edges) {
int[] p = new int[n];
for (int i = 0; i < n; i++) p[i] = i;
int components = n;
for (int[] e : edges) {
int a = find(p, e[0]), b = find(p, e[1]);
if (a != b) { p[a] = b; components--; }
}
return components;
}
private int find(int[] p, int x) { return p[x] == x ? x : (p[x] = find(p, p[x])); }
# python
# LC 323 - Number of Connected Components in Undirected Graph
# IDEA: Union-Find — start with n components, decrement on each successful union
# time = O(N * α(N)), space = O(N)
class Solution(object):
def countComponents(self, n, edges):
parent = list(range(n))
components = n
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # path compression
return parent[x]
for a, b in edges:
ra, rb = find(a), find(b)
if ra != rb:
parent[ra] = rb
components -= 1 # two components merged into one
return components
7) Number of Operations to Make Network Connected — LC 1319
Need at least N-1 edges; count components; extra edges reconnect disconnected components.
// LC 1319 - Number of Operations to Make Network Connected
// IDEA: Union-Find — count components; need (components-1) extra cables
// time = O(N * α(N)), space = O(N)
public int makeConnected(int n, int[][] connections) {
if (connections.length < n - 1) return -1;
int[] p = new int[n];
for (int i = 0; i < n; i++) p[i] = i;
int components = n;
for (int[] c : connections) {
int a = find(p, c[0]), b = find(p, c[1]);
if (a != b) { p[a] = b; components--; }
}
return components - 1;
}
private int find(int[] p, int x) { return p[x]==x ? x : (p[x]=find(p,p[x])); }
# python
# LC 1319 - Number of Operations to Make Network Connected
# IDEA: Union-Find — need >= n-1 cables; answer = (components - 1) redundant cables reused
# time = O(N * α(N)), space = O(N)
class Solution(object):
def makeConnected(self, n, connections):
if len(connections) < n - 1: # not enough cables to ever connect n nodes
return -1
parent = list(range(n))
components = n
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # path compression
return parent[x]
for a, b in connections:
ra, rb = find(a), find(b)
if ra != rb:
parent[ra] = rb
components -= 1
# (components - 1) cables are needed to join the remaining components
return components - 1
Variation — LC 1579 Remove Max Number of Edges to Keep Graph Fully Traversable: run two parallel DSUs (Alice’s and Bob’s). Process type-3 (shared) edges first, unioning into both; then type-1 into Alice only, type-2 into Bob only. Count every edge whose
union()returnedfalse(redundant) — that count is the answer, but return-1unless both DSUs end with exactly 1 component.
Variation — LC 2076 Process Restricted Friend Requests: check before committing the union. For each request
(a,b): if already connected → accept; otherwise scan all restrictions(x,y)and reject iffind(x)/find(y)matchfind(a)/find(b)in either order; only union when no restriction is violated. O(Q · R · α(N)).
8) Count Unreachable Pairs of Nodes — LC 2316 — running-remainder counting
Union all edges → group nodes into components → count pairs of nodes that live in different components (they are unreachable from each other).
Python ref: leetcode_python/Depth-First-Search/count-unreachable-pairs-of-nodes-in-an-undirected-graph.py
Key Idea: two nodes are unreachable ⟺ they belong to different connected components. So the answer = number of cross-component node pairs = Σ (s_i · s_j) over all pairs of components i < j, where s_i is the size of component i.
The remain trick — O(k) cross-pair counting (no nested loop)Priority 1 of 5 — Nice to have
Naively you might:
- compute
C(n, 2)(all pairs) then subtract intra-component pairsΣ C(s_i, 2), or - double-loop over every pair of components
s_i · s_j→ O(k²).
Instead, keep a running remain = “nodes not yet consumed” and accumulate in one pass:
res = 0
remain = n
for s in size.values():
remain -= s # remain now = total nodes in the *remaining* components
res += s * remain # pair this component's s nodes with every node still ahead
return res
Why it works (avoids double-counting):
For component i (processed in order), after remain -= s_i, remain = n − (s_1 + … + s_i) = Σ_{j>i} s_j.
So each step adds s_i · Σ_{j>i} s_j. Summing over all i:
Σ_i s_i · (Σ_{j>i} s_j) = Σ_{i < j} s_i · s_j
which is exactly every cross-component pair, counted once. Subtracting first (remain -= s before multiplying) is what excludes the component’s pairing with itself and prevents (i, j) / (j, i) duplicates.
Visual trace (example graph n = 7, edges = [[0,2],[0,5],[2,4],[1,6],[5,4]] → components of size 4, 2, 1, expected 14):
remain = 7
s=4 → remain = 3 → res += 4*3 = 12 (res=12)
s=2 → remain = 1 → res += 2*1 = 2 (res=14)
s=1 → remain = 0 → res += 1*0 = 0 (res=14) ✅
The per-step values depend on iteration order, but the total is invariant (=
Σ_{i<j} s_i·s_j).
Full solution
# LC 2316 - Count Unreachable Pairs of Nodes in an Undirected Graph
# IDEA: Union-Find → component sizes → running-remainder cross-pair count
# time = O((N + E) * α(N)), space = O(N)
class MyUF:
def __init__(self, n):
self.parents = list(range(n))
def get_parent(self, x):
if self.parents[x] != x:
self.parents[x] = self.get_parent(self.parents[x]) # path compression
return self.parents[x]
def union(self, x, y):
px, py = self.get_parent(x), self.get_parent(y)
if px != py:
self.parents[py] = px
class Solution(object):
def countPairs(self, n, edges):
uf = MyUF(n)
for x, y in edges:
uf.union(x, y)
# root -> component size (store the COUNT, not the node list)
size = {}
for i in range(n):
root = uf.get_parent(i)
size[root] = size.get(root, 0) + 1
res, remain = 0, n
for s in size.values():
remain -= s # remaining nodes ahead of this component
res += s * remain # cross-component pairs, counted once
return res
// LC 2316 - Count Unreachable Pairs of Nodes in an Undirected Graph
// IDEA: Union-Find → component sizes → running-remainder cross-pair count
// time = O((N + E) * α(N)), space = O(N)
public long countPairs(int n, int[][] edges) {
int[] parent = new int[n], size = new int[n];
for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; }
for (int[] e : edges) union(parent, size, e[0], e[1]);
long res = 0, remain = n; // use long: pairs can exceed int range
for (int i = 0; i < n; i++) {
if (find(parent, i) == i) { // i is a root → this component's size is size[i]
remain -= size[i];
res += (long) size[i] * remain;
}
}
return res;
}
private int find(int[] p, int x) { return p[x] == x ? x : (p[x] = find(p, p[x])); }
private void union(int[] p, int[] sz, int x, int y) {
int rx = find(p, x), ry = find(p, y);
if (rx == ry) return;
if (sz[rx] < sz[ry]) { int t = rx; rx = ry; ry = t; }
p[ry] = rx; sz[rx] += sz[ry]; // union by size keeps size[root] correct
}
Gotchas:
- Store the count, not the nodes. You only ever need
s_i, sosize[root] += 1beats collecting node lists → O(N) space, not O(N) per component. - Subtract before multiplying (
remain -= sthenres += s * remain) — reversing the two lines would count each component against itself. - Watch overflow (Java). With
nup to10^5, cross-pairs approach~5·10^9>Integer.MAX_VALUE; uselong. - Same trick, general use: counting cross-group pairs given group sizes
[s_1..s_k]is alwaysΣ_{i<j} s_i·s_j, computable in one O(k) pass this way — handy far beyond Union-Find.
9) Longest Consecutive Sequence — LC 128 — HashSet O(N)
For each number, only start counting if (num-1) is absent — marks sequence start.
// LC 128 - Longest Consecutive Sequence
// IDEA: HashSet — only extend sequences from their start element
// time = O(N), space = O(N)
public int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int n : nums) set.add(n);
int longest = 0;
for (int n : set) {
if (!set.contains(n - 1)) { // sequence start
int len = 1;
while (set.contains(n + len)) len++;
longest = Math.max(longest, len);
}
}
return longest;
}
# python
# LC 128 - Longest Consecutive Sequence
# IDEA: a set, and only ever extend a run from its START element -- so across the whole
# scan the inner while-loop touches each element at most once
# time = O(N), space = O(N)
class Solution(object):
def longestConsecutive(self, nums):
pool = set(nums)
longest = 0
for n in pool:
### NOTE !!! without this guard the inner loop reruns per element -> O(N^2)
if n - 1 in pool:
continue # not a run start
length = 1
while n + length in pool:
length += 1
longest = max(longest, length)
return longest
Grids
10) Number of Islands — LC 200 — grid → 1D via row * cols + col
public int numIslands(char[][] grid) {
int rows = grid.length, cols = grid[0].length;
UnionFind uf = new UnionFind(rows * cols);
int islands = 0;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] == '1') {
islands++;
int idx = r * cols + c;
// Check 4 directions
int[][] dirs = {{0,1}, {1,0}, {0,-1}, {-1,0}};
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols
&& grid[nr][nc] == '1') {
int nidx = nr * cols + nc;
if (uf.union(idx, nidx)) {
islands--;
}
}
}
}
}
}
return islands;
}
# python
# LC 200 - Number of Islands (union-find)
# IDEA: count every '1' as its own island up front, then subtract one per SUCCESSFUL union
# time = O(M*N*alpha(M*N)), space = O(M*N)
class Solution(object):
def numIslands(self, grid):
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
parent = list(range(rows * cols))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]] # path halving
x = parent[x]
return x
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb:
return False # already joined -> not a merge
parent[rb] = ra
return True
islands = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] != '1':
continue
islands += 1
### NOTE !!! right and down only -- each neighbouring pair is then seen once
for dr, dc in ((0, 1), (1, 0)):
nr, nc = r + dr, c + dc
if nr < rows and nc < cols and grid[nr][nc] == '1':
if union(r * cols + c, nr * cols + nc):
islands -= 1
return islands
11) Surrounded Regions — LC 130 — a virtual border node
Union all border ‘O’ cells to a virtual node; any ‘O’ not connected gets flipped to ‘X’.
// LC 130 - Surrounded Regions
// IDEA: Union-Find — connect border O cells to virtual node; flip disconnected O cells
// time = O(M*N), space = O(M*N)
public void solve(char[][] board) {
int m = board.length, n = board[0].length, virtual = m * n;
int[] p = new int[virtual + 1];
for (int i = 0; i <= virtual; i++) p[i] = i;
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (board[i][j] == 'O') {
int id = i * n + j;
if (i == 0 || i == m-1 || j == 0 || j == n-1) union(p, id, virtual);
else for (int[] d : dirs) {
int ni = i+d[0], nj = j+d[1];
if (board[ni][nj] == 'O') union(p, id, ni*n+nj);
}
}
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++)
if (board[i][j] == 'O' && find(p, i*n+j) != find(p, virtual)) board[i][j] = 'X';
}
private int find(int[] p, int x) { return p[x]==x ? x : (p[x]=find(p,p[x])); }
private void union(int[] p, int x, int y) { p[find(p,x)] = find(p,y); }
# python
# LC 130 - Surrounded Regions
# IDEA: one VIRTUAL node stands for "the border". Every 'O' reachable from an edge
# ends up in its component; everything else is enclosed and flips to 'X'.
# time = O(M*N*alpha(M*N)), space = O(M*N)
class Solution(object):
def solve(self, board):
if not board or not board[0]:
return
m, n = len(board), len(board[0])
virtual = m * n # the extra node, index m*n
parent = list(range(virtual + 1))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a, b):
parent[find(a)] = find(b)
for i in range(m):
for j in range(n):
if board[i][j] != 'O':
continue
idx = i * n + j
if i in (0, m - 1) or j in (0, n - 1):
union(idx, virtual) # touches the border
else:
for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):
if board[i + di][j + dj] == 'O':
union(idx, (i + di) * n + (j + dj))
### NOTE !!! LC 130 mutates in place and returns nothing
for i in range(m):
for j in range(n):
if board[i][j] == 'O' and find(i * n + j) != find(virtual):
board[i][j] = 'X'
Variation — LC 959 Regions Cut By Slashes: the DSU node is sub-cell, not cell. Split every cell into 4 triangles (
0=top,1=right,2=bottom,3=left, id =4*(r*n+c)+k). Inside a cell:'/'→ union(0,3) & union(1,2);'\'→ union(0,1) & union(2,3);' '→ union all four. Across cells: union this cell’s1with the right neighbor’s3, and this cell’s2with the bottom neighbor’s0. Answer = component count.
Variation — LC 1559 Detect Cycles in 2D Grid: only union each cell with its right and down neighbor when the letters match; if
find(a) == find(b)before the union, a cycle exists (a grid cycle is automatically length ≥ 4). Same “union fails ⇒ cycle” test as LC 684, applied on a grid.
12) Making A Large Island — LC 827 — size-aware DSU + candidate flip Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern
Pattern: “connect components once, then evaluate every candidate merge in O(1)”. Label all islands with a size-tracking DSU in one pass, then for each
0cell sum the sizes of its distinct neighbouring roots (+1 for the flipped cell itself).
Key Idea: the Set<root> deduplication is the whole trick — two of the four neighbours may belong to the same island, and adding its size twice is the classic wrong answer.
// java
// LC 827 - Making A Large Island
// IDEA: union all 1-cells with size bookkeeping, then for each 0-cell sum the DISTINCT
// neighbouring component sizes + 1
// time = O(N^2 * α(N^2)), space = O(N^2)
int[] p, sz;
public int largestIsland(int[][] grid) {
int n = grid.length, total = n * n;
p = new int[total];
sz = new int[total];
for (int i = 0; i < total; i++) { p[i] = i; sz[i] = 1; }
// pass 1: merge adjacent land cells (right + down is enough for a full scan)
for (int r = 0; r < n; r++)
for (int c = 0; c < n; c++)
if (grid[r][c] == 1) {
if (r + 1 < n && grid[r + 1][c] == 1) union(r * n + c, (r + 1) * n + c);
if (c + 1 < n && grid[r][c + 1] == 1) union(r * n + c, r * n + c + 1);
}
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
int best = 0;
// pass 2: try flipping every 0; also cover the "grid is all 1s" case
for (int r = 0; r < n; r++) {
for (int c = 0; c < n; c++) {
if (grid[r][c] == 1) {
best = Math.max(best, sz[find(r * n + c)]); // no flip needed
continue;
}
Set<Integer> roots = new HashSet<>(); // dedupe: neighbours may share an island
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < n && nc >= 0 && nc < n && grid[nr][nc] == 1)
roots.add(find(nr * n + nc));
}
int totalSize = 1; // the flipped cell itself
for (int root : roots) totalSize += sz[root];
best = Math.max(best, totalSize);
}
}
return best;
}
private int find(int x) { return p[x] == x ? x : (p[x] = find(p[x])); }
private void union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return;
if (sz[ra] < sz[rb]) { int t = ra; ra = rb; rb = t; }
p[rb] = ra;
sz[ra] += sz[rb];
}
# python
# LC 827 - Making A Large Island
# IDEA: size-tracking DSU over land cells, then evaluate each 0-flip via distinct neighbour roots
# time = O(N^2 * α(N^2)), space = O(N^2)
class Solution(object):
def largestIsland(self, grid):
n = len(grid)
parent = list(range(n * n))
size = [1] * (n * n)
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb:
return
if size[ra] < size[rb]:
ra, rb = rb, ra
parent[rb] = ra
size[ra] += size[rb]
for r in range(n):
for c in range(n):
if grid[r][c] == 1:
if r + 1 < n and grid[r + 1][c] == 1:
union(r * n + c, (r + 1) * n + c)
if c + 1 < n and grid[r][c + 1] == 1:
union(r * n + c, r * n + c + 1)
best = 0
for r in range(n):
for c in range(n):
if grid[r][c] == 1:
best = max(best, size[find(r * n + c)]) # handles the all-1s grid
continue
roots = set() # MUST dedupe by root
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 1:
roots.add(find(nr * n + nc))
best = max(best, 1 + sum(size[root] for root in roots))
return best
Gotchas:
size[x]is only meaningful whenxis a root — always index it withfind(...).- Don’t forget the all-land case (no
0to flip): seedingbestfrom existing component sizes covers it.
Weighted, Sorted-Edge & Offline Variants
13) Evaluate Division — LC 399 — weighted union-find with ratios Priority 4 of 5 — High value — a gap here costs you rounds
class WeightedUnionFind {
Map<String, String> parent;
Map<String, Double> ratio; // ratio[x] = x / parent[x]
public WeightedUnionFind() {
parent = new HashMap<>();
ratio = new HashMap<>();
}
public String find(String x) {
if (!parent.containsKey(x)) {
parent.put(x, x);
ratio.put(x, 1.0);
}
if (!x.equals(parent.get(x))) {
String originalParent = parent.get(x);
parent.put(x, find(originalParent));
ratio.put(x, ratio.get(x) * ratio.get(originalParent));
}
return parent.get(x);
}
public void union(String x, String y, double value) {
String rootX = find(x);
String rootY = find(y);
if (!rootX.equals(rootY)) {
parent.put(rootX, rootY);
ratio.put(rootX, value * ratio.get(y) / ratio.get(x));
}
}
public double query(String x, String y) {
if (!parent.containsKey(x) || !parent.containsKey(y)) {
return -1.0;
}
String rootX = find(x);
String rootY = find(y);
if (!rootX.equals(rootY)) return -1.0;
return ratio.get(x) / ratio.get(y);
}
}
public double[] calcEquation(List<List<String>> equations,
double[] values,
List<List<String>> queries) {
WeightedUnionFind uf = new WeightedUnionFind();
for (int i = 0; i < equations.size(); i++) {
String a = equations.get(i).get(0);
String b = equations.get(i).get(1);
uf.union(a, b, values[i]);
}
double[] results = new double[queries.size()];
for (int i = 0; i < queries.size(); i++) {
String c = queries.get(i).get(0);
String d = queries.get(i).get(1);
results[i] = uf.query(c, d);
}
return results;
}
# python
# LC 399 - Evaluate Division (weighted union-find)
# IDEA: ratio[x] = x / parent[x]. Path compression MULTIPLIES the ratios it collapses,
# so after find(x) the stored ratio is x / root directly.
# time = O((E + Q) * alpha(N)), space = O(N)
class WeightedUnionFind(object):
def __init__(self):
self.parent = {}
self.ratio = {} # ratio[x] = x / parent[x]
def find(self, x):
if x not in self.parent:
self.parent[x] = x
self.ratio[x] = 1.0
if self.parent[x] != x:
original = self.parent[x]
self.parent[x] = self.find(original)
### NOTE !!! multiply on the way back up -- this is what makes ratio[x] = x / root
self.ratio[x] *= self.ratio[original]
return self.parent[x]
def union(self, x, y, value): # x / y == value
rx, ry = self.find(x), self.find(y)
if rx != ry:
self.parent[rx] = ry
self.ratio[rx] = value * self.ratio[y] / self.ratio[x]
def query(self, x, y):
if x not in self.parent or y not in self.parent:
return -1.0 # an unseen variable is unanswerable
if self.find(x) != self.find(y):
return -1.0 # different components -> no path
return self.ratio[x] / self.ratio[y]
class Solution(object):
def calcEquation(self, equations, values, queries):
uf = WeightedUnionFind()
for (a, b), v in zip(equations, values):
uf.union(a, b, v)
return [uf.query(c, d) for c, d in queries]
14) Path With Minimum Effort — LC 1631 — sorted-edge (Kruskal-style)
Pattern: “minimize the maximum edge on a path” / “smallest threshold that connects A and B”. Sort all edges ascending, union them one by one, and stop the moment
find(src) == find(dst). The weight of the edge that just closed the connection is the answer — no binary search, no Dijkstra needed.
Key Idea: the DSU is a monotone connectivity oracle. Adding edges in increasing weight order means “the graph restricted to weights ≤ w”; the first w at which source and target join is by definition the minimum possible bottleneck. This is exactly Kruskal’s MST sweep, stopped early.
When to use: the cost of a path is max(edge) (not sum(edge)), or the query is “are A and B connected using only edges ≤ limit”.
// java
// LC 1631 - Path With Minimum Effort
// IDEA: sorted-edge Union-Find (Kruskal sweep) — add edges cheapest-first; the edge that
// first connects (0,0) with (m-1,n-1) is the minimum possible bottleneck
// time = O(M*N*log(M*N)), space = O(M*N)
public int minimumEffortPath(int[][] heights) {
int m = heights.length, n = heights[0].length;
// build one edge per adjacent cell pair: {weight, cellA, cellB}
List<int[]> edges = new ArrayList<>();
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
int id = r * n + c;
if (r + 1 < m) edges.add(new int[]{Math.abs(heights[r][c] - heights[r + 1][c]), id, id + n});
if (c + 1 < n) edges.add(new int[]{Math.abs(heights[r][c] - heights[r][c + 1]), id, id + 1});
}
}
edges.sort((a, b) -> a[0] - b[0]); // cheapest first
int[] p = new int[m * n];
for (int i = 0; i < m * n; i++) p[i] = i;
for (int[] e : edges) {
int ra = find(p, e[1]), rb = find(p, e[2]);
if (ra != rb) p[ra] = rb;
if (find(p, 0) == find(p, m * n - 1)) return e[0]; // just connected → this weight is the answer
}
return 0; // single cell (no edges) → effort 0
}
private int find(int[] p, int x) { return p[x] == x ? x : (p[x] = find(p, p[x])); }
# python
# LC 1631 - Path With Minimum Effort
# IDEA: sorted-edge Union-Find (Kruskal sweep) — the first edge that joins start & end is the bottleneck
# time = O(M*N*log(M*N)), space = O(M*N)
class Solution(object):
def minimumEffortPath(self, heights):
m, n = len(heights), len(heights[0])
parent = list(range(m * n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # path compression
return parent[x]
edges = [] # (weight, cellA, cellB)
for r in range(m):
for c in range(n):
idx = r * n + c
if r + 1 < m:
edges.append((abs(heights[r][c] - heights[r + 1][c]), idx, idx + n))
if c + 1 < n:
edges.append((abs(heights[r][c] - heights[r][c + 1]), idx, idx + 1))
edges.sort() # cheapest first
for w, a, b in edges:
ra, rb = find(a), find(b)
if ra != rb:
parent[ra] = rb
if find(0) == find(m * n - 1): # start & end now connected using weights <= w
return w
return 0 # 1x1 grid
Variations of this template
LC 778 Swim in Rising Water — the weight lives on the cell, not the edge. Heights are a permutation of
0..n*n-1, so precomputepos[height] = cellId, then fort = 0, 1, 2, ...activate cellpos[t]and union it with the neighbours that are already active; return the firsttwherefind(0) == find(n*n-1). Same monotone sweep, O(N²·α) with no sort needed.
// java
// LC 778 - Swim in Rising Water
// IDEA: same monotone sweep as LC 1631, but activate CELLS in increasing elevation
// time = O(N^2 * α(N^2)), space = O(N^2)
public int swimInWater(int[][] grid) {
int n = grid.length, total = n * n;
int[] pos = new int[total]; // elevation -> cell id (heights are a permutation)
for (int r = 0; r < n; r++)
for (int c = 0; c < n; c++)
pos[grid[r][c]] = r * n + c;
int[] p = new int[total];
for (int i = 0; i < total; i++) p[i] = i;
boolean[] active = new boolean[total];
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
for (int t = 0; t < total; t++) {
int id = pos[t], r = id / n, c = id % n;
active[id] = true; // water level t reaches this cell
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr < 0 || nr >= n || nc < 0 || nc >= n) continue;
int nid = nr * n + nc;
if (!active[nid]) continue; // only merge with already-flooded cells
int ra = find(p, id), rb = find(p, nid);
if (ra != rb) p[ra] = rb;
}
if (find(p, 0) == find(p, total - 1)) return t;
}
return total - 1;
}
# python
# LC 778 - Swim in Rising Water
# IDEA: activate cells in increasing elevation; answer = first time start & end are connected
# time = O(N^2 * α(N^2)), space = O(N^2)
class Solution(object):
def swimInWater(self, grid):
n = len(grid)
total = n * n
pos = [0] * total
for r in range(n):
for c in range(n):
pos[grid[r][c]] = r * n + c
parent = list(range(total))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
active = [False] * total
for t in range(total):
idx = pos[t]
r, c = divmod(idx, n)
active[idx] = True
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and active[nr * n + nc]:
ra, rb = find(idx), find(nr * n + nc)
if ra != rb:
parent[ra] = rb
if find(0) == find(total - 1):
return t
return total - 1
LC 1697 Checking Existence of Edge Length Limited Paths — the offline query form of the same sweep: sort edges by weight AND sort the queries by
limit, then walk both with one pointer, unioning every edge withweight < limitbefore answeringfind(p) == find(q). Remember to restore the original query order when returning the answers.
15) Bricks Falling When Hit — LC 803 — offline reverse union-find Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern
Pattern: the problem deletes things, but DSU can only merge. Fix: run time backwards — delete everything first, then add the deletions back one at a time.
Key Idea: a brick is stable iff it is connected to a virtual roof node (row 0). Erasing a brick is un-DSU-able, so:
- Apply all hits up front (set those cells to 0).
- Build the DSU on what survives, unioning row-0 bricks into the roof node
m*n. - Walk the hits in reverse, re-adding each brick. The roof component’s size jump
after − before − 1(minus the restored brick itself) is exactly the number of bricks that fell for that hit.
Requires union by size so size[find(roof)] is meaningful.
// java
// LC 803 - Bricks Falling When Hit
// IDEA: offline REVERSE union-find — erase every hit first, then undo them one by one;
// bricks that fall on hit i == bricks that re-attach to the roof when hit i is undone
// time = O(M*N*α(M*N) + K*α(M*N)), space = O(M*N)
int[] p, sz;
public int[] hitBricks(int[][] grid, int[][] hits) {
int m = grid.length, n = grid[0].length, roof = m * n;
int[][] g = new int[m][];
for (int i = 0; i < m; i++) g[i] = grid[i].clone();
/** NOTE !!! record whether each hit actually removed a brick.
* Two hits on the same cell both see grid[r][c] == 1, so testing the ORIGINAL grid in
* the reverse pass credits the fall to the wrong hit. Only the first hit on a cell is
* effective; every later one lands on an already-empty cell and must score 0.
*/
boolean[] effective = new boolean[hits.length];
for (int i = 0; i < hits.length; i++) { // step 1: apply ALL hits up front
int r = hits[i][0], c = hits[i][1];
if (g[r][c] == 1) { g[r][c] = 0; effective[i] = true; }
}
p = new int[m * n + 1];
sz = new int[m * n + 1];
for (int i = 0; i <= m * n; i++) { p[i] = i; sz[i] = 1; }
// step 2: build DSU on the surviving bricks (up/left neighbours suffice for a full scan)
for (int r = 0; r < m; r++)
for (int c = 0; c < n; c++)
if (g[r][c] == 1) {
if (r == 0) union(r * n + c, roof);
if (r > 0 && g[r - 1][c] == 1) union(r * n + c, (r - 1) * n + c);
if (c > 0 && g[r][c - 1] == 1) union(r * n + c, r * n + c - 1);
}
// step 3: undo hits in reverse order
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
int[] res = new int[hits.length];
for (int i = hits.length - 1; i >= 0; i--) {
if (!effective[i]) continue; // empty cell, or a repeat hit → nothing falls
int r = hits[i][0], c = hits[i][1];
int before = sz[find(roof)];
g[r][c] = 1; // put the brick back
if (r == 0) union(r * n + c, roof);
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && g[nr][nc] == 1) union(r * n + c, nr * n + nc);
}
int after = sz[find(roof)];
res[i] = Math.max(0, after - before - 1); // -1: the restored brick itself never "fell"
}
return res;
}
private int find(int x) { return p[x] == x ? x : (p[x] = find(p[x])); }
private void union(int a, int b) { // union by SIZE — sz[root] must stay exact
int ra = find(a), rb = find(b);
if (ra == rb) return;
if (sz[ra] < sz[rb]) { int t = ra; ra = rb; rb = t; }
p[rb] = ra;
sz[ra] += sz[rb];
}
# python
# LC 803 - Bricks Falling When Hit
# IDEA: offline reverse union-find + virtual roof node + union by size
# time = O(M*N*α(M*N) + K*α(M*N)), space = O(M*N)
class Solution(object):
def hitBricks(self, grid, hits):
m, n = len(grid), len(grid[0])
roof = m * n
g = [row[:] for row in grid]
### NOTE !!! only the FIRST hit on a cell removes a brick; later hits on the same
### cell land on an empty slot and must score 0, so record which is which.
effective = [False] * len(hits)
for i, (r, c) in enumerate(hits): # apply ALL hits first
if g[r][c] == 1:
g[r][c] = 0
effective[i] = True
parent = list(range(m * n + 1))
size = [1] * (m * n + 1)
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb:
return
if size[ra] < size[rb]:
ra, rb = rb, ra
parent[rb] = ra
size[ra] += size[rb] # union by size keeps size[root] exact
for r in range(m):
for c in range(n):
if g[r][c] == 1:
if r == 0:
union(r * n + c, roof) # row 0 hangs from the roof
if r > 0 and g[r - 1][c] == 1:
union(r * n + c, (r - 1) * n + c)
if c > 0 and g[r][c - 1] == 1:
union(r * n + c, r * n + c - 1)
res = [0] * len(hits)
for i in range(len(hits) - 1, -1, -1): # undo hits backwards
if not effective[i]:
continue # empty cell, or a repeat hit → 0
r, c = hits[i]
before = size[find(roof)]
g[r][c] = 1
if r == 0:
union(r * n + c, roof)
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < m and 0 <= nc < n and g[nr][nc] == 1:
union(r * n + c, nr * n + nc)
after = size[find(roof)]
res[i] = max(0, after - before - 1) # max(0, ...) also handles duplicate hits
return res
Gotchas:
- A hit on a cell that was already
0in the original grid contributes0. - Duplicate hits on the same cell are the trap. Both see
grid[r][c] == 1, so testing the original grid in the reverse pass credits the fall to whichever hit the loop reaches first — which, running backwards, is the later one. The correct answer belongs to the earlier hit. Record aneffectiveflag during the forward pass and skip on that instead. - Reverse DSU only works when the deletions are known offline (all given up front).
Union-Find on Other Structures
16) Accounts Merge — LC 721 — union-find over emails
Union emails belonging to the same person; group by root; sort and format.
// LC 721 - Accounts Merge
// IDEA: Union-Find — union all emails in same account; group by root
// time = O(N * M * α(N*M)), space = O(N*M)
public List<List<String>> accountsMerge(List<List<String>> accounts) {
Map<String, String> parent = new HashMap<>();
Map<String, String> emailToName = new HashMap<>();
// init
for (List<String> acc : accounts)
for (int i = 1; i < acc.size(); i++) {
parent.put(acc.get(i), acc.get(i));
emailToName.put(acc.get(i), acc.get(0));
}
// union
for (List<String> acc : accounts)
for (int i = 2; i < acc.size(); i++)
union(parent, acc.get(1), acc.get(i));
// group by root
Map<String, TreeSet<String>> groups = new HashMap<>();
for (String email : parent.keySet())
groups.computeIfAbsent(find(parent, email), k -> new TreeSet<>()).add(email);
List<List<String>> result = new ArrayList<>();
for (Map.Entry<String, TreeSet<String>> entry : groups.entrySet()) {
List<String> list = new ArrayList<>();
list.add(emailToName.get(entry.getKey()));
list.addAll(entry.getValue());
result.add(list);
}
return result;
}
private String find(Map<String, String> parent, String x) {
if (!parent.get(x).equals(x)) parent.put(x, find(parent, parent.get(x)));
return parent.get(x);
}
private void union(Map<String, String> parent, String x, String y) {
parent.put(find(parent, x), find(parent, y));
}
# python
# LC 721 - Accounts Merge
# IDEA: union every email in an account to the account's FIRST email, then group by root.
# The name is looked up from any email in the group.
# time = O(N*M*alpha + N*M log(N*M)) for the sort, space = O(N*M)
class Solution(object):
def accountsMerge(self, accounts):
parent = {}
email_to_name = {}
def find(x):
parent.setdefault(x, x)
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a, b):
parent[find(a)] = find(b)
for acc in accounts:
name, emails = acc[0], acc[1:]
for e in emails:
parent.setdefault(e, e)
email_to_name[e] = name
### NOTE !!! union to the FIRST email, which links the whole account in one pass
union(emails[0], e)
groups = {}
for e in parent:
groups.setdefault(find(e), []).append(e)
return [[email_to_name[root]] + sorted(mails)
for root, mails in groups.items()]
Variation — LC 839 Similar String Groups: same “union then group by root” shape, but the edges are not given. All strings are anagrams, so run the O(N² · L) pairwise check —
union(i, j)iffs[i] == s[j]or they differ at exactly 2 positions — then the answer is the component count.
17) Smallest String with Swaps — LC 1202 — union-find + sorting
Union swap pairs; sort characters within each component; place sorted chars back.
// LC 1202 - Smallest String with Swaps
// IDEA: Union-Find — group indices; sort chars in each group and reassign
// time = O(N log N), space = O(N)
public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
int n = s.length();
int[] p = new int[n];
for (int i = 0; i < n; i++) p[i] = i;
for (List<Integer> pair : pairs) union(p, pair.get(0), pair.get(1));
Map<Integer, List<Integer>> groups = new HashMap<>();
for (int i = 0; i < n; i++) groups.computeIfAbsent(find(p, i), k -> new ArrayList<>()).add(i);
char[] res = s.toCharArray();
for (List<Integer> idx : groups.values()) {
char[] chars = new char[idx.size()];
for (int i = 0; i < idx.size(); i++) chars[i] = s.charAt(idx.get(i));
Arrays.sort(chars);
Collections.sort(idx);
for (int i = 0; i < idx.size(); i++) res[idx.get(i)] = chars[i];
}
return new String(res);
}
private int find(int[] p, int x) { return p[x]==x ? x : (p[x]=find(p,p[x])); }
private void union(int[] p, int x, int y) { p[find(p,x)] = find(p,y); }
# python
# LC 1202 - Smallest String with Swaps
# IDEA: any two indices in the same component can be swapped freely, so the characters of a
# component can be permuted arbitrarily -- sort them and write them back in index order
# time = O(N log N), space = O(N)
class Solution(object):
def smallestStringWithSwaps(self, s, pairs):
n = len(s)
parent = list(range(n))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
for a, b in pairs:
parent[find(a)] = find(b)
groups = {}
for i in range(n):
groups.setdefault(find(i), []).append(i)
res = list(s)
for idxs in groups.values():
### NOTE !!! idxs is already ascending; sorting the CHARS and zipping them back
### in that order is what yields the lexicographically smallest result
chars = sorted(res[i] for i in idxs)
for i, ch in zip(idxs, chars):
res[i] = ch
return ''.join(res)
18) Most Stones Removed with Same Row or Column — LC 947
Union stones in the same row or column; answer = stones − number of components.
// LC 947 - Most Stones Removed with Same Row or Column
// IDEA: Union-Find — stones sharing row/column are in same component; remove all but one
// time = O(N^2 * α(N)), space = O(N)
public int removeStones(int[][] stones) {
int n = stones.length;
int[] p = new int[n];
for (int i = 0; i < n; i++) p[i] = i;
for (int i = 0; i < n; i++)
for (int j = i+1; j < n; j++)
if (stones[i][0] == stones[j][0] || stones[i][1] == stones[j][1])
union(p, i, j);
Set<Integer> roots = new HashSet<>();
for (int i = 0; i < n; i++) roots.add(find(p, i));
return n - roots.size();
}
private int find(int[] p, int x) { return p[x]==x ? x : (p[x]=find(p,p[x])); }
private void union(int[] p, int x, int y) { p[find(p,x)] = find(p,y); }
# python
# LC 947 - Most Stones Removed with Same Row or Column
# IDEA: stones sharing a row or column are one component; a component of size k can be
# reduced to a single stone, so the answer is n - (number of components)
# time = O(N^2 * alpha(N)), space = O(N)
class Solution(object):
def removeStones(self, stones):
n = len(stones)
parent = list(range(n))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
for i in range(n):
for j in range(i + 1, n):
if stones[i][0] == stones[j][0] or stones[i][1] == stones[j][1]:
parent[find(i)] = find(j)
### NOTE !!! count ROOTS, not unions -- a component of any size contributes exactly 1
return n - len({find(i) for i in range(n)})
18-1) Couples Holding Hands — LC 765 — components → swaps
The union is on the COUPLE, not the person. Number the couples
0 .. n-1, so personpbelongs to couplep / 2. Each seat pair(2i, 2i+1)says “these two couples are entangled” — union them. A connected component ofkcouples is a cycle that takes exactlyk - 1swaps to untangle, so the answer is the samen - componentsarithmetic as LC 947 above.
row = [0, 2, 4, 6, 7, 1, 3, 5] couples: 0,1,2,3
seat pairs couples union
(0, 2) 0, 1 {0,1}
(4, 6) 2, 3 {0,1} {2,3}
(7, 1) 3, 0 {0,1,2,3}
(3, 5) 1, 2 (already joined)
4 couples, 1 component -> 4 - 1 = 3 swaps
// java
// LC 765 - Couples Holding Hands
// IDEA: union the two COUPLE ids sharing a seat pair; a component of k couples needs
// k-1 swaps, so answer = couples - components.
// time = O(n * alpha(n)), space = O(n)
public int minSwapsCouples(int[] row) {
int n = row.length / 2; // number of couples
int[] parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
for (int i = 0; i < row.length; i += 2) {
int a = find(parent, row[i] / 2), b = find(parent, row[i + 1] / 2);
parent[a] = b; // seat pair -> these couples are entangled
}
Set<Integer> roots = new HashSet<>();
for (int i = 0; i < n; i++) roots.add(find(parent, i));
return n - roots.size(); // count ROOTS, not unions
}
private int find(int[] parent, int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // path halving
x = parent[x];
}
return x;
}
The greedy alternative is shorter to write and worth knowing, because it also produces the
swaps rather than just counting them: walk the even seats, and if the person next to row[i] is not
their partner, swap the partner in from wherever they are. Each swap seats one couple permanently,
so the greedy is optimal and the two methods always agree.
# python
# LC 765 - Couples Holding Hands (greedy — same answer, and it shows the moves)
# IDEA: partner of p is p ^ 1. Fix seat pairs left to right; a position map makes each
# swap O(1), so the whole scan is linear.
# time = O(n), space = O(n)
def minSwapsCouples(row):
row = list(row)
pos = {p: i for i, p in enumerate(row)} # person -> current seat
swaps = 0
for i in range(0, len(row), 2):
partner = row[i] ^ 1 # 0<->1, 2<->3, ...
if row[i + 1] == partner:
continue
j = pos[partner]
# swap the stranger out and the partner in, keeping `pos` in step
pos[row[i + 1]], pos[partner] = j, i + 1
row[j], row[i + 1] = row[i + 1], row[j]
swaps += 1
return swaps
Why
p ^ 1gives the partner — couples are(0,1), (2,3), (4,5)…, so partners differ only in the lowest bit.p ^ 1beatsp + 1 if p % 2 == 0 else p - 1and is the reason the couple id isp / 2(equivalentlyp >> 1).
Same arithmetic, different problems: LC 947 Most Stones Removed (above) answers
items - components too. LC 839 Similar String Groups builds the same components but returns the
component count itself — so it is the same union-find and a different final line. The pattern to
carry away is “a component of size k costs k-1 operations”, and either way you count roots, never
unions.
19) Smallest Subtree with all the Deepest Nodes — LC 865 — BFS + union-find climb
BFS finds deepest nodes and parent map; then all deepest nodes “climb” upward via parents until they converge to the LCA. This is the same as LC 1123.
// LC 865 - Smallest Subtree with all the Deepest Nodes
// IDEA: BFS to find deepest level + build parent map, then climb upward until convergence
// time = O(N), space = O(N)
public TreeNode subtreeWithAllDeepest(TreeNode root) {
Map<TreeNode, TreeNode> parent = new HashMap<>();
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
parent.put(root, null);
List<TreeNode> level = new ArrayList<>();
// BFS: build parent map, track each level (last level = deepest)
while (!q.isEmpty()) {
int size = q.size();
level = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode cur = q.poll();
level.add(cur);
if (cur.left != null) { parent.put(cur.left, cur); q.offer(cur.left); }
if (cur.right != null) { parent.put(cur.right, cur); q.offer(cur.right); }
}
}
// Climb: replace each node with its parent until all converge
Set<TreeNode> set = new HashSet<>(level);
while (set.size() > 1) {
Set<TreeNode> next = new HashSet<>();
for (TreeNode node : set) next.add(parent.get(node));
set = next;
}
return set.iterator().next();
}
# python
# LC 865 - Smallest Subtree with all the Deepest Nodes (same problem as LC 1123)
# IDEA: BFS to the deepest level while recording each node's parent, then walk every
# deepest node upward in lockstep until the set collapses to one node -- the LCA
# time = O(N), space = O(N)
from collections import deque
class Solution(object):
def subtreeWithAllDeepest(self, root):
if not root:
return None
parent = {root: None}
q = deque([root])
level = [root]
while q:
level = []
for _ in range(len(q)):
cur = q.popleft()
level.append(cur)
for child in (cur.left, cur.right):
if child:
parent[child] = cur
q.append(child)
# `level` now holds the LAST level visited = the deepest nodes
### NOTE !!! climbing in LOCKSTEP is what makes this correct -- all nodes are at the
### same depth, so they reach their common ancestor on the same step
nodes = set(level)
while len(nodes) > 1:
nodes = {parent[n] for n in nodes}
return nodes.pop()