Ý tưởng: Khởi tạo mỗi đỉnh là một thành phần riêng (count = n). Mỗi cạnh, union hai đầu; nếu chúng chưa cùng nhóm thì giảm count. Kết quả là số nhóm còn lại.
Hai tối ưu:
- Path compression: trong find, trỏ thẳng node về gốc → các lần sau gần như O(1).
- Union by rank/size: luôn gắn cây thấp vào cây cao → tránh cây bị thoái hóa thành chuỗi.
Kết hợp cả hai cho độ phức tạp gần như hằng số (α — hàm Ackermann ngược).
ts
function countComponents(n: number, edges: number[][]): number {
const parent = Array.from({ length: n }, (_, i) => i)
const rank = new Array(n).fill(1)
const find = (x: number): number => {
while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x] } // nén đường
return x
}
let count = n
for (const [a, b] of edges) {
const ra = find(a), rb = find(b)
if (ra === rb) continue
if (rank[ra] < rank[rb]) parent[ra] = rb
else if (rank[ra] > rank[rb]) parent[rb] = ra
else { parent[rb] = ra; rank[ra]++ }
count--
}
return count
}Độ phức tạp: ~O(V + E·α(V)) thời gian, O(V) bộ nhớ.
Lưu ý: Cũng giải được bằng DFS/BFS O(V+E); Union-Find vượt trội khi cạnh đến động (streaming) hoặc cần hỏi liên thông nhiều lần.