Union Find (Disjoint Set Union, DSU) is a data structure for tracking elements partitioned into disjoint sets. It efficiently handles union and find operations.
What is Union Find? #
A data structure that maintains a collection of disjoint (non-overlapping) sets and supports two operations:
- Find - Determine which set an element belongs to
- Union - Merge two sets into one
Applications:
- Detect cycles in graphs
- Find connected components
- Kruskal’s MST algorithm
- Network connectivity
- Image processing
Basic Implementation #
class UnionFind {
constructor(size) {
this.parent = Array.from({ length: size }, (_, i) => i);
this.count = size; // Number of disjoint sets
}
find(x) {
if (this.parent[x] !== x) {
return this.find(this.parent[x]);
}
return x;
}
union(x, y) {
const rootX = this.find(x);
const rootY = this.find(y);
if (rootX === rootY) {
return false; // Already connected
}
this.parent[rootX] = rootY;
this.count--;
return true;
}
connected(x, y) {
return this.find(x) === this.find(y);
}
getCount() {
return this.count;
}
}
// Usage
const uf = new UnionFind(5);
uf.union(0, 1);
uf.union(1, 2);
console.log(uf.connected(0, 2)); // true
console.log(uf.connected(0, 3)); // false
console.log(uf.getCount()); // 3 sets
Path Compression #
Optimize find operation by flattening tree structure.
class UnionFind {
constructor(size) {
this.parent = Array.from({ length: size }, (_, i) => i);
this.count = size;
}
find(x) {
if (this.parent[x] !== x) {
// Path compression
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}
union(x, y) {
const rootX = this.find(x);
const rootY = this.find(y);
if (rootX === rootY) {
return false;
}
this.parent[rootX] = rootY;
this.count--;
return true;
}
}Union by Rank #
Attach smaller tree under root of larger tree.
class UnionFind {
constructor(size) {
this.parent = Array.from({ length: size }, (_, i) => i);
this.rank = new Array(size).fill(0);
this.count = size;
}
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}
union(x, y) {
const rootX = this.find(x);
const rootY = this.find(y);
if (rootX === rootY) {
return false;
}
// Union by rank
if (this.rank[rootX] < this.rank[rootY]) {
this.parent[rootX] = rootY;
} else if (this.rank[rootX] > this.rank[rootY]) {
this.parent[rootY] = rootX;
} else {
this.parent[rootY] = rootX;
this.rank[rootX]++;
}
this.count--;
return true;
}
}Union by Size #
Track size of each set.
class UnionFind {
constructor(size) {
this.parent = Array.from({ length: size }, (_, i) => i);
this.size = new Array(size).fill(1);
this.count = size;
}
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}
union(x, y) {
const rootX = this.find(x);
const rootY = this.find(y);
if (rootX === rootY) {
return false;
}
// Union by size
if (this.size[rootX] < this.size[rootY]) {
this.parent[rootX] = rootY;
this.size[rootY] += this.size[rootX];
} else {
this.parent[rootY] = rootX;
this.size[rootX] += this.size[rootY];
}
this.count--;
return true;
}
getSize(x) {
return this.size[this.find(x)];
}
}
const uf = new UnionFind(5);
uf.union(0, 1);
uf.union(1, 2);
console.log(uf.getSize(0)); // 3
console.log(uf.getSize(3)); // 1
Number of Connected Components #
function countComponents(n, edges) {
const uf = new UnionFind(n);
for (const [u, v] of edges) {
uf.union(u, v);
}
return uf.getCount();
}
console.log(countComponents(5, [[0, 1], [1, 2], [3, 4]])); // 2
console.log(countComponents(5, [[0, 1], [1, 2], [2, 3], [3, 4]])); // 1
Detect Cycle in Undirected Graph #
function hasCycle(n, edges) {
const uf = new UnionFind(n);
for (const [u, v] of edges) {
if (!uf.union(u, v)) {
return true; // Cycle detected
}
}
return false;
}
console.log(hasCycle(3, [[0, 1], [1, 2], [2, 0]])); // true
console.log(hasCycle(3, [[0, 1], [1, 2]])); // false
Redundant Connection #
Find edge that creates cycle.
function findRedundantConnection(edges) {
const n = edges.length;
const uf = new UnionFind(n + 1);
for (const [u, v] of edges) {
if (!uf.union(u, v)) {
return [u, v]; // This edge creates cycle
}
}
return [];
}
console.log(findRedundantConnection([[1, 2], [1, 3], [2, 3]])); // [2, 3]
console.log(findRedundantConnection([[1, 2], [2, 3], [3, 4], [1, 4], [1, 5]])); // [1, 4]
Accounts Merge #
function accountsMerge(accounts) {
const emailToName = new Map();
const emailToId = new Map();
let id = 0;
// Map emails to IDs
for (const account of accounts) {
const name = account[0];
for (let i = 1; i < account.length; i++) {
const email = account[i];
if (!emailToId.has(email)) {
emailToId.set(email, id++);
emailToName.set(email, name);
}
}
}
const uf = new UnionFind(id);
// Union emails from same account
for (const account of accounts) {
const firstEmail = account[1];
const firstId = emailToId.get(firstEmail);
for (let i = 2; i < account.length; i++) {
const email = account[i];
uf.union(firstId, emailToId.get(email));
}
}
// Group emails by root
const groups = new Map();
for (const [email, emailId] of emailToId) {
const root = uf.find(emailId);
if (!groups.has(root)) {
groups.set(root, []);
}
groups.get(root).push(email);
}
// Create result
const result = [];
for (const emails of groups.values()) {
emails.sort();
result.push([emailToName.get(emails[0]), ...emails]);
}
return result;
}
const accounts = [
['John', 'john@mail.com', 'john_work@mail.com'],
['John', 'john@mail.com', 'john@other.com'],
['Mary', 'mary@mail.com']
];
console.log(accountsMerge(accounts));
// [['John', 'john@mail.com', 'john@other.com', 'john_work@mail.com'], ['Mary', 'mary@mail.com']]
Friend Circles #
function findCircleNum(isConnected) {
const n = isConnected.length;
const uf = new UnionFind(n);
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
if (isConnected[i][j] === 1) {
uf.union(i, j);
}
}
}
return uf.getCount();
}
const isConnected = [
[1, 1, 0],
[1, 1, 0],
[0, 0, 1]
];
console.log(findCircleNum(isConnected)); // 2
Number of Islands #
function numIslands(grid) {
if (grid.length === 0) return 0;
const rows = grid.length;
const cols = grid[0].length;
let waterCells = 0;
const uf = new UnionFind(rows * cols);
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (grid[i][j] === '0') {
waterCells++;
} else {
// Union with right neighbor
if (j + 1 < cols && grid[i][j + 1] === '1') {
uf.union(i * cols + j, i * cols + j + 1);
}
// Union with bottom neighbor
if (i + 1 < rows && grid[i + 1][j] === '1') {
uf.union(i * cols + j, (i + 1) * cols + j);
}
}
}
}
return uf.getCount() - waterCells;
}
const grid = [
['1', '1', '0', '0', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '1', '0', '0'],
['0', '0', '0', '1', '1']
];
console.log(numIslands(grid)); // 3
Optimize Water Distribution #
function minCostToSupplyWater(n, wells, pipes) {
// Add virtual node 0 connected to all wells
const edges = [];
for (let i = 0; i < wells.length; i++) {
edges.push([0, i + 1, wells[i]]);
}
for (const [house1, house2, cost] of pipes) {
edges.push([house1, house2, cost]);
}
// Sort edges by cost
edges.sort((a, b) => a[2] - b[2]);
// Kruskal's algorithm
const uf = new UnionFind(n + 1);
let totalCost = 0;
let edgesUsed = 0;
for (const [u, v, cost] of edges) {
if (uf.union(u, v)) {
totalCost += cost;
edgesUsed++;
if (edgesUsed === n) break;
}
}
return totalCost;
}
console.log(minCostToSupplyWater(3, [1, 2, 2], [[1, 2, 1], [2, 3, 1]])); // 3
Satisfiability of Equality Equations #
function equationsPossible(equations) {
const uf = new UnionFind(26);
// Process equality equations
for (const eq of equations) {
if (eq[1] === '=') {
const x = eq.charCodeAt(0) - 'a'.charCodeAt(0);
const y = eq.charCodeAt(3) - 'a'.charCodeAt(0);
uf.union(x, y);
}
}
// Check inequality equations
for (const eq of equations) {
if (eq[1] === '!') {
const x = eq.charCodeAt(0) - 'a'.charCodeAt(0);
const y = eq.charCodeAt(3) - 'a'.charCodeAt(0);
if (uf.find(x) === uf.find(y)) {
return false;
}
}
}
return true;
}
console.log(equationsPossible(['a==b', 'b!=a'])); // false
console.log(equationsPossible(['a==b', 'b==c', 'a==c'])); // true
Smallest String With Swaps #
function smallestStringWithSwaps(s, pairs) {
const n = s.length;
const uf = new UnionFind(n);
// Union indices that can be swapped
for (const [i, j] of pairs) {
uf.union(i, j);
}
// Group indices by root
const groups = new Map();
for (let i = 0; i < n; i++) {
const root = uf.find(i);
if (!groups.has(root)) {
groups.set(root, []);
}
groups.get(root).push(i);
}
// Sort characters in each group
const result = s.split('');
for (const indices of groups.values()) {
const chars = indices.map(i => s[i]).sort();
indices.sort((a, b) => a - b);
for (let i = 0; i < indices.length; i++) {
result[indices[i]] = chars[i];
}
}
return result.join('');
}
console.log(smallestStringWithSwaps('dcab', [[0, 3], [1, 2]])); // 'bacd'
console.log(smallestStringWithSwaps('dcab', [[0, 3], [1, 2], [0, 2]])); // 'abcd'
Most Stones Removed #
function removeStones(stones) {
const n = stones.length;
const uf = new UnionFind(n);
// Union stones in same row or column
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
if (stones[i][0] === stones[j][0] || stones[i][1] === stones[j][1]) {
uf.union(i, j);
}
}
}
return n - uf.getCount();
}
console.log(removeStones([[0, 0], [0, 1], [1, 0], [1, 2], [2, 1], [2, 2]])); // 5
Optimized Version with Coordinate Mapping #
function removeStones(stones) {
const uf = new Map();
function find(x) {
if (!uf.has(x)) {
uf.set(x, x);
}
if (uf.get(x) !== x) {
uf.set(x, find(uf.get(x)));
}
return uf.get(x);
}
function union(x, y) {
uf.set(find(x), find(y));
}
for (const [x, y] of stones) {
// Use ~y to distinguish from x coordinates
union(x, ~y);
}
const roots = new Set();
for (const [x, y] of stones) {
roots.add(find(x));
}
return stones.length - roots.size;
}Time Complexity #
| Operation | Without Optimization | With Path Compression | With Both |
|---|---|---|---|
| Find | O(n) | O(log n) | O(α(n)) |
| Union | O(n) | O(log n) | O(α(n)) |
| Space | O(n) | O(n) | O(n) |
α(n) = inverse Ackermann function (practically constant)
Best Practices #
- Always use path compression - Flatten tree on find
- Union by rank or size - Keep trees balanced
- Map for non-integer keys - Use coordinate or string keys
- Track component count - Decrement on successful union
- Check before union - Detect cycles
- Initialize properly - Each element is own parent
- Use for connectivity - Efficient for graph problems
Union Find is essential for graph connectivity problems. With optimizations, it achieves near-constant time operations.