Bài 13: MST, Dijkstra, Topo Sort - Đồ Thị Nâng Cao¶
Tác giả: FPTOJ Team
Nội dung tham khảo từ: VNOI Wiki - Cây khung nhỏ nhất, Đường đi ngắn nhất, Sắp xếp Tô-pô
1. MST - Cây Khung Nhỏ Nhất¶
Bản chất vấn đề¶
Cho đồ thị vô hướng liên thông \(G = (V, E)\) với \(|V| = n\) đỉnh, \(|E| = m\) cạnh, mỗi cạnh có trọng số \(w(u,v)\). Tìm cây con \(T \subseteq E\) sao cho:
- \(T\) bao trùm tất cả \(n\) đỉnh (liên thông).
- Tổng trọng số \(\sum_{(u,v) \in T} w(u,v)\) là nhỏ nhất.
- \(T\) có đúng \(n - 1\) cạnh (không chu trình).
Đây là bài toán Cây khung nhỏ nhất (Minimum Spanning Tree - MST).
Tư duy cốt lõi¶
Có hai thuật toán kinh điển: Kruskal (cạnh-centric) và Prim (đỉnh-centric).
Kruskal - Tham lam trên cạnh:
Sắp xếp tất cả \(m\) cạnh theo trọng số tăng dần. Duyệt từng cạnh, nếu thêm cạnh đó không tạo chu trình thì chấp nhận. Dùng DSU (Disjoint Set Union) để kiểm tra chu trình trong \(O(\alpha(n))\).
Prim - Tham lam trên đỉnh:
Bắt đầu từ đỉnh任意. Mỗi bước, chọn cạnh có trọng số nhỏ nhất nối một đỉnh đã thăm với một đỉnh chưa thăm. Dùng min-heap để trích xuất cạnh nhỏ nhất nhanh.
Phân tích tính đúng đắn¶
Cut Property (Tính chất cắt): Với mọi tập \(S \subset V\) (\(S \neq \emptyset, S \neq V\)), cạnh có trọng số nhỏ nhất giữa \(S\) và \(V \setminus S\) nhất định thuộc MST.
Kruskal đúng vì: Khi xét cạnh \((u,v)\) nhỏ nhất chưa xét, nếu \(u\) và \(v\) thuộc hai thành phần liên thông khác nhau (tạo một "cắt"), thì \((u,v)\) là cạnh nhỏ nhất qua cắt đó → thuộc MST theo Cut Property.
Prim đúng vì: Tại mỗi bước, tập đỉnh đã thăm tạo một cắt. Cạnh nhỏ nhất nối đỉnh đã thăm với đỉnh chưa thăm chính là cạnh nhỏ nhất qua cắt đó → thuộc MST.
Đánh giá độ phức tạp¶
| Thuật toán | Độ phức tạp | Ghi chú |
|---|---|---|
| Kruskal | \(O(m \log m)\) | Chi phối bởi bước sắp xếp cạnh |
| Prim (heap) | \(O((n + m) \log n)\) | Mỗi cạnh push/pop heap tối đa 1 lần |
| Prim (ma trận) | \(O(n^2)\) | Tốt hơn khi \(m\) lớn (đồ thị dày) |
Kruskal phù hợp khi đồ thị thưa (\(m \approx n\)). Prim với heap phù hợp khi đồ thị trung bình. Prim với ma trận kề phù hợp khi đồ thị rất dày (\(m \approx n^2\)).

Thử tương tác
2. Dijkstra - Đường Đi Ngắn Nhất¶
Bản chất vấn đề¶
Cho đồ thị có hướng (hoặc vô hướng) \(G = (V, E)\) với trọng số không âm \(w(u,v) \geq 0\), đỉnh nguồn \(s\). Tìm đường đi từ \(s\) đến mọi đỉnh \(v\) sao cho tổng trọng số \(d(s,v) = \sum w\) là nhỏ nhất.
Đây là bài toán Đường đi ngắn nhất nguồn đơn (Single-Source Shortest Path - SSSP).
Tư duy cốt lõi¶
Dijkstra là thuật toán tham lam: luôn mở rộng từ đỉnh có khoảng cách tạm thời nhỏ nhất.
Bất biến quan trọng: Khi một đỉnh \(u\) được lấy ra từ priority queue, \(dist[u]\) đã là giá trị cuối cùng — không thể cải thiện thêm.
Điều kiện: Trọng số phải không âm. Nếu có cạnh trọng số âm, dùng Bellman-Ford thay thế.
Minh họa chạy chi tiết¶
Xét đồ thị sau với đỉnh nguồn \(s = 1\):
Bảng trace từng bước (PQ = priority queue, lưu (dist, đỉnh)):
| Bước | Pop | Duyệt cạnh | Cập nhật \(dist\) | PQ sau bước |
|---|---|---|---|---|
| Khởi tạo | - | - | \(dist = [\infty, 0, \infty, \infty]\) | \([(0,1)]\) |
| 1 | \((0, 1)\) | \((1,2): dist[1]+1=1\), \((1,3): dist[1]+4=4\) | \(dist[2]=1, dist[3]=4\) | \([(1,2), (4,3)]\) |
| 2 | \((1, 2)\) | \((2,4): dist[2]+2=3\) | \(dist[4]=3\) | \([(3,4), (4,3)]\) |
| 3 | \((3, 4)\) | Không có kề | - | \([(4,3)]\) |
| 4 | \((4, 3)\) | \((3,4): dist[3]+1=5 > dist[4]=3\) | Không cập nhật | \([]\) |
Kết quả: \(dist = [\infty, 0, 1, 4, 3]\). Đường ngắn nhất \(1 \to 4\): \(1 \to 2 \to 4\), độ dài \(= 3\).
Lưu ý: Tại bước 4, đỉnh 3 cố gắng cập nhật \(dist[4]\) nhưng bị bỏ qua vì \(5 > 3\). Đây là lý do Dijkstra chỉ hoạt động đúng với trọng số không âm.
Phân tích tính đúng đắn¶
Lemma: Khi đỉnh \(u\) được pop ra khỏi PQ với \(d = dist[u]\), thì \(dist[u]\) đã là khoảng cách ngắn nhất từ \(s\) đến \(u\).
Chứng minh (phản chứng): Giả sử \(dist[u]\) chưa tối ưu, tức tồn tại đường đi ngắn hơn qua đỉnh \(x\) chưa xử lý. Khi đó \(dist[x] < dist[u]\) (vì đường đi qua \(x\) ngắn hơn). Nhưng \(x\) chưa xử lý nghĩa là \(x\) vẫn trong PQ với ưu tiên \(\leq dist[x] < dist[u]\), nên \(x\) phải được pop trước \(u\). Mâu thuẫn.
Suy ra thuật toán luôn pop đỉnh có khoảng cách ngắn nhất trước → đảm bảo tính đúng đắn.
Đánh giá độ phức tạp¶
| Thành phần | Độ phức tạp |
|---|---|
| Mỗi đỉnh | Pop từ heap: \(O(\log n)\) |
| Mỗi cạnh | Push vào heap tối đa 1 lần: \(O(\log n)\) |
| Tổng | \(O((n + m) \log n)\) |
Với Fibonacci heap, độ phức tạp cải thiện còn \(O(n \log n + m)\), nhưng ít dùng trong thi đấu.
Dijkstra chỉ cho trọng số không âm
Nếu đồ thị có cạnh trọng số âm, dùng Bellman-Ford (\(O(nm)\)) hoặc SPFA thay thế. Xem Bài 23: Floyd-Warshall & Bellman-Ford.
3. Sắp Xếp Tô-pô (Topological Sort)¶
Bản chất vấn đề¶
Cho đồ thị có hướng không chu trình (DAG) \(G = (V, E)\) với \(|V| = n\) đỉnh, \(|E| = m\) cạnh. Tìm một hoán vị \(\sigma\) của các đỉnh sao cho với mọi cạnh \((u, v) \in E\), ta có \(\sigma(u) < \sigma(v)\) — tức mọi cạnh đều hướng từ trái sang phải.
Đây là bài toán Sắp xếp tô-pô (Topological Sort). Nếu đồ thị có chu trình, không tồn tại thứ tự tô-pô.
Tư duy cốt lõi¶
Phổ biến nhất là thuật toán Kahn dựa trên bậc vào (in-degree):
- Tính bậc vào \(in[v]\) cho mỗi đỉnh \(v\).
- Đỉnh nào có \(in[v] = 0\) (không bị phụ thuộc) được đưa vào hàng đợi.
- Lấy đỉnh \(u\) ra, "loại bỏ" \(u\) bằng cách giảm \(in[v]\) cho mọi đỉnh \(v\) kề \(u\).
- Nếu \(in[v]\) giảm về \(0\), đưa \(v\) vào hàng đợi.
- Lặp cho đến khi hàng đợi rỗng.
Minh họa chạy chi tiết¶
Xét DAG sau:
| Bước | Queue | Pop | Kết quả | Cập nhật \(in\) |
|---|---|---|---|---|
| Khởi tạo | \([1]\) | - | \([]\) | \(in = [0, 1, 1, 2, 1]\) |
| 1 | \([2, 3]\) | \(1\) | \([1]\) | \(in[2] \to 0, in[3] \to 0\) |
| 2 | \([3]\) | \(2\) | \([1, 2]\) | \(in[4] \to 1\) |
| 3 | \([4]\) | \(3\) | \([1, 2, 3]\) | \(in[4] \to 0\) |
| 4 | \([5]\) | \(4\) | \([1, 2, 3, 4]\) | \(in[5] \to 0\) |
| 5 | \([]\) | \(5\) | \([1, 2, 3, 4, 5]\) | - |
Kết quả: \([1, 2, 3, 4, 5]\) — mọi cạnh đều hướng từ trái sang phải.
Phân tích tính đúng đắn¶
Tính đúng đắn của Kahn:
- Mỗi đỉnh được đưa vào queue đúng một lần khi \(in[v] = 0\), tức tất cả đỉnh "tiên quyết" của \(v\) đã được xử lý.
- Nếu kết quả chứa đủ \(n\) đỉnh: mọi đỉnh đều được xử lý, và mọi cạnh \((u,v)\) đều hướng từ trái sang phải vì \(u\) luôn được xử lý trước \(v\) (do \(in[v]\) chỉ giảm về \(0\) sau khi \(u\) đã pop).
- Nếu kết quả có \(< n\) đỉnh: tồn tại chu trình. Khi đó, các đỉnh trong chu trình có \(in > 0\) vĩnh viễn vì chúng phụ thuộc lẫn nhau.
Đánh giá độ phức tạp¶
| Thành phần | Độ phức tạp |
|---|---|
| Tính \(in\)-degree | \(O(n + m)\) |
| Duyệt queue | \(O(n + m)\) (mỗi đỉnh/enqueue đúng 1 lần) |
| Tổng | \(O(n + m)\) |
Sắp xếp tô-pô có độ phức tạp tuyến tính — rất hiệu quả.
4. Tổng hợp & Bẫy hay gặp¶
Bảng so sánh¶
| Thuật toán | Khi nào dùng | Độ phức tạp |
|---|---|---|
| Kruskal | MST, cạnh ít (đồ thị thưa) | \(O(m \log m)\) |
| Prim | MST, cạnh nhiều (đồ thị dày) | \(O((n+m) \log n)\) |
| Dijkstra | Đường đi ngắn nhất, trọng số \(\geq 0\) | \(O((n+m) \log n)\) |
| Topo Sort | Sắp xếp thứ tự ưu tiên trên DAG | \(O(n + m)\) |
Bẫy hay gặp¶
Bẫy 1: Dijkstra với trọng số âm
Dijkstra không hoạt động đúng khi có trọng số âm. Khi đỉnh \(u\) được pop, \(dist[u]\) được coi là tối ưu — nhưng trọng số âm có thể tạo đường đi ngắn hơn qua đỉnh chưa xử lý. Dùng Bellman-Ford thay thế.
Bẫy 2: Topo Sort trên đồ thị có chu trình
Nếu kết quả có \(< n\) đỉnh, đồ thị có chu trình. Quên kiểm tra điều kiện này sẽ dẫn đến kết quả sai mà không biết.
Bẫy 3: MST trên đồ thị không liên thông
Nếu số cạnh được chọn \(< n - 1\), đồ thị không liên thông và không tồn tại MST. Luôn kiểm tra edges_used == n - 1 (Kruskal) hoặc count == n (Prim).
Bẫy 4: Quên visited trong Prim
Nếu không kiểm tra visited[u] trước khi xử lý, đỉnh sẽ bị xử lý nhiều lần do cùng một đỉnh có thể nằm nhiều lần trong heap với trọng số khác nhau.
Bẫy 5: Dijkstra — quên bỏ qua đỉnh cũ trong heap
Điều kiện if (d > dist[u]) continue là bắt buộc. Heap có thể chứa nhiều bản ghi cho cùng một đỉnh; chỉ bản ghi có \(dist\) nhỏ nhất là hợp lệ.
Bài tập luyện tập¶
| Bài | Nền tảng | Độ khó | Chủ đề |
|---|---|---|---|
| CSES - Road Reparation | CSES | ⭐⭐ | MST |
| CSES - Road Construction | CSES | ⭐⭐ | DSU + MST |
| CSES - Shortest Routes I | CSES | ⭐⭐ | Dijkstra |
| CSES - Shortest Routes II | CSES | ⭐⭐ | Floyd-Warshall |
| CSES - Course Schedule | CSES | ⭐⭐ | Topo Sort |
| CSES - Longest Flight Route | CSES | ⭐⭐⭐ | Topo + DP |
| VNOJ - QBMST | VNOJ | ⭐⭐ | MST cơ bản |
| VNOJ - DIJKSTRA | VNOJ | ⭐⭐ | Dijkstra |
| VNOJ - TOPOSORT | VNOJ | ⭐⭐ | Topo sort |
Bài viết liên quan¶
Tài liệu tham khảo¶
- VNOI Wiki - Cây khung nhỏ nhất
- VNOI Wiki - Đường đi ngắn nhất
- VNOI Wiki - Sắp xếp Tô-pô
- CP-Algorithms - Prim's Algorithm
- CP-Algorithms - Kruskal's Algorithm
- CP-Algorithms - Topological Sort
Bài tiếp theo: Hash xâu & Z-algorithm →