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ạp | Ghi chú |
|---|---|---|
| Thời gian | O(V · E) | V − 1 lượt qua toàn bộ E cạnh |
| Không gian | O(V) | Mỗi đỉnh lưu một khoảng cách |
| Trọng số âm | Hỗ trợ | Ưu điểm chính so với Dijkstra |
| Chu trình âm | Phát hiện được | Lượ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. |
| 2 | Lặp lại bước tiếp theo V − 1 lần. |
| 3 | Với mỗi cạnh (u → v, w), kiểm tra xem dist[u] + w < dist[v] hay không. |
| 4 | Nếu đúng, nới lỏng cạnh: đặt dist[v] = dist[u] + w. |
| 5 | Dừ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ượt | Khoả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 khi | Trá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
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}")Code Bellman-Ford Algorithm bằng JavaScript
1const nodes = ["A", "B", "C", "D", "E"];2const edges = [3 ["A", "B", 6], ["A", "C", 7], ["B", "D", 5], ["B", "E", -4],4 ["C", "D", -3], ["D", "B", -2], ["E", "A", 2],5];6
7function bellmanFord(source) {8 const dist = Object.fromEntries(nodes.map((n) => [n, Infinity]));9 dist[source] = 0;10 // Relax every edge V-1 times11 for (let i = 0; i < nodes.length - 1; i++) {12 for (const [u, v, w] of edges) {13 if (dist[u] + w < dist[v]) dist[v] = dist[u] + w;14 }15 }16 // One more pass: any improvement means a negative cycle17 for (const [u, v, w] of edges) {18 if (dist[u] + w < dist[v]) throw new Error("Negative cycle detected");19 }20 return dist;21}22
23console.log("Shortest distances from A:", bellmanFord("A"));Code Bellman-Ford Algorithm bằng Java
1import java.util.Arrays;2
3public class Main {4 public static void main(String[] args) {5 int n = 5;6 // Directed edges {from, to, weight}; negative weights allowed7 int[][] edges = {8 {0, 1, 6}, {0, 2, 7}, {1, 3, 5}, {1, 2, 8},9 {2, 4, 9}, {3, 1, -2}, {4, 3, 7}, {2, 3, -3}10 };11 int[] dist = new int[n];12 Arrays.fill(dist, Integer.MAX_VALUE);13 dist[0] = 0;14
15 // Relax every edge V-1 times16 for (int i = 0; i < n - 1; i++) {17 for (int[] e : edges) {18 if (dist[e[0]] != Integer.MAX_VALUE && dist[e[0]] + e[2] < dist[e[1]]) {19 dist[e[1]] = dist[e[0]] + e[2];20 }21 }22 }23
24 // One more pass: any improvement means a negative cycle25 boolean hasNegativeCycle = false;26 for (int[] e : edges) {27 if (dist[e[0]] != Integer.MAX_VALUE && dist[e[0]] + e[2] < dist[e[1]]) {28 hasNegativeCycle = true;29 }30 }31
32 System.out.println("Distances from 0: " + Arrays.toString(dist));33 System.out.println("Negative cycle: " + hasNegativeCycle);34 }35}Code Bellman-Ford Algorithm bằng C++
1#include <iostream>2#include <vector>3
4struct Edge {5 int from, to, weight;6};7
8int main() {9 const int INF = 1000000000;10 const int n = 5;11 std::vector<Edge> edges = {12 {0, 1, 6}, {0, 2, 7}, {1, 3, 5}, {2, 3, -3},13 {1, 4, -4}, {3, 4, 9}, {2, 1, -2},14 };15 std::vector<int> dist(n, INF);16 dist[0] = 0;17 // Relax every edge V-1 times18 for (int pass = 0; pass < n - 1; ++pass) {19 for (const Edge& e : edges) {20 if (dist[e.from] != INF && dist[e.from] + e.weight < dist[e.to]) {21 dist[e.to] = dist[e.from] + e.weight;22 }23 }24 }25 // One extra pass: any improvement means a negative cycle26 bool negativeCycle = false;27 for (const Edge& e : edges) {28 if (dist[e.from] != INF && dist[e.from] + e.weight < dist[e.to]) {29 negativeCycle = true;30 }31 }32 if (negativeCycle) {33 std::cout << "Negative cycle detected\n";34 } else {35 for (int v = 0; v < n; ++v) {36 std::cout << "Distance 0 -> " << v << ": " << dist[v] << "\n";37 }38 }39 return 0;40}Code Bellman-Ford Algorithm bằng C
1#include <stdbool.h>2#include <stdio.h>3
4#define N 55#define INF 10000000006
7typedef struct {8 int from, to, weight;9} Edge;10
11int main(void) {12 Edge edges[] = {13 {0, 1, 6}, {0, 2, 7}, {1, 3, 5}, {2, 3, -3},14 {1, 4, -4}, {3, 4, 9}, {2, 1, -2},15 };16 int m = sizeof(edges) / sizeof(edges[0]);17 int dist[N];18 for (int v = 0; v < N; v++) dist[v] = INF;19 dist[0] = 0;20 // Relax every edge V-1 times21 for (int pass = 0; pass < N - 1; pass++) {22 for (int i = 0; i < m; i++) {23 Edge e = edges[i];24 if (dist[e.from] != INF && dist[e.from] + e.weight < dist[e.to]) {25 dist[e.to] = dist[e.from] + e.weight;26 }27 }28 }29 // One extra pass: any improvement means a negative cycle30 bool negativeCycle = false;31 for (int i = 0; i < m; i++) {32 Edge e = edges[i];33 if (dist[e.from] != INF && dist[e.from] + e.weight < dist[e.to]) {34 negativeCycle = true;35 }36 }37 if (negativeCycle) {38 printf("Negative cycle detected\n");39 } else {40 for (int v = 0; v < N; v++) {41 printf("Distance 0 -> %d: %d\n", v, dist[v]);42 }43 }44 return 0;45}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?
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?
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?
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?
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?
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).