🧠
Union-Find (Disjoint Set Union)
💡 Union-Find is a powerful data structure to efficiently merge sets and find connected components without explicit graph traversal.
Intuition
Initialize each node as its own set. For each edge, union the sets of the two nodes. The number of unique parents after all unions is the number of connected components.
Algorithm
- Initialize parent array where each node is its own parent.
- Define find function with path compression to find root parent.
- Define union function to merge two sets if they have different parents.
- Iterate over edges and union their nodes.
- Count how many nodes are their own parent to get number of components.
💡 The key is understanding how union merges sets and find identifies the root parent efficiently.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0]*n
self.count = 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):
rootX = self.find(x)
rootY = self.find(y)
if rootX != rootY:
if self.rank[rootX] < self.rank[rootY]:
self.parent[rootX] = rootY
elif self.rank[rootX] > self.rank[rootY]:
self.parent[rootY] = rootX
else:
self.parent[rootY] = rootX
self.rank[rootX] += 1
self.count -= 1
def countComponents(n, edges):
uf = UnionFind(n)
for u, v in edges:
uf.union(u, v)
return uf.count
# Driver code
if __name__ == '__main__':
n = 5
edges = [[0,1],[1,2],[3,4]]
print(countComponents(n, edges)) # Output: 2
Line Notes
self.parent = list(range(n))Initialize each node as its own parent
self.rank = [0]*nRank array to keep tree shallow during union
def find(self, x)Find root parent with path compression
if self.parent[x] != xIf not root, recursively find root and compress path
def union(self, x, y)Union two sets if roots differ
if rootX != rootYOnly union if nodes belong to different sets
self.count -= 1Decrement count of components after successful union
for u, v in edgesUnion all connected nodes
return uf.countReturn number of connected components
public class NumberOfConnectedComponents {
static class UnionFind {
int[] parent, rank;
int count;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
count = n;
for (int i = 0; i < n; i++) parent[i] = i;
}
public int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] < rank[rootY]) parent[rootX] = rootY;
else if (rank[rootX] > rank[rootY]) parent[rootY] = rootX;
else {
parent[rootY] = rootX;
rank[rootX]++;
}
count--;
}
}
}
public static int countComponents(int n, int[][] edges) {
UnionFind uf = new UnionFind(n);
for (int[] edge : edges) {
uf.union(edge[0], edge[1]);
}
return uf.count;
}
public static void main(String[] args) {
int n = 5;
int[][] edges = {{0,1},{1,2},{3,4}};
System.out.println(countComponents(n, edges)); // Output: 2
}
}
Line Notes
parent = new int[n]Initialize parent array with each node as its own parent
rank = new int[n]Rank array to optimize union operations
if (parent[x] != x)Path compression in find to flatten tree
if (rootX != rootY)Union only if roots differ
count--Decrease component count after union
for (int[] edge : edges)Union all connected nodes
return uf.countReturn number of connected components
public static void main(String[] args)Driver code to test the solution
#include <iostream>
#include <vector>
using namespace std;
class UnionFind {
public:
vector<int> parent, rank;
int count;
UnionFind(int n) : parent(n), rank(n, 0), count(n) {
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unionSet(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] < rank[rootY]) parent[rootX] = rootY;
else if (rank[rootX] > rank[rootY]) parent[rootY] = rootX;
else {
parent[rootY] = rootX;
rank[rootX]++;
}
count--;
}
}
};
int countComponents(int n, vector<vector<int>>& edges) {
UnionFind uf(n);
for (auto& edge : edges) {
uf.unionSet(edge[0], edge[1]);
}
return uf.count;
}
int main() {
int n = 5;
vector<vector<int>> edges = {{0,1},{1,2},{3,4}};
cout << countComponents(n, edges) << endl; // Output: 2
return 0;
}
Line Notes
vector<int> parent, rankParent and rank arrays for union-find structure
UnionFind(int n) : parent(n), rank(n, 0), count(n)Initialize parent and rank arrays and count
for (int i = 0; i < n; i++) parent[i] = iEach node is its own parent initially
int find(int x)Find root parent with path compression
void unionSet(int x, int y)Union two sets if roots differ
if (rootX != rootY)Only union if nodes belong to different sets
count--Decrement count of connected components after union
for (auto& edge : edges)Union all connected nodes
return uf.countReturn number of connected components
class UnionFind {
constructor(n) {
this.parent = Array.from({length: n}, (_, i) => i);
this.rank = Array(n).fill(0);
this.count = n;
}
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) {
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--;
}
}
}
function countComponents(n, edges) {
const uf = new UnionFind(n);
for (const [u, v] of edges) {
uf.union(u, v);
}
return uf.count;
}
// Driver code
const n = 5;
const edges = [[0,1],[1,2],[3,4]];
console.log(countComponents(n, edges)); // Output: 2
Line Notes
this.parent = Array.from({length: n}, (_, i) => i)Initialize each node as its own parent
this.rank = Array(n).fill(0)Rank array to keep trees balanced
if (this.parent[x] !== x)Path compression in find to flatten tree
if (rootX !== rootY)Union only if roots differ
this.count--Decrease count of connected components after union
for (const [u, v] of edges)Union all connected nodes
return uf.countReturn number of connected components
console.log(countComponents(n, edges))Driver code to test the function
TimeO(α(n) * (n + e))
SpaceO(n)
Union-Find operations are almost O(1) amortized (α is inverse Ackermann function). We perform union/find for each edge and node.
💡 For n=1000 and e=2000, this is roughly 3000 near-constant time operations, very efficient.
Interview Verdict: Accepted
Union-Find is optimal for this problem and often preferred in interviews for its efficiency and elegance.