Sắp xếp tô pô (Topological Sort)

Khái niệm

Sắp xếp tô pô (Topological Sort, hay Topological Ordering) là phép sắp xếp các đỉnh của một đồ thị có hướng không chu trình (DAG) sao cho với mọi cạnh u → v, đỉnh u luôn xuất hiện trước v trong kết quả.

Điều kiện: Chỉ áp dụng được cho DAG (Directed Acyclic Graph). Nếu đồ thị có chu trình, không tồn tại thứ tự tô pô.

Ví dụ thực tế:

  • Thứ tự học môn: “Lập trình cơ bản” phải trước “Cấu trúc dữ liệu”
  • Thứ tự build: file A phụ thuộc file B → phải biên dịch B trước A
  • Công việc dây chuyền: công đoạn trước phải xong trước công đoạn sau
graph LR; A[Toán đại cương] --> C[Giải tích]; A --> D[Đại số tuyến tính]; B[Lập trình cơ bản] --> E[Cấu trúc dữ liệu]; C --> F[Phương trình vi phân]; D --> G[Tối ưu hóa]; E --> G;

Một thứ tự hợp lệ: A → B → C → D → E → F → G

Thuật toán Kahn (BFS)

Thuật toán Kahn dùng BFS với mảng bậc vào (in-degree): liên tục chọn và loại bỏ các đỉnh có bậc vào bằng 0 (không còn môn tiên quyết).

from collections import defaultdict, deque

def topo_sort_kahn(n, canh):
    """
    Sắp xếp tô pô bằng thuật toán Kahn.
    Trả về danh sách thứ tự hợp lệ, hoặc [] nếu có chu trình.

    n: số đỉnh (0-indexed)
    canh: [(u, v)] — cạnh hướng từ u đến v
    """
    graph   = defaultdict(list)
    in_deg  = [0] * n

    for u, v in canh:
        graph[u].append(v)
        in_deg[v] += 1

    # Bắt đầu từ tất cả đỉnh có bậc vào = 0
    queue  = deque(i for i in range(n) if in_deg[i] == 0)
    result = []

    while queue:
        u = queue.popleft()
        result.append(u)
        for v in graph[u]:
            in_deg[v] -= 1
            if in_deg[v] == 0:
                queue.append(v)

    # Nếu không lấy đủ n đỉnh → có chu trình
    if len(result) == n:
        return result
    else:
        return []    # Chu trình phát hiện

# Ví dụ: 6 môn học (0-5)
# 0=Toán, 1=LtrCB, 2=Giaitich, 3=DSTS, 4=CTDL, 5=ToiUu
canh = [(0,2),(0,3),(1,4),(2,5),(3,5),(4,5)]
thu_tu = topo_sort_kahn(6, canh)
print("Thứ tự học:", thu_tu)    # Một kết quả hợp lệ: [0, 1, 2, 3, 4, 5]

# Test đồ thị có chu trình
canh_chu_trinh = [(0,1),(1,2),(2,0)]
print("Chu trình:", topo_sort_kahn(3, canh_chu_trinh))   # []

Thuật toán DFS

Dùng DFS, đẩy đỉnh vào stack sau khi đã duyệt xong tất cả đỉnh kề. Đảo ngược stack thu được thứ tự tô pô.

from collections import defaultdict

def topo_sort_dfs(n, canh):
    """
    Sắp xếp tô pô bằng DFS.
    Trả về (thứ_tự, có_chu_trình).
    """
    graph = defaultdict(list)
    for u, v in canh:
        graph[u].append(v)

    # Trạng thái: 0=chưa thăm, 1=đang thăm, 2=xong
    state = [0] * n
    stack = []
    co_chu_trinh = [False]

    def dfs(u):
        if co_chu_trinh[0]:
            return
        state[u] = 1                # Đang thăm
        for v in graph[u]:
            if state[v] == 1:        # Gặp đỉnh đang thăm → chu trình
                co_chu_trinh[0] = True
                return
            if state[v] == 0:
                dfs(v)
        state[u] = 2                # Xong
        stack.append(u)             # Đẩy vào stack SAU KHI xong

    for i in range(n):
        if state[i] == 0:
            dfs(i)

    if co_chu_trinh[0]:
        return [], True
    return stack[::-1], False       # Đảo ngược stack

canh = [(0,2),(0,3),(1,4),(2,5),(3,5),(4,5)]
thu_tu, cycle = topo_sort_dfs(6, canh)
print("Thứ tự:", thu_tu)
print("Có chu trình:", cycle)

So sánh hai thuật toán

Tiêu chí Kahn (BFS) DFS
Độ phức tạp O(V + E) O(V + E)
Phát hiện chu trình Dễ (len != n) Cần thêm trạng thái
Nhiều kết quả hợp lệ Dễ sinh bằng priority queue Khó hơn
Phù hợp khi Cần kiểm soát thứ tự Tích hợp vào DFS sẵn có

Bài tập luyện tập

Dễ — Kiểm tra có phải DAG không

from collections import defaultdict, deque

def la_dag(n, canh):
    """Trả về True nếu đồ thị có hướng không có chu trình."""
    graph  = defaultdict(list)
    in_deg = [0] * n
    for u, v in canh:
        graph[u].append(v)
        in_deg[v] += 1
    q = deque(i for i in range(n) if in_deg[i] == 0)
    count = 0
    while q:
        u = q.popleft()
        count += 1
        for v in graph[u]:
            in_deg[v] -= 1
            if in_deg[v] == 0:
                q.append(v)
    return count == n

print(la_dag(4, [(0,1),(1,2),(2,3)]))           # True
print(la_dag(3, [(0,1),(1,2),(2,0)]))           # False (chu trình)
print(la_dag(5, [(0,1),(0,2),(1,3),(2,3),(3,4)]))  # True

Trung bình — Số cách sắp xếp hợp lệ tối thiểu theo thứ tự từ điển

import heapq
from collections import defaultdict

def topo_thu_tu_tu_dien(n, canh):
    """
    Sắp xếp tô pô theo thứ tự từ điển nhỏ nhất.
    Dùng min-heap thay vì thông thường queue.
    """
    graph  = defaultdict(list)
    in_deg = [0] * n
    for u, v in canh:
        graph[u].append(v)
        in_deg[v] += 1

    heap = [i for i in range(n) if in_deg[i] == 0]
    heapq.heapify(heap)
    result = []
    while heap:
        u = heapq.heappop(heap)
        result.append(u)
        for v in graph[u]:
            in_deg[v] -= 1
            if in_deg[v] == 0:
                heapq.heappush(heap, v)
    return result if len(result) == n else []

canh = [(0,2),(0,3),(1,4),(2,5),(3,5),(4,5)]
print("Thứ tự từ điển nhỏ nhất:", topo_thu_tu_tu_dien(6, canh))
# [0, 1, 2, 3, 4, 5]

Khó — Đường đi dài nhất trong DAG

from collections import defaultdict, deque

def duong_di_dai_nhat_dag(n, canh_trong_so):
    """
    Tìm đường đi dài nhất (theo trọng số) trong DAG.
    Dùng topo sort + DP.
    """
    graph  = defaultdict(list)
    in_deg = [0] * n
    for u, v, w in canh_trong_so:
        graph[u].append((v, w))
        in_deg[v] += 1

    # Bước 1: Topo sort (Kahn)
    q     = deque(i for i in range(n) if in_deg[i] == 0)
    order = []
    temp_in = in_deg[:]
    while q:
        u = q.popleft()
        order.append(u)
        for v, _ in graph[u]:
            temp_in[v] -= 1
            if temp_in[v] == 0:
                q.append(v)

    # Bước 2: DP theo thứ tự topo
    dist = [-float('inf')] * n
    for src in [i for i in range(n) if in_deg[i] == 0]:
        dist[src] = 0
    for u in order:
        if dist[u] == -float('inf'):
            continue
        for v, w in graph[u]:
            dist[v] = max(dist[v], dist[u] + w)
    return max(dist)

# DAG: 0→1(3), 0→2(6), 1→3(4), 2→3(2), 0→3(2)
canh = [(0,1,3),(0,2,6),(1,3,4),(2,3,2),(0,3,2)]
print("Đường đi dài nhất:", duong_di_dai_nhat_dag(4, canh))   # 8 (0→1→3)

Kết luận

Sắp xếp tô pô là nền tảng cho nhiều bài toán trên DAG. Thuật toán Kahn đơn giản và trực quan hơn, dễ phát hiện chu trình. Tiếp theo hãy học Union-Find — cấu trúc dữ liệu hiệu quả cho bài toán kết nối.

Bình luận