Thuật toán Prim
Cập nhật lần cuối
Thuật toán Prim 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 mọi đỉnh mà không tạo chu trình, bằng cách cho một cây duy nhất mọc dần ra từ một đỉnh xuất phát. Ở mỗi bước, thuật toán xem xét mọi cạnh đi từ cây sang một đỉnh nằm ngoài cây và thêm cạnh rẻ nhất. Nhấn Phát ở trên để xem cây mở rộng, mỗi lần lấy cạnh có trọng số nhỏ nhất dẫn đến một đỉnh mới.
Vì luôn chọn cạnh cắt nhỏ nhất, mỗi lần thêm đều an toàn (chắc chắn thuộc về một MST nào đó). Với hàng đợi ưu tiên dùng binary heap, lưu cạnh rẻ nhất đến mỗi đỉnh bên ngoài, Prim chạy trong O(E log V). Thuật toán này khác với Kruskal, vốn sắp xếp mọi cạnh trên toàn đồ thị thay vì cho một cây liên thông mọc dần.
Độ phức tạp thời gian và không gian
| Cách cài đặt | Độ phức tạp | Ghi chú |
|---|---|---|
| Binary heap | O(E log V) | Hàng đợi ưu tiên chứa các cạnh cắt |
| Ma trận kề | O(V²) | Đơn giản hơn; tốt cho đồ thị dày |
| Không gian | O(V + E) | Đánh dấu đỉnh trong cây và hàng đợi ưu tiên |
| Phù hợp nhất với | Đồ thị dày | Mọc ra từ một đỉnh xuất phát |
Từng bước
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Bắt đầu cây với một đỉnh bất kỳ. |
| 2 | Xem xét mọi cạnh đi từ cây sang một đỉnh nằm ngoài cây. |
| 3 | Chọn cạnh cắt có trọng số nhỏ nhất. |
| 4 | Thêm cạnh đó và đỉnh mới của nó vào cây. |
| 5 | Lặp lại cho đến khi mọi đỉnh đều nằm trong cây. |
Ví dụ minh họa
Xây dựng MST của một đồ thị 4 đỉnh với các cạnh A-B=1, A-C=3, B-C=2, B-D=4, C-D=5, xuất phát từ A:
| Bước | Cây | Các cạnh cắt | Cạnh được chọn |
|---|---|---|---|
| 1 | {A} | A-B=1, A-C=3 | A-B (trọng số 1) |
| 2 | {A, B} | A-C=3, B-C=2, B-D=4 | B-C (trọng số 2) |
| 3 | {A, B, C} | B-D=4, C-D=5 | B-D (trọng số 4) |
| 4 | {A, B, C, D} | không còn: mọi đỉnh đã ở trong cây | xong: trọng số MST 1+2+4 = 7 |
Khi nào nên dùng thuật toán Prim
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Bạn cần cây khung nhỏ nhất của một đồ thị vô hướng, có trọng số và liên thông. | Đồ thị có hướng hoặc bạn cần đường đi ngắn nhất: hãy dùng Dijkstra hoặc Bellman-Ford. |
Đồ thị dày (E gần bằng V²); dạng ma trận O(V²) vừa đơn giản vừa nhanh. | Đồ thị thưa và các cạnh đã được sắp xếp hoặc dễ sắp xếp: Kruskal thường đơn giản hơn. |
| Bạn muốn cây mọc dần ra từ một vùng (ví dụ bố trí mạng theo từng bước). | Đồ thị không liên thông: Prim chỉ phủ một thành phần, trong khi bạn cần rừng khung nhỏ nhất. |
| Bạn đã có sẵn cấu trúc kề và một hàng đợi ưu tiên. | Bạn cần phát hiện chu trình trên toàn bộ tập cạnh: union-find (Kruskal) phù hợp hơn với bài toán đó. |
Code Prim's Algorithm
Một cách cài đặt Prim'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 Prim's Algorithm bằng Python
1import heapq2
3
4def prim(graph, start):5 visited = {start}6 heap = [(w, start, v) for v, w in graph[start]]7 heapq.heapify(heap)8 mst, total = [], 09 while heap and len(visited) < len(graph):10 w, u, v = heapq.heappop(heap)11 if v in visited:12 continue13 visited.add(v)14 mst.append((u, v, w))15 total += w16 # Offer the new node's edges to the frontier17 for neighbor, weight in graph[v]:18 if neighbor not in visited:19 heapq.heappush(heap, (weight, v, neighbor))20 return mst, total21
22
23graph = {24 "A": [("B", 4), ("C", 1)],25 "B": [("A", 4), ("C", 3), ("D", 2)],26 "C": [("A", 1), ("B", 3), ("D", 5)],27 "D": [("B", 2), ("C", 5), ("E", 7)],28 "E": [("D", 7)],29}30
31mst, total = prim(graph, "A")32for u, v, w in mst:33 print(f"{u} - {v} (weight {w})")34print("Total MST weight:", total)Code Prim's Algorithm bằng JavaScript
1const graph = {2 A: { B: 4, C: 2 },3 B: { A: 4, C: 1, D: 5 },4 C: { A: 2, B: 1, D: 8, E: 10 },5 D: { B: 5, C: 8, E: 2 },6 E: { C: 10, D: 2 },7};8
9function prim(start) {10 const inTree = new Set([start]);11 const mst = [];12 let total = 0;13 while (inTree.size < Object.keys(graph).length) {14 // JS has no built-in heap: scan all crossing edges for the cheapest15 let best = null;16 for (const u of inTree) {17 for (const [v, w] of Object.entries(graph[u])) {18 if (!inTree.has(v) && (best === null || w < best[2])) {19 best = [u, v, w];20 }21 }22 }23 const [u, v, w] = best;24 inTree.add(v);25 mst.push(`${u}-${v} (${w})`);26 total += w;27 }28 return { mst, total };29}30
31const { mst, total } = prim("A");32console.log("MST edges:", mst.join(", "));33console.log("Total weight:", total);Code Prim's Algorithm bằng Java
1import java.util.ArrayList;2import java.util.List;3import java.util.PriorityQueue;4
5public class Main {6 public static void main(String[] args) {7 int n = 6;8 List<List<int[]>> adj = new ArrayList<>();9 for (int i = 0; i < n; i++) adj.add(new ArrayList<>());10 int[][] edges = {11 {0, 1, 4}, {0, 2, 3}, {1, 2, 1}, {1, 3, 2},12 {2, 3, 4}, {3, 4, 2}, {4, 5, 6}, {2, 5, 7}13 };14 for (int[] e : edges) {15 adj.get(e[0]).add(new int[]{e[1], e[2]});16 adj.get(e[1]).add(new int[]{e[0], e[2]});17 }18
19 boolean[] inMst = new boolean[n];20 // Always grow the tree along the cheapest crossing edge21 PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);22 pq.add(new int[]{0, 0}); // {node, edge weight}23 int total = 0, taken = 0;24 while (!pq.isEmpty() && taken < n) {25 int[] cur = pq.poll();26 if (inMst[cur[0]]) continue;27 inMst[cur[0]] = true;28 total += cur[1];29 taken++;30 for (int[] edge : adj.get(cur[0])) {31 if (!inMst[edge[0]]) pq.add(edge);32 }33 }34 System.out.println("MST nodes reached: " + taken);35 System.out.println("MST total weight: " + total);36 }37}Code Prim's Algorithm bằng C++
1#include <iostream>2#include <queue>3#include <vector>4
5int main() {6 // Undirected weighted graph: adj[u] = {(neighbor, weight), ...}7 std::vector<std::vector<std::pair<int, int>>> adj = {8 {{1, 2}, {3, 6}}, // 09 {{0, 2}, {2, 3}, {3, 8}, {4, 5}}, // 110 {{1, 3}, {4, 7}}, // 211 {{0, 6}, {1, 8}}, // 312 {{1, 5}, {2, 7}}, // 413 };14 int n = static_cast<int>(adj.size());15 std::vector<bool> inMST(n, false);16 using State = std::pair<int, int>; // (edge weight, node)17 std::priority_queue<State, std::vector<State>, std::greater<State>> pq;18 pq.push({0, 0});19 int total = 0, picked = 0;20 // Always grow the tree along the cheapest crossing edge21 while (!pq.empty() && picked < n) {22 auto [w, u] = pq.top();23 pq.pop();24 if (inMST[u]) continue;25 inMST[u] = true;26 total += w;27 ++picked;28 std::cout << "Add node " << u << " (edge weight " << w << ")\n";29 for (auto [v, weight] : adj[u]) {30 if (!inMST[v]) pq.push({weight, v});31 }32 }33 std::cout << "MST total weight: " << total << "\n";34 return 0;35}Code Prim's Algorithm bằng C
1#include <stdbool.h>2#include <stdio.h>3
4#define N 55#define INF 10000000006
7int main(void) {8 // Undirected weighted graph: w[u][v] = 0 means no edge9 int w[N][N] = {10 {0, 2, 0, 6, 0},11 {2, 0, 3, 8, 5},12 {0, 3, 0, 0, 7},13 {6, 8, 0, 0, 0},14 {0, 5, 7, 0, 0},15 };16 int key[N], parent[N];17 bool inMST[N] = {false};18 for (int v = 0; v < N; v++) {19 key[v] = INF;20 parent[v] = -1;21 }22 key[0] = 0;23 int total = 0;24 // O(V^2) scan for the cheapest crossing edge (no heap needed here)25 for (int iter = 0; iter < N; iter++) {26 int u = -1;27 for (int v = 0; v < N; v++) {28 if (!inMST[v] && (u == -1 || key[v] < key[u])) u = v;29 }30 inMST[u] = true;31 total += key[u];32 if (parent[u] != -1) {33 printf("Add edge %d - %d (weight %d)\n", parent[u], u, key[u]);34 }35 for (int v = 0; v < N; v++) {36 if (w[u][v] > 0 && !inMST[v] && w[u][v] < key[v]) {37 key[v] = w[u][v];38 parent[v] = u;39 }40 }41 }42 printf("MST total weight: %d\n", total);43 return 0;44}Câu hỏi thường gặp về thuật toán Prim
Độ phức tạp thời gian của thuật toán Prim là bao nhiêu?
O(E log V). Phiên bản ma trận kề đơn giản hơn, mỗi bước quét tìm cạnh cắt nhỏ nhất, tốn O(V²) và có thể nhanh hơn trên đồ thị dày. Cả hai đều dùng O(V + E) bộ nhớ.Thuật toán Prim và Kruskal khác nhau như thế nào?
Cả hai đều tìm cây khung nhỏ nhất theo kiểu tham lam. Prim cho một cây liên thông mọc ra từ đỉnh xuất phát, lặp lại việc thêm cạnh rẻ nhất đi ra khỏi cây (dùng hàng đợi ưu tiên). Kruskal xét mọi cạnh theo trọng số tăng dần và thêm cạnh rẻ nhất không tạo chu trình (dùng union-find). Prim thường được ưu tiên trên đồ thị dày, Kruskal trên đồ thị thưa.
Thuật toán Prim có luôn tìm được MST tối ưu không?
Có. Ở mỗi bước, cạnh rẻ nhất cắt qua cây hiện tại luôn an toàn để thêm vào, vì nó thuộc về một cây khung nhỏ nhất nào đó (tính chất lát cắt). Lặp lại việc thêm các cạnh an toàn sẽ cho ra một cây khung nhỏ nhất thực sự, miễn là đồ thị liên thông.
Khi nào nên dùng thuật toán Prim thay vì Kruskal?
O(V²) của nó không phải sắp xếp cả E cạnh và cho một cây mọc ra từ đỉnh xuất phát. Kruskal phát huy thế mạnh trên đồ thị thưa, nơi sắp xếp danh sách cạnh rẻ và union-find giữ cho việc kiểm tra chu trình nhanh. Cả hai đều cho ra MST đúng, nên lựa chọn chủ yếu phụ thuộc vào mật độ cạnh và các cấu trúc dữ liệu bạn đã có.Đỉnh xuất phát có ảnh hưởng đến MST thu được không?
Không. Tổng trọng số của MST là như nhau dù bạn xuất phát từ đỉnh nào, vì trọng số cây khung nhỏ nhất của một đồ thị liên thông là duy nhất. Các cạnh cụ thể chỉ có thể khác nhau khi nhiều cạnh có cùng trọng số và tồn tại nhiều MST. Ngoài trường hợp đó, Prim luôn cho ra đúng cùng một cây bất kể đỉnh xuất phát.
Thuật toán Prim có xử lý được đồ thị không liên thông không?
Không trực tiếp. Prim cho một cây duy nhất mọc dần, nên nó chỉ phủ thành phần liên thông chứa đỉnh xuất phát và bỏ qua phần còn lại. Để phủ mọi thành phần, bạn phải chạy lại Prim từ một đỉnh chưa được thăm nhiều lần, tạo ra một rừng khung nhỏ nhất thay vì một cây.