Menu
CoddyTech

Thuật toán Bellman-Ford

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

Bellman-Ford tìm đường đi ngắn nhất từ một đỉnh nguồn đến mọi đỉnh khác, và khác với Dijkstra, thuật toán vẫn chạy đúng khi một số cạnh có trọng số âm. Cách làm là nới lỏng (relaxation) theo kiểu vét cạn: liên tục quét qua mọi cạnh, nếu đi qua cạnh đó cho khoảng cách ngắn hơn thì cập nhật khoảng cách của đỉnh đích. Nhấn Phát ở trên để xem các khoảng cách tạm thời giảm dần qua từng lượt.

Sau V - 1 lượt duyệt qua tất cả các cạnh, mọi đường đi ngắn nhất (có tối đa V - 1 cạnh) đều đã được tìm thấy. Vì vậy độ phức tạp là O(V · E), chậm hơn Dijkstra, nhưng thuật toán xử lý được trọng số âm và phát hiện được chu trình trọng số âm (nếu lượt thứ V vẫn còn nới lỏng được một cạnh thì không tồn tại đường đi ngắn nhất).

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

Tiêu chíĐộ phức tạpGhi chú
Thời gianO(V · E)V − 1 lượt qua toàn bộ E cạnh
Không gianO(V)Mỗi đỉnh lưu một khoảng cách
Trọng số âmHỗ trợƯu điểm chính so với Dijkstra
Chu trình âmPhát hiện đượcLượt thứ V còn nới lỏng được nghĩa là có chu trình âm

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.
2Lặp lại bước tiếp theo V − 1 lần.
3Với mỗi cạnh (u → v, w), kiểm tra xem dist[u] + w < dist[v] hay không.
4Nếu đúng, nới lỏng cạnh: đặt dist[v] = dist[u] + w.
5Dừng sớm nếu một lượt đầy đủ không thay đổi gì.

Ví dụ minh họa

Đỉnh nguồn S trên 4 đỉnh S, A, B, C với các cạnh S→A (4), S→B (5), A→C (3), B→A (-3), được nới lỏng theo đúng thứ tự đó. Ban đầu khoảng cách là S=0, các đỉnh còn lại là ∞:

LượtKhoảng cách [S, A, B, C]Thao tác
Khởi tạo[0, ∞, ∞, ∞]Khoảng cách của đỉnh nguồn là 0, mọi đỉnh khác là vô cùng.
1[0, 2, 5, 7]Nới lỏng S→A thành 4, S→B thành 5, A→C thành 7, rồi B→A hạ A xuống 5 + (-3) = 2.
2[0, 2, 5, 5]Giờ A→C cải thiện C thành 2 + 3 = 5; không cạnh nào khác nới lỏng được.
3[0, 2, 5, 5]Không cạnh nào nới lỏng được, nên khoảng cách đã là cuối cùng: đến A tốn 2 qua S→B→A.

Khi nào nên dùng Bellman-Ford

Nên dùng khiTránh dùng khi
Trọng số cạnh có thể âm.Mọi trọng số đều không âm (Dijkstra nhanh hơn).
Bạn cần phát hiện chu trình trọng số âm.Đồ thị rất lớn và dày đặc: O(V · E) quá chậm.
Đồ thị nhỏ hoặc thưa.Bạn chỉ cần một truy vấn từ nguồn đến đích trên một đồ thị lớn.
Bạn cần một giải pháp cơ sở đơn giản, dễ cài đặt.Trọng số không âm và độ trễ là yếu tố quan trọng.

Code Bellman-Ford Algorithm

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

Python
1def bellman_ford(vertices, edges, start):2    dist = {v: float("inf") for v in vertices}3    dist[start] = 04    # Relax every edge V-1 times5    for _ in range(len(vertices) - 1):6        for u, v, w in edges:7            if dist[u] + w < dist[v]:8                dist[v] = dist[u] + w9    # One more pass: any improvement means a negative cycle10    for u, v, w in edges:11        if dist[u] + w < dist[v]:12            raise ValueError("Graph contains a negative-weight cycle")13    return dist14
15
16vertices = ["A", "B", "C", "D", "E"]17edges = [18    ("A", "B", 6), ("A", "C", 7), ("B", "D", 5), ("B", "E", -4),19    ("C", "D", -3), ("D", "B", -2), ("E", "D", 7),20]21
22for node, d in bellman_ford(vertices, edges, "A").items():23    print(f"A -> {node}: {d}")
Chạy code này trong Playground Python

Câu hỏi thường gặp về Bellman-Ford

Độ phức tạp thời gian của Bellman-Ford là bao nhiêu?
Bellman-Ford chạy trong thời gian O(V · E): thuật toán thực hiện V - 1 lượt, mỗi lượt nới lỏng toàn bộ E cạnh. Như vậy chậm hơn O((V + E) log V) của Dijkstra, đó là cái giá để hỗ trợ trọng số cạnh âm.
Khi nào nên dùng Bellman-Ford thay cho Dijkstra?

Dùng Bellman-Ford khi đồ thị có thể có cạnh trọng số âm, hoặc khi bạn cần phát hiện chu trình trọng số âm. Dijkstra nhanh hơn nhưng chỉ đúng với trọng số không âm. Một ứng dụng thực tế phổ biến là phát hiện cơ hội chênh lệch tỷ giá (arbitrage) khi đổi tiền tệ, nơi chu trình âm có ý nghĩa quan trọng.

Bellman-Ford phát hiện chu trình âm bằng cách nào?
Sau V - 1 lượt nới lỏng, mọi đường đi ngắn nhất đã được xác định nếu không có chu trình âm. Nếu thêm một lượt nữa vẫn nới lỏng được một cạnh, nghĩa là có thể đi tới một chu trình trọng số âm, và đường đi ngắn nhất không xác định (bạn có thể đi vòng mãi để giảm chi phí).
Bellman-Ford và Floyd-Warshall khác nhau như thế nào?
Bellman-Ford tính đường đi ngắn nhất từ một đỉnh nguồn trong O(V · E), còn Floyd-Warshall tính đường đi ngắn nhất giữa mọi cặp đỉnh trong O(V³). Trên đồ thị dày, chạy Bellman-Ford từ mọi đỉnh nguồn tốn O(V² · E), nên Floyd-Warshall thường là lựa chọn tốt hơn khi bạn cần mọi cặp. Cả hai đều xử lý được cạnh âm và có thể báo hiệu chu trình âm.
Vì sao Bellman-Ford cần đúng V - 1 lượt?
Trong đồ thị không có chu trình âm, mọi đường đi ngắn nhất dùng tối đa V - 1 cạnh, vì một đường đi qua nhiều hơn V đỉnh chắc chắn phải lặp lại một đỉnh. Mỗi lượt bảo đảm có thêm ít nhất một cạnh của mọi đường đi ngắn nhất được nới lỏng đúng, nên V - 1 lượt là đủ để chốt tất cả. Một hiểu lầm phổ biến là chạy thêm lượt sẽ cải thiện kết quả: điều đó không bao giờ xảy ra trừ khi có chu trình âm.
Bellman-Ford có thể dừng sớm không?
Có. Nếu một lượt đầy đủ qua tất cả các cạnh không nới lỏng được gì, mọi khoảng cách đã tối ưu và bạn có thể dừng trước khi đủ V - 1 lượt. Tối ưu dừng sớm này thường giúp thuật toán nhanh hơn nhiều trên các đồ thị hội tụ nhanh, dù cận trên trong trường hợp xấu nhất vẫn là O(V · E).
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