Menu
CoddyTech

Thuật toán Dijkstra

Cập nhật lần cuối

Thuật toán Dijkstra tìm đường đi ngắn nhất từ một đỉnh nguồn đến mọi đỉnh khác trong đồ thị có trọng số cạnh không âm. Thuật toán lưu một khoảng cách tạm thời cho mỗi đỉnh, liên tục chốt đỉnh chưa chốt có khoảng cách tạm thời nhỏ nhất, rồi nới lỏng các cạnh của nó: cập nhật khoảng cách của một đỉnh kề mỗi khi tìm thấy đường đi ngắn hơn qua đỉnh hiện tại. Nhấn Phát ở trên để xem các khoảng cách giảm dần khi từng đỉnh được chốt.

Ý tưởng cốt lõi là tham lam: khi đã chọn đỉnh chưa chốt gần nhất, khoảng cách của nó là cuối cùng, vì mọi đường khác đến nó đều phải đi qua một đỉnh vốn đã xa hơn. Với hàng đợi ưu tiên dùng binary heap, Dijkstra chạy trong thời gian O((V + E) log V). Thuật toán yêu cầu trọng số không âm: hãy dùng Bellman-Ford nếu cạnh có thể âm.

Độ phức tạp thời gian và không gian

Cách cài đặtĐộ phức tạpGhi chú
Binary heapO((V + E) log V)Lựa chọn phổ biến và thực tế
Duyệt mảngO(V²)Đơn giản hơn; ổn với đồ thị dày
Không gianO(V)Khoảng cách + hàng đợi ưu tiên
Yêu cầuTrọng số không âmCạnh âm phá vỡ lựa chọn tham lam

Từng bước thực hiện

BướcĐiều gì xảy ra
1Đặt khoảng cách của đỉnh nguồn là 0 và của mọi đỉnh khác là vô cùng.
2Chọn đỉnh chưa chốt có khoảng cách tạm thời nhỏ nhất.
3Đánh dấu đã chốt: khoảng cách ngắn nhất của nó giờ là cuối cùng.
4Với mỗi đỉnh kề, tính khoảng cách qua đỉnh hiện tại + trọng số cạnh.
5Nếu giá trị đó nhỏ hơn khoảng cách hiện tại của đỉnh kề, nới lỏng nó.
6Lặp lại cho đến khi mọi đỉnh đến được đều đã được chốt.

Ví dụ minh họa

Đường đi ngắn nhất từ đỉnh nguồn A trên đồ thị với các cạnh A-B=4, A-C=1, C-B=2, C-D=5, B-D=1:

BướcChốtKhoảng cáchThao tác
0-A=0, B=∞, C=∞, D=∞Khởi tạo: đỉnh nguồn A=0, mọi đỉnh khác là vô cùng.
1A (0)B=4, C=1, D=∞Nới lỏng các cạnh từ A: đặt B=4, C=1.
2C (1)B=3, D=6Qua C: B=1+2=3 tốt hơn 4; D=1+5=6.
3B (3)D=4Qua B: D=3+1=4 tốt hơn 6.
4D (4)A=0, C=1, B=3, D=4Chốt D; không còn gì để nới lỏng. Xong.

Khi nào nên dùng thuật toán Dijkstra

Nên dùng khiTránh dùng khi
Mọi trọng số cạnh đều không âmCó cạnh có thể âm: hãy dùng Bellman-Ford
Bạn cần đường đi ngắn nhất từ một nguồn đến mọi đỉnhBạn cần đường đi ngắn nhất giữa mọi cặp đỉnh: Floyd-Warshall đơn giản hơn
Đồ thị có trọng số và bạn muốn khoảng cách chính xácĐồ thị không trọng số: BFS thông thường nhanh và đơn giản hơn
Bạn có sẵn một heap/hàng đợi ưu tiên tốtBạn muốn nhanh chóng đến một đích duy nhất bằng heuristic: hãy dùng A*

Code Dijkstra's Algorithm

Một cách cài đặt Dijkstra'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 Dijkstra's Algorithm bằng Python

Python
1import heapq2
3
4def dijkstra(graph, start):5    dist = {node: float("inf") for node in graph}6    dist[start] = 07    pq = [(0, start)]8    while pq:9        d, node = heapq.heappop(pq)10        if d > dist[node]:11            continue  # stale entry, a shorter path was already found12        for neighbor, weight in graph[node]:13            new_dist = d + weight14            if new_dist < dist[neighbor]:15                dist[neighbor] = new_dist16                heapq.heappush(pq, (new_dist, neighbor))17    return dist18
19
20graph = {21    "A": [("B", 4), ("C", 1)],22    "B": [("D", 1)],23    "C": [("B", 2), ("D", 5)],24    "D": [("E", 3)],25    "E": [],26}27
28for node, d in dijkstra(graph, "A").items():29    print(f"A -> {node}: {d}")
Chạy code này trong Playground Python

Câu hỏi thường gặp về thuật toán Dijkstra

Độ phức tạp thời gian của thuật toán Dijkstra là bao nhiêu?
Với hàng đợi ưu tiên dùng binary heap, thuật toán chạy trong O((V + E) log V). Phiên bản đơn giản duyệt mảng để tìm giá trị nhỏ nhất ở mỗi bước là O(V²), và thực tế có thể nhanh hơn trên đồ thị dày. Cả hai đều dùng O(V) bộ nhớ.
Vì sao Dijkstra không chạy đúng với trọng số cạnh âm?

Dijkstra giả định rằng khi đã chốt đỉnh chưa chốt gần nhất, khoảng cách đó là cuối cùng. Một cạnh âm về sau có thể tạo ra đường đi ngắn hơn đến một đỉnh đã chốt, phá vỡ giả định này. Với đồ thị có trọng số âm, hãy dùng thuật toán Bellman-Ford.

Dijkstra và BFS khác nhau như thế nào?

BFS tìm đường đi ngắn nhất theo số cạnh (mỗi cạnh coi như có trọng số 1), dùng một hàng đợi thông thường. Dijkstra tổng quát hóa điều này cho đồ thị có trọng số bằng cách luôn mở rộng đỉnh có tổng khoảng cách nhỏ nhất, dùng hàng đợi ưu tiên. Trên đồ thị không trọng số, hai thuật toán cho ra cùng các đường đi.

Dijkstra và tìm kiếm A* khác nhau như thế nào?

A* là Dijkstra cộng thêm một heuristic ước lượng khoảng cách còn lại đến đích, nhờ vậy hướng việc tìm kiếm về phía mục tiêu thay vì lan đều ra mọi hướng. Khi heuristic bằng 0, A* trở thành chính Dijkstra. Dùng A* khi bạn có một đích duy nhất và một heuristic chấp nhận được tốt; dùng Dijkstra khi bạn cần khoảng cách đến mọi đỉnh.

Khi nào nên dùng Dijkstra thay cho Bellman-Ford?
Dùng Dijkstra bất cứ khi nào mọi trọng số cạnh đều không âm: nó nhanh hơn với O((V + E) log V) so với O(V·E) của Bellman-Ford. Chỉ chọn Bellman-Ford khi cạnh có thể âm hoặc bạn cần phát hiện chu trình âm. Trên đồ thị không âm, Dijkstra gần như luôn là lựa chọn tốt hơn.
Dijkstra có thăm lại một đỉnh sau khi đã chốt không?

Không. Khi một đỉnh đã được chốt, khoảng cách của nó là cuối cùng và không bao giờ được nới lỏng lại. Một lỗi hay gặp khi cài đặt bằng heap là để lại các mục cũ trong hàng đợi ưu tiên sau khi khoảng cách của một đỉnh được cải thiện; bạn phải bỏ qua đỉnh vừa lấy ra nếu nó đã được chốt (khoảng cách lúc lấy ra lớn hơn khoảng cách đã ghi nhận). Quên bước kiểm tra này vẫn cho kết quả đúng nhưng tốn công vô ích.

Hình minh họa các ngôn ngữ lập trình của Coddy

Thành thạo thuật toán cùng Coddy

Bắt đầu