Thuật toán Dijkstra

Khái niệm

Dijkstra là thuật toán tìm đường đi ngắn nhất từ một nguồn (Single Source Shortest Path — SSSP) cho đồ thị có trọng số không âm.

Ý tưởng: Dùng min-heap (hàng đợi ưu tiên) — liên tục chọn đỉnh chưa xử lý có khoảng cách nhỏ nhất, rồi “thư giãn” (relax) các cạnh kề.

Điều kiện áp dụng: Mọi trọng số cạnh ≥ 0. (Nếu có trọng số âm → dùng Bellman-Ford)

Cài đặt Độ phức tạp
Ma trận kề (không heap) O(V²)
Danh sách kề + min-heap O((V + E) log V)
graph LR; S((S\ndist=0)) -- 4 --> A((A)); S -- 1 --> C((C)); C -- 2 --> A; C -- 5 --> B((B)); A -- 1 --> B; A -- 3 --> T((T\nđích)); B -- 2 --> T;

Từ S: S→C (1) → C→A (3) → A→T (6) là đường ngắn nhất. Không đi S→A trực tiếp vì tổng 4 > 3.

Cài đặt với min-heap

import heapq
from collections import defaultdict

def dijkstra(n, graph, src):
    """
    Tìm khoảng cách ngắn nhất từ src đến tất cả đỉnh còn lại.

    graph: dict {u: [(v, w), ...]} — danh sách kề có trọng số
    src  : đỉnh xuất phát (0-indexed)

    Trả về: dist[] — dist[v] là khoảng cách ngắn nhất từ src đến v
    """
    INF  = float('inf')
    dist = [INF] * n
    dist[src] = 0
    heap = [(0, src)]    # (khoảng_cách, đỉnh)

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:   # Đã tìm được đường ngắn hơn trước đó → bỏ qua
            continue
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))

    return dist


# Đồ thị ví dụ: 6 đỉnh (0=S, 1=A, 2=B, 3=C, 4=D, 5=T)
edges = [(0,1,4),(0,3,1),(3,1,2),(3,2,5),(1,2,1),(1,5,3),(2,5,2)]
g = defaultdict(list)
for u, v, w in edges:
    g[u].append((v, w))
    g[v].append((u, w))

dist = dijkstra(6, g, src=0)
print("Khoảng cách từ đỉnh 0:")
for v, d in enumerate(dist):
    print(f"  → đỉnh {v}: {d}")

Khôi phục đường đi

import heapq
from collections import defaultdict

def dijkstra_voi_duong_di(n, graph, src):
    """
    Dijkstra kèm truy vết đường đi.
    Trả về (dist[], prev[]) — prev[v] là đỉnh liền trước v trên đường ngắn nhất.
    """
    INF  = float('inf')
    dist = [INF] * n
    prev = [-1]  * n
    dist[src] = 0
    heap = [(0, src)]

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                prev[v] = u
                heapq.heappush(heap, (dist[v], v))

    return dist, prev

def lay_duong_di(prev, dst):
    """Truy vết từ mảng prev để lấy đường đi đến dst."""
    path = []
    cur  = dst
    while cur != -1:
        path.append(cur)
        cur = prev[cur]
    return path[::-1]

# Test
edges = [(0,1,4),(0,3,1),(3,1,2),(3,2,5),(1,2,1),(1,5,3),(2,5,2)]
g = defaultdict(list)
for u, v, w in edges:
    g[u].append((v, w))
    g[v].append((u, w))

dist, prev = dijkstra_voi_duong_di(6, g, src=0)
print(f"Khoảng cách 0→5: {dist[5]}")          # 6
print(f"Đường đi 0→5: {lay_duong_di(prev, 5)}")   # [0, 3, 1, 5] hoặc [0, 3, 1, 2, 5]

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

Dễ — Khoảng cách ngắn nhất giữa hai đỉnh

import heapq
from collections import defaultdict

def khoang_cach_ngan_nhat(n, canh, src, dst):
    """Tìm khoảng cách ngắn nhất từ src đến dst, -1 nếu không tới được."""
    g = defaultdict(list)
    for u, v, w in canh:
        g[u].append((v, w))
        g[v].append((u, w))
    dist = [float('inf')] * n
    dist[src] = 0
    heap = [(0, src)]
    while heap:
        d, u = heapq.heappop(heap)
        if u == dst:
            return d
        if d > dist[u]:
            continue
        for v, w in g[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))
    return -1 if dist[dst] == float('inf') else dist[dst]

canh = [(0,1,7),(0,2,9),(0,5,14),(1,2,10),(1,3,15),(2,3,11),(2,5,2),(3,4,6),(4,5,9)]
print(khoang_cach_ngan_nhat(6, canh, 0, 4))    # 20

Trung bình — Đường đi ngắn nhất trên lưới có chướng ngại vật

import heapq

def dijkstra_luoi(luoi):
    """
    Tìm đường đi ngắn nhất từ (0,0) đến (rows-1,cols-1).
    Ô '#' là tường. Ô khác có chi phí = giá trị số tại ô đó.
    """
    rows, cols = len(luoi), len(luoi[0])
    INF  = float('inf')
    dist = [[INF] * cols for _ in range(rows)]
    dist[0][0] = luoi[0][0]
    heap = [(luoi[0][0], 0, 0)]

    while heap:
        d, r, c = heapq.heappop(heap)
        if d > dist[r][c]:
            continue
        if r == rows-1 and c == cols-1:
            return d
        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            nr, nc = r+dr, c+dc
            if 0 <= nr < rows and 0 <= nc < cols and luoi[nr][nc] != '#':
                nd = dist[r][c] + luoi[nr][nc]
                if nd < dist[nr][nc]:
                    dist[nr][nc] = nd
                    heapq.heappush(heap, (nd, nr, nc))
    return -1

grid = [
    [1, 3, 1],
    [1, 5, 1],
    [4, 2, 1]
]
print(dijkstra_luoi(grid))   # 7 (1→3→1→1→1)

Khó — K đường đi ngắn nhất

import heapq
from collections import defaultdict

def k_duong_ngan_nhat(n, canh_co_huong, src, dst, k):
    """
    Tìm khoảng cách của đường đi ngắn thứ k từ src đến dst.
    Ý tưởng: mỗi đỉnh được "giải quyết" tối đa k lần.
    """
    g = defaultdict(list)
    for u, v, w in canh_co_huong:
        g[u].append((v, w))

    count = [0] * n   # Số lần đỉnh được lấy ra khỏi heap
    heap  = [(0, src)]
    while heap:
        d, u = heapq.heappop(heap)
        count[u] += 1
        if u == dst and count[u] == k:
            return d
        if count[u] > k:
            continue
        for v, w in g[u]:
            heapq.heappush(heap, (d + w, v))
    return -1   # Không đủ k đường đi

canh = [(0,1,1),(0,2,3),(1,3,2),(2,3,1),(1,2,1)]
print(k_duong_ngan_nhat(4, canh, 0, 3, 1))   # 3 (0→1→3)
print(k_duong_ngan_nhat(4, canh, 0, 3, 2))   # 4 (0→1→2→3)
print(k_duong_ngan_nhat(4, canh, 0, 3, 3))   # 4 (0→2→3 cũng là 4)

Kết luận

Dijkstra là thuật toán đường ngắn nhất mạnh nhất khi không có trọng số âm. Khi có trọng số âm, cần dùng Bellman-Ford. Sau khi nắm cả hai, hãy học Cây khung nhỏ nhất (MST).

Bình luận