Sắp xếp topo
Cập nhật lần cuối
Sắp xếp topo (topological sort) của một đồ thị có hướng không chu trình (DAG) là một cách xếp các đỉnh thành hàng sao cho với mọi cạnh u → v, u luôn đứng trước v. Nó trả lời những câu hỏi như "nên chạy các tác vụ này theo thứ tự nào để mọi điều kiện tiên quyết đều xong trước?" Nhấn Phát ở trên để xem thuật toán Kahn lần lượt tách các đỉnh ra theo một thứ tự hợp lệ.
Thuật toán Kahn liên tục lấy một đỉnh không còn cạnh đi vào (bậc vào 0), thêm nó vào thứ tự, rồi xóa các cạnh đi ra của nó, việc này có thể tạo thêm những đỉnh mới có bậc vào 0. Thuật toán chỉ chạy được trên DAG: nếu có chu trình, một số đỉnh sẽ không bao giờ đạt bậc vào 0 và không tồn tại thứ tự hợp lệ. Thuật toán chạy trong thời gian O(V + E).
Độ phức tạp thời gian và không gian
| Đại lượng | Độ phức tạp | Ghi chú |
|---|---|---|
| Thời gian | O(V + E) | Mỗi đỉnh được đưa ra một lần, mỗi cạnh bị xóa một lần |
| Không gian | O(V) | Bậc vào của các đỉnh, tập sẵn sàng và thứ tự kết quả |
| Yêu cầu | Một DAG | Đồ thị có chu trình không có thứ tự topo |
| Kết quả | Không duy nhất | Có thể có nhiều thứ tự hợp lệ |
Từng bước (thuật toán Kahn)
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Tính bậc vào (số cạnh đi vào) của mọi đỉnh. |
| 2 | Gom mọi đỉnh có bậc vào 0 vào một tập sẵn sàng. |
| 3 | Lấy một đỉnh sẵn sàng, thêm nó vào thứ tự kết quả. |
| 4 | Giảm bậc vào của từng đỉnh kế tiếp của nó. |
| 5 | Đỉnh kế tiếp nào giảm về bậc vào 0 sẽ được thêm vào tập sẵn sàng. |
| 6 | Lặp lại cho đến khi tập sẵn sàng rỗng. |
Ví dụ minh họa
Sắp xếp DAG có các cạnh A→C, B→C, C→D, C→E, D→F, E→F (bậc vào ban đầu A:0 B:0 C:2 D:1 E:1 F:2):
| Bước | Tập sẵn sàng | Thứ tự | Thao tác |
|---|---|---|---|
| 0 | {A, B} | [] | A và B có bậc vào 0 ngay từ đầu, nên cả hai đều sẵn sàng. |
| 1 | {B} | [A] | Đưa ra A; cạnh A→C của nó giảm bậc vào của C từ 2 xuống 1. |
| 2 | {C} | [A, B] | Đưa ra B; cạnh B→C của nó giảm C từ 1 xuống 0, nên C trở nên sẵn sàng. |
| 3 | {D, E} | [A, B, C] | Đưa ra C; các cạnh C→D và C→E giảm D và E về 0, cả hai trở nên sẵn sàng. |
| 4 | {E} | [A, B, C, D] | Đưa ra D; cạnh D→F của nó giảm bậc vào của F từ 2 xuống 1. |
| 5 | {F} | [A, B, C, D, E] | Đưa ra E; cạnh E→F của nó giảm F từ 1 xuống 0, nên F trở nên sẵn sàng. |
| 6 | {} | [A, B, C, D, E, F] | Đưa ra F; tập sẵn sàng rỗng và cả 6 đỉnh đã có thứ tự: xong. |
Khi nào nên dùng sắp xếp topo
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Bạn cần một thứ tự tôn trọng các phụ thuộc (các bước build, cài đặt package, môn học tiên quyết). | Đồ thị vô hướng: thứ tự topo chỉ được định nghĩa cho đồ thị có hướng. |
| Đồ thị là một DAG và bạn muốn một thứ tự tuyến tính hợp lệ bất kỳ. | Đồ thị có thể chứa chu trình mà bạn vẫn cần một thứ tự toàn phần (điều đó không tồn tại). |
| Bạn muốn phát hiện chu trình với chi phí thấp: sắp xếp topo thất bại chứng tỏ có chu trình. | Bạn cần thứ tự ngắn nhất hoặc tối ưu theo một trọng số nào đó; sắp xếp topo thông thường bỏ qua trọng số. |
Bạn sẽ xử lý thứ tự này một lần trong O(V + E). | Các cạnh thay đổi liên tục và bạn phải sắp xếp lại sau mỗi lần cập nhật, khi đó một cấu trúc cập nhật tăng dần phù hợp hơn. |
Code Topological Sort
Một cách cài đặt Topological Sort 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 Topological Sort bằng Python
1from collections import deque2
3
4def topological_sort(graph):5 # Kahn's algorithm: repeatedly remove nodes with no incoming edges6 in_degree = {node: 0 for node in graph}7 for node in graph:8 for neighbor in graph[node]:9 in_degree[neighbor] += 110 queue = deque(node for node in graph if in_degree[node] == 0)11 order = []12 while queue:13 node = queue.popleft()14 order.append(node)15 for neighbor in graph[node]:16 in_degree[neighbor] -= 117 if in_degree[neighbor] == 0:18 queue.append(neighbor)19 if len(order) != len(graph):20 raise ValueError("Graph has a cycle, no topological order")21 return order22
23
24graph = {25 "shirt": ["tie", "jacket"],26 "tie": ["jacket"],27 "pants": ["shoes", "jacket"],28 "socks": ["shoes"],29 "shoes": [],30 "jacket": [],31}32
33print(" -> ".join(topological_sort(graph)))Code Topological Sort bằng JavaScript
1const graph = {2 A: ["C"],3 B: ["C", "D"],4 C: ["E"],5 D: ["F"],6 E: ["F"],7 F: [],8};9
10// Kahn algorithm: repeatedly take a node with no incoming edges11function topologicalSort() {12 const inDegree = {};13 for (const node in graph) inDegree[node] = 0;14 for (const node in graph) {15 for (const next of graph[node]) inDegree[next]++;16 }17 const queue = Object.keys(inDegree).filter((n) => inDegree[n] === 0);18 const order = [];19 while (queue.length > 0) {20 const node = queue.shift();21 order.push(node);22 for (const next of graph[node]) {23 if (--inDegree[next] === 0) queue.push(next);24 }25 }26 if (order.length !== Object.keys(graph).length) {27 throw new Error("Graph has a cycle");28 }29 return order;30}31
32console.log("Topological order:", topologicalSort().join(" -> "));Code Topological Sort bằng Java
1import java.util.ArrayDeque;2import java.util.List;3import java.util.Queue;4
5public class Main {6 public static void main(String[] args) {7 int n = 6;8 // DAG: an edge u -> v means u must come before v9 List<List<Integer>> adj = List.of(10 List.of(2),11 List.of(2, 3),12 List.of(4),13 List.of(4),14 List.of(5),15 List.of()16 );17 int[] inDegree = new int[n];18 for (List<Integer> next : adj) {19 for (int v : next) inDegree[v]++;20 }21
22 // Kahn: start from every node with no prerequisites23 Queue<Integer> queue = new ArrayDeque<>();24 for (int i = 0; i < n; i++) {25 if (inDegree[i] == 0) queue.add(i);26 }27
28 StringBuilder order = new StringBuilder("Topological order:");29 while (!queue.isEmpty()) {30 int node = queue.poll();31 order.append(" ").append(node);32 for (int next : adj.get(node)) {33 if (--inDegree[next] == 0) queue.add(next);34 }35 }36 System.out.println(order);37 }38}Code Topological Sort bằng C++
1#include <iostream>2#include <queue>3#include <vector>4
5int main() {6 // Directed acyclic graph: adj[u] lists nodes that depend on u7 std::vector<std::vector<int>> adj = {8 {}, // 09 {}, // 110 {3}, // 2 -> 311 {1}, // 3 -> 112 {0, 1}, // 4 -> 0, 113 {0, 2}, // 5 -> 0, 214 };15 int n = static_cast<int>(adj.size());16 std::vector<int> inDegree(n, 0);17 for (const auto& neighbors : adj) {18 for (int v : neighbors) ++inDegree[v];19 }20 // Kahn's algorithm: start from nodes with no incoming edges21 std::queue<int> ready;22 for (int u = 0; u < n; ++u) {23 if (inDegree[u] == 0) ready.push(u);24 }25 std::cout << "Topological order: ";26 while (!ready.empty()) {27 int u = ready.front();28 ready.pop();29 std::cout << u << " ";30 // Removing u unlocks neighbors whose in-degree drops to 031 for (int v : adj[u]) {32 if (--inDegree[v] == 0) ready.push(v);33 }34 }35 std::cout << "\n";36 return 0;37}Code Topological Sort bằng C
1#include <stdio.h>2
3#define N 64
5int main(void) {6 // Directed acyclic graph: adj[u][v] = 1 means an edge u -> v7 int adj[N][N] = {0};8 adj[2][3] = 1;9 adj[3][1] = 1;10 adj[4][0] = 1;11 adj[4][1] = 1;12 adj[5][0] = 1;13 adj[5][2] = 1;14 int inDegree[N] = {0};15 for (int u = 0; u < N; u++) {16 for (int v = 0; v < N; v++) inDegree[v] += adj[u][v];17 }18 // Kahn's algorithm: start from nodes with no incoming edges19 int queue[N];20 int head = 0, tail = 0;21 for (int u = 0; u < N; u++) {22 if (inDegree[u] == 0) queue[tail++] = u;23 }24 printf("Topological order: ");25 while (head < tail) {26 int u = queue[head++];27 printf("%d ", u);28 // Removing u unlocks neighbors whose in-degree drops to 029 for (int v = 0; v < N; v++) {30 if (adj[u][v] && --inDegree[v] == 0) queue[tail++] = v;31 }32 }33 printf("\n");34 return 0;35}Câu hỏi thường gặp về sắp xếp topo
Sắp xếp topo dùng để làm gì?
Nó sắp xếp các tác vụ sao cho mọi phụ thuộc luôn đứng trước thứ cần đến nó. Các ứng dụng thực tế gồm hệ thống build và trình quản lý package (biên dịch phần phụ thuộc trước), xếp lịch môn học có môn tiên quyết, và thứ tự tính công thức trong bảng tính.
Độ phức tạp thời gian của sắp xếp topo là bao nhiêu?
O(V + E), vì mỗi đỉnh được xử lý một lần và mỗi cạnh được xét một lần. Chúng dùng thêm O(V) bộ nhớ.Tại sao sắp xếp topo cần một DAG?
a phải đứng trước b và b phải đứng trước a, không có thứ tự tuyến tính nào thỏa mãn cả hai. Vì vậy thứ tự topo tồn tại khi và chỉ khi đồ thị là đồ thị có hướng không chu trình. Thuật toán Kahn phát hiện chu trình khi nó kết thúc mà chưa đưa ra hết mọi đỉnh.Thuật toán Kahn và sắp xếp topo bằng DFS khác nhau như thế nào?
O(V + E); Kahn tránh được đệ quy sâu và tự nhiên cho biết tập sẵn sàng, còn DFS thường viết ngắn hơn.Khi nào nên dùng sắp xếp topo thay vì sắp xếp thông thường?
O(n log n) sắp xếp theo giá trị; sắp xếp topo sắp xếp theo các cạnh "phải đứng trước", và khác với sắp xếp so sánh, nó có thể cho ra nhiều đáp án hợp lệ với cùng một đầu vào.Kết quả của sắp xếp topo có duy nhất không?
Thường là không. Mỗi khi có hai đỉnh trở lên cùng sẵn sàng (bậc vào 0), bạn có thể đưa chúng ra theo thứ tự bất kỳ, nên phần lớn DAG có nhiều thứ tự topo hợp lệ. Thứ tự chỉ duy nhất khi ở mỗi bước có đúng một đỉnh sẵn sàng, điều xảy ra khi DAG tạo thành một chuỗi duy nhất (một đường đi Hamilton).