Thuật toán Kruskal
Cập nhật lần cuối
Thuật toán Kruskal xây dựng cây khung nhỏ nhất (minimum spanning tree, MST), tức là tập cạnh có tổng trọng số nhỏ nhất nối tất cả các đỉnh mà không tạo chu trình. Thuật toán sắp xếp mọi cạnh theo trọng số, rồi tham lam thêm cạnh rẻ nhất tiếp theo, miễn là nó nối hai đỉnh chưa được kết nối với nhau. Nhấn Phát ở trên để xem cây lớn dần, mỗi lần thêm một cạnh an toàn rẻ nhất.
Việc kiểm tra "đã được kết nối chưa?" dùng cấu trúc dữ liệu union-find (tập rời rạc, disjoint set): một cạnh bị bỏ qua nếu hai đầu mút của nó đã nằm trong cùng một thành phần, vì thêm nó vào sẽ tạo chu trình. Bước sắp xếp chiếm phần lớn chi phí, cho tổng độ phức tạp O(E log E). Kruskal phát huy thế mạnh trên đồ thị thưa.
Độ phức tạp thời gian và không gian
| Đại lượng | Độ phức tạp | Ghi chú |
|---|---|---|
| Thời gian | O(E log E) | Chủ yếu do sắp xếp các cạnh |
| Thao tác union-find | ≈ O(E α(V)) | Gần như hằng số mỗi lần kiểm tra nhờ nén đường đi |
| Không gian | O(V + E) | Danh sách cạnh và cấu trúc tập rời rạc |
| Phù hợp nhất với | Đồ thị thưa | Làm việc trên một danh sách cạnh chung |
Từng bước
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Sắp xếp mọi cạnh theo trọng số tăng dần. |
| 2 | Đặt mỗi đỉnh vào một thành phần riêng (union-find). |
| 3 | Lấy cạnh rẻ nhất tiếp theo. |
| 4 | Nếu hai đầu mút nằm ở hai thành phần khác nhau, thêm cạnh vào cây và hợp nhất hai thành phần. |
| 5 | Nếu không, bỏ qua cạnh đó (nó sẽ tạo chu trình). |
| 6 | Dừng khi cây có V − 1 cạnh. |
Ví dụ minh họa
Đồ thị có các đỉnh A, B, C, D và các cạnh A-B(1), B-C(2), A-C(3), C-D(4), B-D(5). Các cạnh sau khi sắp xếp: A-B(1), B-C(2), A-C(3), C-D(4), B-D(5):
| Cạnh (trọng số) | Các thành phần trước đó | Thao tác |
|---|---|---|
A-B(1) | {A} {B} {C} {D} | Khác thành phần: thêm vào MST, hợp nhất thành {A,B}. |
B-C(2) | {A,B} {C} {D} | Khác thành phần: thêm vào MST, hợp nhất thành {A,B,C}. |
A-C(3) | {A,B,C} {D} | A và C đã cùng thành phần: bỏ qua (sẽ tạo chu trình). |
C-D(4) | {A,B,C} {D} | Khác thành phần: thêm vào MST, hợp nhất thành {A,B,C,D}. |
| Dừng | {A,B,C,D} | Cây có V - 1 = 3 cạnh. MST = A-B, B-C, C-D, tổng trọng số 7. |
Khi nào nên dùng thuật toán Kruskal
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Đồ thị thưa (ít cạnh so với số đỉnh). | Đồ thị dày: Prim dùng heap thường nhanh hơn. |
| Các cạnh đã có sẵn dưới dạng một danh sách chung để sắp xếp. | Các cạnh chỉ có trong danh sách kề và phải duyệt theo từng đỉnh. |
| Bạn muốn một cách cài đặt đơn giản dựa trên union-find. | Bạn cần cây mọc ra từ một đỉnh xuất phát cụ thể. |
| Bạn muốn xây dựng rừng khung của một đồ thị không liên thông. | Bạn phải xử lý các cạnh đến liên tục mà không sắp xếp toàn bộ. |
Code Kruskal's Algorithm
Một cách cài đặt Kruskal's Algorithm gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Kruskal's Algorithm bằng Python
1def find(parent, x):2 while parent[x] != x:3 parent[x] = parent[parent[x]] # path compression4 x = parent[x]5 return x6
7
8def kruskal(vertices, edges):9 # Take the cheapest edge that does not close a cycle10 parent = {v: v for v in vertices}11 mst, total = [], 012 for w, u, v in sorted(edges):13 root_u, root_v = find(parent, u), find(parent, v)14 if root_u != root_v:15 parent[root_u] = root_v16 mst.append((u, v, w))17 total += w18 return mst, total19
20
21vertices = ["A", "B", "C", "D", "E"]22edges = [23 (4, "A", "B"), (1, "A", "C"), (3, "B", "C"), (2, "B", "D"),24 (5, "C", "D"), (6, "C", "E"), (7, "D", "E"),25]26
27mst, total = kruskal(vertices, edges)28for u, v, w in mst:29 print(f"{u} - {v} (weight {w})")30print("Total MST weight:", total)Code Kruskal's Algorithm bằng JavaScript
1const nodes = ["A", "B", "C", "D", "E"];2const edges = [3 ["A", "B", 4], ["A", "C", 2], ["B", "C", 1], ["B", "D", 5],4 ["C", "D", 8], ["C", "E", 10], ["D", "E", 2],5];6
7function kruskal() {8 // Union-find with path compression detects cycles cheaply9 const parent = Object.fromEntries(nodes.map((n) => [n, n]));10 const find = (x) => (parent[x] === x ? x : (parent[x] = find(parent[x])));11 const mst = [];12 let total = 0;13 const sorted = [...edges].sort((a, b) => a[2] - b[2]);14 for (const [u, v, w] of sorted) {15 const rootU = find(u);16 const rootV = find(v);17 if (rootU !== rootV) {18 parent[rootU] = rootV;19 mst.push(`${u}-${v} (${w})`);20 total += w;21 }22 }23 return { mst, total };24}25
26const { mst, total } = kruskal();27console.log("MST edges:", mst.join(", "));28console.log("Total weight:", total);Code Kruskal's Algorithm bằng Java
1import java.util.Arrays;2
3public class Main {4 static int[] parent;5
6 static int find(int x) {7 if (parent[x] != x) parent[x] = find(parent[x]); // path compression8 return parent[x];9 }10
11 static boolean union(int a, int b) {12 int rootA = find(a), rootB = find(b);13 if (rootA == rootB) return false; // would create a cycle14 parent[rootA] = rootB;15 return true;16 }17
18 public static void main(String[] args) {19 int n = 6;20 int[][] edges = {21 {0, 1, 4}, {0, 2, 3}, {1, 2, 1}, {1, 3, 2},22 {2, 3, 4}, {3, 4, 2}, {4, 5, 6}, {2, 5, 7}23 };24 Arrays.sort(edges, (a, b) -> a[2] - b[2]);25 parent = new int[n];26 for (int i = 0; i < n; i++) parent[i] = i;27
28 int total = 0, used = 0;29 for (int[] e : edges) {30 if (union(e[0], e[1])) {31 System.out.println("Take edge " + e[0] + "-" + e[1] + " (weight " + e[2] + ")");32 total += e[2];33 if (++used == n - 1) break;34 }35 }36 System.out.println("MST total weight: " + total);37 }38}Code Kruskal's Algorithm bằng C++
1#include <algorithm>2#include <iostream>3#include <numeric>4#include <vector>5
6struct Edge {7 int u, v, weight;8};9
10struct DSU {11 std::vector<int> parent;12 explicit DSU(int n) : parent(n) {13 std::iota(parent.begin(), parent.end(), 0);14 }15 int find(int x) {16 if (parent[x] != x) parent[x] = find(parent[x]); // path compression17 return parent[x];18 }19 bool unite(int a, int b) {20 int ra = find(a), rb = find(b);21 if (ra == rb) return false; // would create a cycle22 parent[ra] = rb;23 return true;24 }25};26
27int main() {28 const int n = 5;29 std::vector<Edge> edges = {30 {0, 1, 4}, {0, 2, 2}, {1, 2, 1}, {1, 3, 5},31 {2, 3, 8}, {2, 4, 10}, {3, 4, 3},32 };33 // Greedily take the cheapest edge that connects two components34 std::sort(edges.begin(), edges.end(),35 [](const Edge& a, const Edge& b) { return a.weight < b.weight; });36 DSU dsu(n);37 int total = 0;38 std::cout << "MST edges:\n";39 for (const Edge& e : edges) {40 if (dsu.unite(e.u, e.v)) {41 std::cout << " " << e.u << " - " << e.v42 << " (weight " << e.weight << ")\n";43 total += e.weight;44 }45 }46 std::cout << "Total weight: " << total << "\n";47 return 0;48}Code Kruskal's Algorithm bằng C
1#include <stdbool.h>2#include <stdio.h>3#include <stdlib.h>4
5#define N 56
7typedef struct {8 int u, v, weight;9} Edge;10
11int parent[N];12
13int find(int x) {14 if (parent[x] != x) parent[x] = find(parent[x]); // path compression15 return parent[x];16}17
18bool unite(int a, int b) {19 int ra = find(a), rb = find(b);20 if (ra == rb) return false; // would create a cycle21 parent[ra] = rb;22 return true;23}24
25int cmpEdge(const void* a, const void* b) {26 return ((const Edge*)a)->weight - ((const Edge*)b)->weight;27}28
29int main(void) {30 Edge edges[] = {31 {0, 1, 4}, {0, 2, 2}, {1, 2, 1}, {1, 3, 5},32 {2, 3, 8}, {2, 4, 10}, {3, 4, 3},33 };34 int m = sizeof(edges) / sizeof(edges[0]);35 for (int i = 0; i < N; i++) parent[i] = i;36 // Greedily take the cheapest edge that connects two components37 qsort(edges, m, sizeof(Edge), cmpEdge);38 int total = 0;39 printf("MST edges:\n");40 for (int i = 0; i < m; i++) {41 if (unite(edges[i].u, edges[i].v)) {42 printf(" %d - %d (weight %d)\n",43 edges[i].u, edges[i].v, edges[i].weight);44 total += edges[i].weight;45 }46 }47 printf("Total weight: %d\n", total);48 return 0;49}Câu hỏi thường gặp về thuật toán Kruskal
Cây khung nhỏ nhất là gì?
V đỉnh, nó có đúng V - 1 cạnh.Thuật toán Kruskal và Prim khác nhau như thế nào?
Cả hai đều xây dựng cây khung nhỏ nhất theo kiểu tham lam. Kruskal sắp xếp mọi cạnh trên toàn đồ thị và thêm cạnh rẻ nhất không tạo chu trình, dùng union-find. Prim cho một cây duy nhất mọc ra từ đỉnh xuất phát, luôn thêm cạnh rẻ nhất đi ra khỏi cây. Kruskal hợp với đồ thị thưa; Prim (dùng heap) hợp với đồ thị dày.
Tại sao thuật toán Kruskal dùng union-find?
Trước khi thêm một cạnh, Kruskal phải kiểm tra hai đầu mút của nó đã được kết nối chưa, vì thêm cạnh như vậy sẽ tạo chu trình. Union-find (tập rời rạc) trả lời câu hỏi "hai đỉnh này có cùng thành phần không?" và hợp nhất các thành phần trong thời gian khấu hao gần như hằng số, giúp thuật toán luôn hiệu quả.
Khi nào nên dùng thuật toán Kruskal thay vì Prim?
O(E log E) rẻ khi E nhỏ. Thuật toán Prim dùng binary heap hoặc Fibonacci heap thường thắng trên đồ thị dày, khi E tiến gần V², vì nó không phải sắp xếp mọi cạnh ngay từ đầu.Thuật toán Kruskal có chạy được trên đồ thị không liên thông không?
V - 1 cạnh và thay vào đó tạo ra một rừng khung nhỏ nhất, mỗi thành phần liên thông có một MST. Điều này xảy ra tự nhiên vì union-find không bao giờ hợp nhất những đỉnh không có đường đi giữa chúng.Lỗi thường gặp nhất khi cài đặt thuật toán Kruskal là gì?
Bỏ qua kỹ thuật nén đường đi (path compression) và hợp nhất theo hạng (union by rank) của union-find, khiến mỗi lần kiểm tra kết nối từ gần như hằng số trở thành gần như tuyến tính và có thể chiếm phần lớn thời gian chạy. Một bug phổ biến khác là quên hợp nhất hai thành phần sau khi thêm cạnh, khiến các cạnh sau có thể tạo chu trình.