Minimum Spanning Tree — Kruskal vs Prim, cut property
Kết nối 100 datacenter với chi phí tối thiểu, không vòng lặp — đó là bài toán MST. Bài này dạy Kruskal (sort edges + Disjoint Set Union) và Prim (grow tree với PriorityQueue), cut property chứng minh correctness, và khi nào chọn thuật toán nào.
Một network engineer cần kết nối 100 datacenter bằng cáp quang. Mỗi cặp datacenter có chi phí lắp đặt khác nhau — distance, terrain, licensing. Mục tiêu: tìm tập cáp với tổng chi phí nhỏ nhất sao cho mọi datacenter đều được kết nối với nhau.
Thêm cáp thừa (tạo vòng lặp) là lãng phí — nếu datacenter A và B đã kết nối qua C, thêm cáp trực tiếp A–B chỉ tốn thêm tiền mà không tăng connectivity. Cấu hình tối ưu là một tree — connected, acyclic, đúng V-1 cạnh cho V đỉnh.
Bài này dạy 2 algorithm tìm Minimum Spanning Tree: Kruskal (sort edges và dùng Disjoint Set Union — preview lesson 09), Prim (grow tree với PriorityQueue), khi nào chọn cái nào, và cut property làm nền tảng correctness cho cả hai.
1. Analogy — Thiết kế đường dây điện
Có N ngôi nhà cần điện. Kỹ sư đo chi phí rải dây giữa từng cặp nhà — khoảng cách, địa hình, vật cản. Cần tìm cách rải dây với tổng chi phí nhỏ nhất sao cho mọi nhà đều có điện.
Nếu rải dây tạo vòng lặp — nhà A kết nối với B qua cả đường thẳng lẫn đường vòng qua C — thì đường vòng hoàn toàn thừa. Cấu hình tối ưu: N nhà, N-1 đường dây, không vòng lặp, mọi nhà kết nối. Đây chính xác là spanning tree.
| Thiết kế đường điện | MST |
|---|---|
| Ngôi nhà | Vertex |
| Đường dây giữa hai nhà | Edge |
| Chi phí rải dây | Edge weight |
| Mọi nhà có điện | Connected graph |
| Không vòng lặp thừa | Acyclic (tree) |
| Tổng chi phí nhỏ nhất | Minimum total weight |
MST = spanning tree (kết nối mọi vertex, V-1 edge, không cycle) với tổng weight nhỏ nhất. Không tối ưu từng đường đi riêng lẻ — tối ưu tổng chi phí toàn bộ cây.
2. Định nghĩa MST
Cho graph G = (V, E) connected, undirected, weighted:
- Spanning tree: tập con E' của E sao cho tất cả V vertex kết nối với nhau, đúng V-1 edges, không cycle.
- MST (Minimum Spanning Tree): spanning tree với tổng weight nhỏ nhất.
Số lượng MST:
- Nếu mọi edge weight khác nhau → MST là duy nhất.
- Nếu có tie (edge weight bằng nhau) → có thể tồn tại nhiều MST có cùng tổng weight tối thiểu.
Lưu ý quan trọng: MST tối ưu tổng weight toàn cây, không tối ưu từng shortest path riêng lẻ giữa các cặp vertex. Đây là điểm khác biệt cốt lõi so với Dijkstra.
3. Cut property — nền tảng correctness
Cut là cách chia tập vertex V thành 2 disjoint set: S và V-S (mọi vertex thuộc đúng một trong hai set).
Cut edge là edge có một endpoint trong S và một endpoint trong V-S — nghĩa là edge này "bắc cầu" qua cut.
Cut property: với bất kỳ cut (S, V-S) nào, edge có weight nhỏ nhất qua cut đó phải thuộc ít nhất một MST.
Tại sao? Gọi edge đó là e. Giả sử e không thuộc MST T. Thêm e vào T tạo ra đúng một cycle — cycle đó phải chứa ít nhất một edge f khác qua cut này (vì cycle phải thoát ra và quay vào S). Do e là edge nhỏ nhất qua cut: weight(e) <= weight(f). Thay f bằng e trong T cho spanning tree T' với tổng weight không tăng — mâu thuẫn với T là MST (hoặc T' cũng là MST nếu tie).
Ý nghĩa thực tế: cả Kruskal và Prim đều hoạt động bằng cách lặp lại cut property — tại mỗi bước, pick edge nhỏ nhất qua một cut nào đó, đảm bảo edge đó thuộc MST.
4. Kruskal algorithm
4.1 Ý tưởng
Kruskal xây MST theo góc nhìn edge-centric:
- Sort tất cả edge theo weight ascending.
- Iterate qua từng edge (từ nhỏ đến lớn): thêm edge vào MST nếu hai endpoint chưa nằm trong cùng một component (tức là thêm edge này không tạo cycle).
- Dừng khi đã có V-1 edges.
Bước 2 — "hai endpoint có trong cùng component không?" — được giải bằng Disjoint Set Union (DSU). DSU sẽ được phân tích chi tiết ở lesson 09; ở đây dùng như black box với hai operation: find(x) trả root của component chứa x, union(x, y) gộp hai component.
4.2 Code Java
public class Kruskal {
record Edge(int u, int v, int weight) {}
public static List<Edge> mst(int n, List<Edge> edges) {
// Sort all edges by weight ascending
edges.sort(Comparator.comparingInt(Edge::weight));
DisjointSet dsu = new DisjointSet(n);
List<Edge> result = new ArrayList<>();
for (Edge e : edges) {
// Only add edge if it connects two different components (no cycle)
if (dsu.union(e.u(), e.v())) {
result.add(e);
if (result.size() == n - 1) break; // MST complete
}
}
return result; // result.size() < n-1 means graph is disconnected
}
static class DisjointSet {
int[] parent;
int[] rank;
DisjointSet(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
}
// Find root with path compression
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
// Union by rank -- returns true if merged, false if already same component
boolean union(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return false; // same component -- adding would create cycle
if (rank[rx] < rank[ry]) { int tmp = rx; rx = ry; ry = tmp; }
parent[ry] = rx;
if (rank[rx] == rank[ry]) rank[rx]++;
return true;
}
}
}
4.3 Complexity
- Time: O(E log E) để sort + O(E × α(V)) cho DSU operations ≈ O(E log V). Hàm α là inverse Ackermann — tăng cực kỳ chậm, thực tế coi như hằng số.
- Space: O(V) cho DSU, O(E) để lưu edges.
4.4 Trace ví dụ
Graph 4 vertex, 5 edges:
0 ---(1)--- 1
| \ |
(4) (3) (2)
| \ |
3 ---(5)--- 2
Edges sorted: (0,1,1), (1,2,2), (0,2,3), (0,3,4), (2,3,5).
| Bước | Edge | DSU state | Kết quả |
|---|---|---|---|
| 1 | (0,1) w1 | find(0)=0, find(1)=1 → khác → union | Thêm. MST: 1 |
| 2 | (1,2) w2 | find(1)=0, find(2)=2 → khác → union | Thêm. MST: 2 |
| 3 | (0,2) w3 | find(0)=0, find(2)=0 → cùng | Bỏ qua (cycle) |
| 4 | (0,3) w4 | find(0)=0, find(3)=3 → khác → union | Thêm. MST: 3 |
V-1 = 3 edges đã có. Dừng. Tổng weight = 1+2+4 = 7.
5. Prim algorithm
5.1 Ý tưởng
Prim xây MST theo góc nhìn vertex-centric:
- Bắt đầu từ một vertex bất kỳ (vertex 0).
- Maintain
PriorityQueuechứa các edge từ "tree-set" (các vertex đã thuộc MST) ra "non-tree" (các vertex chưa thuộc MST). - Lặp: poll edge nhỏ nhất từ PQ; nếu endpoint đích chưa trong tree — thêm vertex đó vào tree, thêm edge vào MST, đẩy tất cả edge từ vertex mới ra các neighbor chưa trong tree vào PQ.
- Dừng khi tất cả vertex trong tree.
Đây là cut property applied trực tiếp: tại mỗi bước, cut là (tree-set, V - tree-set), và Prim luôn pick edge nhỏ nhất qua cut đó.
5.2 Code Java
public class Prim {
record Edge(int from, int to, int weight) {}
public static List<Edge> mst(int n, List<List<Edge>> adj) {
boolean[] inTree = new boolean[n];
// Min-heap by edge weight
PriorityQueue<Edge> pq = new PriorityQueue<>(
Comparator.comparingInt(Edge::weight)
);
List<Edge> result = new ArrayList<>();
// Start from vertex 0
inTree[0] = true;
for (Edge e : adj.get(0)) pq.offer(e);
while (!pq.isEmpty() && result.size() < n - 1) {
Edge e = pq.poll();
if (inTree[e.to()]) continue; // endpoint already in tree -- skip (lazy delete)
// Add this vertex to the tree
inTree[e.to()] = true;
result.add(e);
// Offer edges from new vertex to non-tree neighbors
for (Edge next : adj.get(e.to())) {
if (!inTree[next.to()]) pq.offer(next);
}
}
return result;
}
}
5.3 Complexity
- Time: O((V + E) log V) với binary heap. Mỗi trong V vertex được thêm vào tree một lần; mỗi trong E edge có thể được push vào PQ một lần (O(log E) = O(log V) per push).
- Space: O(V) cho
inTree, O(E) worst-case cho PQ (lazy deletion).
5.4 Trace ví dụ
Cùng graph 4 vertex 5 edges như trên.
| Bước | PQ poll | inTree | Kết quả |
|---|---|---|---|
| Init | — | 0 | PQ: [(0→1,1),(0→2,3),(0→3,4)] |
| 1 | (0→1, w1) | 1 | Thêm. Push 1→2 w2. PQ: [(1→2,2),(0→2,3),(0→3,4)] |
| 2 | (1→2, w2) | 2 | Thêm. Push 2→3 w5. PQ: [(0→2,3),(0→3,4),(2→3,5)] |
| 3 | (0→2, w3) | — | vertex 2 đã trong tree → skip |
| 4 | (0→3, w4) | 3 | Thêm. PQ: [(2→3,5)] |
MST: 4, tổng weight = 7. Kết quả giống Kruskal.
6. Kruskal vs Prim — khi nào chọn gì
| Aspect | Kruskal | Prim |
|---|---|---|
| Approach | Edge-sorted + DSU | Vertex-grow + PQ |
| Time complexity | O(E log V) | O((V+E) log V) |
| Space | O(V) DSU | O(V + E) PQ |
| Tốt cho | Sparse graph (E nhỏ) | Dense graph (E gần V²) |
| Disconnected graph | Trả minimum spanning forest | Chỉ xử lý 1 component từ vertex 0 |
| Input format tự nhiên | Edge list | Adjacency list |
Khi nào Kruskal thắng Prim?
Với sparse graph (E nhỏ hơn nhiều so với V²), O(E log V) của Kruskal tốt hơn O((V+E) log V) của Prim. Ví dụ: V = 1000, E = 2000 (sparse). Kruskal ≈ 2000 × 10 = 20.000 operations chủ yếu. Prim ≈ (1000 + 2000) × 10 = 30.000 — chậm hơn một chút, nhưng không đáng kể ở scale này.
Khi nào Prim thắng Kruskal?
Dense graph (E gần V²). Với E = V², Kruskal sort E edges = V² log V operations. Prim vẫn O((V + V²) log V) ≈ O(V² log V) — tương đương. Nhưng Prim có thể được optimize thêm bằng adjacency matrix thay PQ, đạt O(V²) cho dense graph — thắng Kruskal rõ ràng.
7. Pitfall tổng hợp
Pitfall 1 — Kruskal trên disconnected graph trả spanning forest, không phải spanning tree:
// Graph has 2 components: {0,1,2} and {3,4}
// Kruskal will return edges connecting each component separately
List<Edge> result = Kruskal.mst(5, edges);
// result.size() = 3, not 4 (n-1)
// This is a minimum spanning FOREST -- correct for disconnected input,
// but caller must check result.size() == n-1 to detect disconnected graph
if (result.size() < n - 1) {
throw new IllegalArgumentException("Graph is not connected");
}
Prim bắt đầu từ vertex 0 và chỉ xử lý component chứa vertex 0 — các component khác hoàn toàn bị bỏ qua.
Pitfall 2 — Prim PQ chứa duplicate entries:
// Multiple edges may reach the same vertex -- all get pushed to PQ
// When we expand vertex u, edges (u->v1), (u->v2) are pushed
// If v1 was already reachable from earlier, PQ has duplicate entries for v1
// CORRECT pattern: check inTree[e.to()] when POLLING, not when pushing
Edge e = pq.poll();
if (inTree[e.to()]) continue; // lazy delete -- safe to skip
Đây là cùng pattern "lazy deletion" như Dijkstra (lesson 05). Push nhiều entries vào PQ là bình thường và an toàn — chỉ cần kiểm tra khi poll. PQ có thể chứa tới O(E) entries — không ảnh hưởng correctness, chỉ tốn thêm space.
Pitfall 3 — Nhầm MST với shortest path tree (Dijkstra):
// MST: minimize TOTAL weight of the tree
// Shortest path tree: minimize distance from source to EACH vertex individually
//
// Counter-example (4 vertices):
// Edges: 0->1 w1, 0->2 w3, 1->3 w1, 2->3 w1
//
// MST: {(0,1,1), (1,3,1), (2,3,1)} -- total weight 3
// (connects 0-1-3-2, total cost 3)
//
// Dijkstra shortest path tree from vertex 0:
// dist[0]=0, dist[1]=1, dist[2]=3, dist[3]=2
// Tree edges: {(0,1,1), (1,3,1), (0,2,3)} -- total weight 5
// Path 0->2 goes directly (cost 3), not via 3->2 (cost 1+1+1=3, same but different tree)
//
// Key: MST edge (2,3,1) means "connect vertex 2 cheapest to the tree overall"
// Dijkstra edge (0,2,3) means "reach vertex 2 from source 0 with minimum cost"
// These are DIFFERENT objectives.
Đừng dùng Dijkstra để tìm MST — chúng giải hai bài toán khác nhau.
8. Variants và mở rộng
Borůvka's algorithm (1926): thuật toán MST đầu tiên được phát minh, trước cả Kruskal và Prim. Mỗi vòng lặp: mỗi component tìm edge nhỏ nhất ra ngoài component đó và thêm tất cả cùng lúc. Số component giảm xuống một nửa sau mỗi vòng → O(E log V) total. Đặc biệt phù hợp cho distributed MST — mỗi component có thể tìm min edge song song, không cần coordinator trung tâm.
Kruskal với lazy sort: thay vì sort toàn bộ edge list một lần, dùng min-heap để lấy edge nhỏ nhất theo từng bước. Useful khi chỉ cần một phần MST hoặc khi edge list quá lớn để sort toàn bộ cùng lúc.
MST verification: cho trước một spanning tree T, kiểm tra T có phải MST của G không trong O(V+E) — không cần chạy Kruskal/Prim lại. Dùng kỹ thuật cycle property: với mỗi non-tree edge e, nếu weight(e) nhỏ hơn max weight trên path trong T giữa hai endpoint của e, thì T không phải MST. Linear verification algorithm của Dixon, Rauch, Tarjan (1992).
9. Ứng dụng thực tế
Network design — telco và datacenter: bài toán gốc của MST. Thiết kế mạng lưới cáp quang kết nối N điểm với chi phí tổng thể nhỏ nhất. Amazon AWS dùng MST-based algorithm để lên kế hoạch đặt cáp giữa các availability zone. Điều kiện thực tế phức tạp hơn (reliability, redundancy) — MST là baseline, sau đó thêm constraints.
Cluster analysis trong machine learning: hierarchical clustering với single-link method tương đương xây MST trên distance graph, rồi cắt bỏ các edge có weight lớn nhất. Số cluster = số cut. Scikit-learn AgglomerativeClustering(linkage='single') dùng MST internally cho large datasets.
Image segmentation trong computer vision: treat mỗi pixel là vertex, edge weight là difference giữa hai pixel liền kề. MST trên pixel graph xác định vùng ảnh liên kết theo màu/độ sáng. Felzenszwalb-Huttenlocher algorithm (2004) dùng MST variant để segment ảnh thời gian thực.
Approximation cho Traveling Salesman Problem: MST là lower bound cho TSP — bất kỳ TSP tour nào cũng có weight ít nhất bằng MST weight. Doubled MST (đi cạnh MST 2 lần) cho upper bound TSP × 2. Christofides algorithm (1976): MST + minimum weight perfect matching trên odd-degree vertices → tour với weight tối đa 1.5 × optimal TSP. Đây là approximation ratio tốt nhất đã biết cho general metric TSP trong nhiều thập kỷ.
10. Deep Dive
Bài báo gốc:
- Kruskal, J.B. (1956) — "On the shortest spanning subtree of a graph and the traveling salesman problem", Proceedings of the AMS: bài báo 2 trang giới thiệu Kruskal's algorithm và kết nối với TSP.
- Prim, R.C. (1957) — "Shortest connection networks and some generalizations", Bell System Technical Journal: Prim's algorithm, tập trung vào dense graph và ứng dụng thiết kế mạng điện thoại.
- Borůvka, O. (1926) — "O jistém problému minimálním" (tiếng Czech): thuật toán MST đầu tiên, phát minh cho bài toán thiết kế mạng điện ở Moravia — 30 năm trước Kruskal và Prim.
Sách tham khảo:
- Introduction to Algorithms (CLRS), Chapter 23 — Minimum Spanning Trees: chứng minh đầy đủ cut property, loop property, correctness của Kruskal và Prim.
Cross-link trong khóa học:
- Module 5, Lesson 09 — Disjoint Set Union: chi tiết path compression, union by rank, proof O(α(n)) amortized — component mà Kruskal dùng như black box ở bài này.
- Module 2, Lesson 06 — PriorityQueue: binary heap mà Prim phụ thuộc.
11. Tóm tắt
- MST là spanning tree (kết nối mọi vertex, V-1 edges, không cycle) với tổng edge weight nhỏ nhất. Unique nếu mọi weight khác nhau.
- Cut property: với bất kỳ cut (S, V-S) nào, edge nhỏ nhất qua cut phải thuộc ít nhất một MST — đây là nền tảng correctness cho cả Kruskal và Prim.
- Kruskal: sort tất cả edges tăng dần, thêm edge nếu không tạo cycle (dùng DSU), O(E log V). Tốt cho sparse graph và edge-list input.
- Prim: grow tree từ một vertex, PQ chứa edges ra ngoài tree, luôn pick edge nhỏ nhất, O((V+E) log V). Tốt cho dense graph và adjacency-list input.
- Lazy deletion trong Prim: push edge nhiều lần vào PQ an toàn — kiểm tra
inTree[e.to()]khi poll, không khi push. Tương tự pattern Dijkstra. - Disconnected graph: Kruskal trả minimum spanning forest (result.size() nhỏ hơn V-1); Prim chỉ xử lý component của vertex 0.
- MST khác shortest path tree: MST tối ưu tổng weight toàn cây; Dijkstra tối ưu khoảng cách từ source đến từng vertex riêng lẻ — hai mục tiêu khác nhau.
- Ứng dụng: network design, ML clustering, image segmentation, TSP approximation (Christofides 1.5×).
12. Tự kiểm tra
Q1Cut property — vì sao edge nhỏ nhất qua một cut đảm bảo thuộc MST? Tại sao điều này chứng minh correctness cho cả Kruskal và Prim?▸
Cut property: với cut (S, V-S), gọi e là edge nhỏ nhất qua cut. Nếu e không thuộc MST T, thêm e vào T tạo đúng một cycle — cycle đó phải chứa edge f khác qua cùng cut. Vì weight(e) <= weight(f), thay f bằng e cho spanning tree có tổng weight không tăng — mâu thuẫn với T là MST tối ưu.
Kruskal correctness: mỗi edge được thêm là edge nhỏ nhất giữa hai component khác nhau — tương đương edge nhỏ nhất qua cut (component này, các component còn lại). Prim correctness: tại mỗi bước, cut là (tree-set, V - tree-set), và Prim luôn poll edge nhỏ nhất qua cut đó từ PQ. Cả hai đều áp dụng cut property lặp đi lặp lại cho đến khi MST hoàn chỉnh.
Q2Kruskal O(E log V) vs Prim O((V+E) log V) — với sparse graph (E nhỏ hơn nhiều so với V²) và dense graph (E gần V²), nên chọn thuật toán nào? Ước lượng số operations.▸
Sparse graph (V=1000, E=2000): Kruskal ≈ E log V = 2000 × 10 = 20.000 operations chính. Prim ≈ (V+E) log V = 3000 × 10 = 30.000. Kruskal nhỉnh hơn — nhưng chênh lệch nhỏ. Kruskal thực sự tỏa sáng khi E rất nhỏ so với V.
Dense graph (V=1000, E=400.000 ≈ V²/2): Kruskal ≈ E log E = 400.000 × 19 ≈ 7.6 triệu. Prim ≈ (V+E) log V = 401.000 × 10 ≈ 4 triệu — Prim nhanh hơn gần 2 lần. Thêm vào đó, Prim với adjacency matrix (không dùng PQ) đạt O(V²) = 1 triệu cho dense graph, thắng rõ ràng.
Q3Disconnected graph — Kruskal và Prim xử lý khác nhau thế nào? Làm sao detect graph disconnected?▸
Kruskal: tự nhiên trả minimum spanning forest — tập các MST của từng connected component. Kết thúc khi hết edges, không phải khi có đủ V-1 edges. Detect disconnect: sau khi chạy xong, kiểm tra result.size() == n - 1. Nếu nhỏ hơn, graph có nhiều hơn một connected component.
Prim: chỉ explore component chứa vertex 0 (hoặc vertex bắt đầu). Các component khác hoàn toàn bị bỏ qua vì PQ không bao giờ có edge tới chúng. Detect disconnect: result.size() < n - 1 sau khi PQ rỗng. Để tìm MST của toàn bộ forest với Prim, cần chạy Prim nhiều lần — một lần cho mỗi unvisited vertex.
Q4Prim PQ có nhiều duplicate entries — vì sao push edge nhiều lần vào PQ vẫn an toàn? So sánh với lazy deletion trong Dijkstra.▸
Khi expand vertex u, tất cả edges từ u tới neighbor chưa trong tree được push vào PQ. Nếu sau đó vertex v được thêm vào tree qua con đường khác, các entries (u→v, weight) cũ trong PQ trở thành "stale". Khi poll entry đó: if (inTree[e.to()]) continue — skip ngay, không xử lý tiếp.
Pattern giống hệt lazy deletion trong Dijkstra: thay vì xóa entry khỏi PQ (JDK PriorityQueue không support decrease-key), ta push entry mới và kiểm tra validity khi poll. Correctness vì mọi vertex chỉ được thêm vào tree một lần — mọi entry stale đều bị skip. Cost: PQ có thể chứa O(E) entries thay O(V), nhưng log E = O(log V) nên complexity tổng không thay đổi.
Q5Cho graph 4 vertex: edges (0,1,w2), (0,2,w4), (1,2,w1), (1,3,w5), (2,3,w3). MST và shortest path tree từ vertex 0 có khác nhau không? Giải thích sự khác biệt.▸
MST (Kruskal): sort edges: (1,2,w1),(0,1,w2),(2,3,w3),(0,2,w4),(1,3,w5). Thêm (1,2,w1) — không cycle. Thêm (0,1,w2) — không cycle. Thêm (2,3,w3) — không cycle. V-1=3 edges xong. MST = { (1,2,1), (0,1,2), (2,3,3) }, tổng = 6.
Shortest path tree từ 0 (Dijkstra): dist[0]=0, dist[1]=2, dist[2]=3 (qua 1), dist[3]=6 (qua 1→2→3). Tree edges: { (0,1,2), (1,2,1), (2,3,3) }, tổng = 6. Trường hợp này kết quả trùng nhau về edge set. Nhưng mục tiêu khác: MST tối ưu tổng chi phí kết nối toàn bộ graph; Dijkstra tối ưu khoảng cách từ source 0 tới từng vertex. Trên nhiều graph khác hai kết quả sẽ khác nhau — đặc biệt khi có edge nối thẳng với weight lớn từ source mà MST không cần.
Q6DSU trong Kruskal — path compression và union by rank giúp gì cho complexity? Nếu không dùng hai optimization này thì sao?▸
Không có optimization: find(x) phải đi lên từng bước đến root — worst case O(V) nếu cây DSU degenerates thành linked list (ví dụ luôn union theo một chiều nhất định). Tổng E lần union × O(V) mỗi lần = O(V × E) chỉ cho DSU operations — toàn bộ Kruskal thành O(V × E + E log E), chậm hơn nhiều.
Path compression: khi find(x) đi lên root, gán trực tiếp parent[x] = root cho mọi node trên đường đi. Các lần find sau trên cùng node mất O(1). Union by rank: luôn attach cây thấp hơn vào cây cao hơn — giữ chiều cao cây O(log V). Kết hợp hai optimization: amortized O(α(V)) per operation, α là inverse Ackermann — tăng cực kỳ chậm (α(10^80) = 4), thực tế coi như O(1). Toàn bộ Kruskal đạt O(E log V).
Bài tiếp theo: Disjoint Set Union — path compression, union by rank
Bài này có giúp bạn hiểu bản chất không?