Khái niệm
Bellman-Ford là thuật toán tìm đường đi ngắn nhất từ một nguồn, có thể xử lý trọng số âm. Nó cũng phát hiện được chu trình âm (negative cycle) — chu trình mà tổng trọng số < 0.
So sánh với Dijkstra:
| Tiêu chí | Dijkstra | Bellman-Ford |
|---|---|---|
| Trọng số âm | ❌ Không hỗ trợ | ✅ Hỗ trợ |
| Phát hiện chu trình âm | ❌ | ✅ |
| Độ phức tạp | O((V+E) log V) | O(V·E) |
| Phù hợp | Trọng số không âm | Có thể có trọng số âm |
Ý tưởng: Thực hiện đúng V-1 vòng lặp, mỗi vòng thư giãn (relax) tất cả cạnh. Sau V-1 vòng, nếu vẫn còn cạnh có thể thư giãn → tồn tại chu trình âm.
graph LR;
A((A\ndist=0)) -- 1 --> B((B));
A -- 4 --> C((C));
B -- 3 --> C;
B -- "-2" --> D((D));
C -- 2 --> D;
A→B→D = 0+1+(−2) = −1 (ngắn hơn A→C→D = 0+4+2 = 6)
Cài đặt chuẩn
def bellman_ford(n, canh_co_huong, src):
"""
Tìm đường ngắn nhất từ src đến tất cả đỉnh.
canh_co_huong: [(u, v, w)] — cạnh có hướng từ u đến v, trọng số w
Trả về (dist[], co_chu_trinh_am)
"""
INF = float('inf')
dist = [INF] * n
dist[src] = 0
# V-1 vòng relax toàn bộ cạnh
for _ in range(n - 1):
cap_nhat = False
for u, v, w in canh_co_huong:
if dist[u] != INF and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
cap_nhat = True
if not cap_nhat: # Tối ưu: dừng sớm nếu không còn cập nhật
break
# Kiểm tra chu trình âm: vòng thứ V vẫn relax được → có chu trình âm
for u, v, w in canh_co_huong:
if dist[u] != INF and dist[u] + w < dist[v]:
return dist, True
return dist, False
# Ví dụ không có chu trình âm
canh = [(0,1,1),(0,2,4),(1,2,3),(1,3,-2),(2,3,2)]
dist, cycle = bellman_ford(4, canh, src=0)
print("Khoảng cách:", dist) # [0, 1, 4, -1]
print("Chu trình âm:", cycle) # False
# Ví dụ có chu trình âm: 1→2→3→1 tổng trọng số = -1
canh2 = [(0,1,0),(1,2,1),(2,3,-2),(3,1,0)]
_, cycle2 = bellman_ford(4, canh2, src=0)
print("Chu trình âm:", cycle2) # True
Khôi phục đường đi
def bellman_ford_voi_duong_di(n, canh, src):
"""Bellman-Ford kèm truy vết đường đi."""
INF = float('inf')
dist = [INF] * n
prev = [-1] * n
dist[src] = 0
for _ in range(n - 1):
for u, v, w in canh:
if dist[u] != INF and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
prev[v] = u
def lay_duong_di(dst):
if dist[dst] == INF:
return None # Không tới được
path, cur = [], dst
while cur != -1:
path.append(cur)
cur = prev[cur]
return path[::-1]
return dist, lay_duong_di
canh = [(0,1,1),(0,2,4),(1,2,3),(1,3,-2),(2,3,2)]
dist, get_path = bellman_ford_voi_duong_di(4, canh, src=0)
print(f"0→3: {dist[3]}, đường đi: {get_path(3)}") # -1, [0, 1, 3]
Bài tập luyện tập
Dễ — Kiểm tra tồn tại chu trình âm
def co_chu_trinh_am(n, canh):
"""Kiểm tra đồ thị có hướng có chu trình âm không (bắt đầu từ mọi nguồn)."""
INF = float('inf')
dist = [0] * n # Trick: khởi tạo 0 ≡ thêm một siêu nguồn kết nối tất cả
for _ in range(n - 1):
for u, v, w in canh:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
for u, v, w in canh:
if dist[u] + w < dist[v]:
return True
return False
canh1 = [(0,1,1),(1,2,2),(2,0,-4)] # Chu trình âm: 0+1+2-4 = -1
canh2 = [(0,1,1),(1,2,2),(2,0,-2)] # Chu trình không âm: 0+1+2-2 = 1
print(co_chu_trinh_am(3, canh1)) # True
print(co_chu_trinh_am(3, canh2)) # False
Trung bình — Đường đi ngắn nhất qua tối đa k cạnh
def duong_ngan_nhat_toi_da_k_canh(n, canh, src, dst, k):
"""
Tìm đường đi ngắn nhất từ src đến dst, sử dụng tối đa k cạnh.
Biến thể của Bellman-Ford: chỉ chạy k vòng.
"""
INF = float('inf')
prev = [INF] * n
prev[src] = 0
for _ in range(k):
curr = prev[:]
for u, v, w in canh:
if prev[u] != INF and prev[u] + w < curr[v]:
curr[v] = prev[u] + w
prev = curr
return prev[dst] if prev[dst] != INF else -1
# Bay từ 0→3, tối đa 2 chặng
canh = [(0,1,100),(0,2,500),(1,2,100),(1,3,600),(2,3,200)]
print(duong_ngan_nhat_toi_da_k_canh(4, canh, 0, 3, 2)) # 700 (0→1→3 hoặc 0→2→3 không phải vì cần 2 cạnh)
print(duong_ngan_nhat_toi_da_k_canh(4, canh, 0, 3, 1)) # 600 (0→1→3 dùng 2 cạnh nhưng k=1 thì chỉ 1 cạnh)
Khó — Phát hiện cơ hội kiếm lời tùy ý (Chu trình âm trong đổi tiền)
import math
def co_co_hoi_kinh_doanh(n, ty_gia):
"""
Kiểm tra có thể đổi tiền lòng vòng để kiếm lời không.
ty_gia[i][j] = tỷ giá quy đổi từ đồng i sang đồng j.
Kiếm lời = tích tỷ giá > 1 = chu trình âm trong đồ thị log.
Biến đổi: log(product) > 0 ↔ sum(-log) < 0 ↔ chu trình âm với trọng số -log.
"""
canh = []
for i in range(n):
for j in range(n):
if ty_gia[i][j] > 0:
# Đổi product > 1 thành sum < 0 bằng -log
canh.append((i, j, -math.log(ty_gia[i][j])))
dist = [0.0] * n
for _ in range(n - 1):
for u, v, w in canh:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
for u, v, w in canh:
if dist[u] + w < dist[v] - 1e-9:
return True
return False
# Ví dụ: USD, EUR, GBP
# USD→EUR→GBP→USD = 1.2 × 0.9 × 1.1 = 1.188 > 1 → kiếm lời được
ty_gia = [
[1.0, 1.2, 0.8],
[0.85, 1.0, 0.9],
[1.25, 1.1, 1.0]
]
print(co_co_hoi_kinh_doanh(3, ty_gia)) # True hoặc False tùy tỷ giá
Kết luận
Bellman-Ford linh hoạt hơn Dijkstra nhờ xử lý được trọng số âm và phát hiện chu trình âm. Hai thuật toán này bổ sung cho nhau: Dijkstra nhanh hơn khi không có trọng số âm. Tiếp theo hãy học Cây khung nhỏ nhất (MST) — ứng dụng kết hợp DSU và sắp xếp cạnh.
Bình luận