Lập trình · Thuật toán
Thuật toán Floyd-Warshall là gì: cách hoạt động, code và độ phức tạp
Trả lời nhanh
Thuật toán Floyd-Warshall tính đường đi ngắn nhất giữa mọi cặp nút trong đồ thị có trọng số. Nó giữ một ma trận khoảng cách V × V và lần lượt với mỗi nút k, kiểm tra xem đi qua k có làm đường từ i đến j ngắn hơn không: dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]). Thuật toán xử lý được trọng số âm, phát hiện chu trình âm và chạy trong thời gian O(V³) với bộ nhớ O(V²).
Floyd-Warshall giải bài toán gì
Đôi khi bạn cần khoảng cách giữa mọi cặp địa điểm chứ không chỉ từ một điểm xuất phát: bảng thời gian di chuyển giữa tất cả các thành phố, độ trễ giữa mọi cặp máy chủ, hoặc khả năng tiếp cận đầy đủ của một đồ thị phụ thuộc. Floyd-Warshall giải bài toán đường đi ngắn nhất mọi cặp này bằng một thuật toán quy hoạch động rất ngắn.
Thuật toán được Robert Floyd công bố năm 1962 (“Algorithm 97: Shortest path”), dựa trên thuật toán bao đóng bắc cầu của Stephen Warshall cũng năm 1962.
Ý tưởng bằng lời đơn giản
Đánh số các nút từ 0 đến V − 1. Sau khi xử lý nút k, dist[i][j] là đường đi ngắn nhất từ i đến j chỉ dùng các nút từ 0 đến k làm điểm dừng trung gian. Cho phép thêm một nút trung gian k cho hai lựa chọn: đường tốt nhất cũ không đi qua k, hoặc đường i → k → j gồm hai đường tốt nhất mà mỗi đường chỉ dùng các nút trước k.
- Điền ma trận: 0 trên đường chéo, trọng số cạnh ở những nơi có cạnh, vô cùng ở nơi còn lại.
- Với k từ 0 đến V − 1, với mọi cặp (i, j): nếu
dist[i][k] + dist[k][j] < dist[i][j]thì cập nhật. - Sau k cuối cùng, ma trận chứa mọi khoảng cách ngắn nhất. Giá trị âm trên đường chéo nghĩa là có chu trình âm.
Thứ tự các vòng lặp rất quan trọng: k phải là vòng lặp ngoài cùng.
Ví dụ từng bước
Đồ thị có hướng với 4 nút và các cạnh 0→1 (3), 0→3 (7), 1→0 (8), 1→2 (2), 2→0 (5), 2→3 (1), 3→0 (2). Ma trận ban đầu (hàng = nút đi, cột = nút đến):
| Từ \ đến | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 0 | 0 | 3 | ∞ | 7 |
| 1 | 8 | 0 | 2 | ∞ |
| 2 | 5 | ∞ | 0 | 1 |
| 3 | 2 | ∞ | ∞ | 0 |
- k = 0: các đường có thể đi qua nút 0. 1→3 thành 8 + 7 = 15, 2→1 thành 5 + 3 = 8 và 3→1 thành 2 + 3 = 5.
- k = 1: qua nút 1. 0→2 thành 3 + 2 = 5 và 3→2 thành 5 + 2 = 7.
- k = 2: qua nút 2. 0→3 cải thiện từ 7 xuống 5 + 1 = 6, 1→3 từ 15 xuống 2 + 1 = 3, và 1→0 từ 8 xuống 2 + 5 = 7.
- k = 3: qua nút 3. 1→0 cải thiện từ 7 xuống 3 + 2 = 5, 2→0 từ 5 xuống 1 + 2 = 3, và 2→1 từ 8 xuống 1 + 5 = 6.
Các hàng của ma trận cuối: 0 → [0, 3, 5, 6], 1 → [5, 0, 2, 3], 2 → [3, 6, 0, 1], 3 → [2, 5, 7, 0].
Cài đặt bằng Python
INF = float("inf")
def floyd_warshall(n, edges):
dist = [[0 if i == j else INF for j in range(n)] for i in range(n)]
nxt = [[j if i == j else None for j in range(n)] for i in range(n)]
for u, v, w in edges:
dist[u][v] = w
nxt[u][v] = v
for k in range(n):
for i in range(n):
for j in range(n):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
nxt[i][j] = nxt[i][k]
return dist, nxt
edges = [(0, 1, 3), (0, 3, 7), (1, 0, 8), (1, 2, 2),
(2, 0, 5), (2, 3, 1), (3, 0, 2)]
dist, nxt = floyd_warshall(4, edges)
for row in dist:
print(row)
# [0, 3, 5, 6]
# [5, 0, 2, 3]
# [3, 6, 0, 1]
# [2, 5, 7, 0]Ma trận nxt lưu bước đầu tiên trên đường tốt nhất từ i đến j, đủ để dựng lại bất kỳ đường đi nào:
def path(nxt, i, j):
if nxt[i][j] is None:
return [] # no path
route = [i]
while i != j:
i = nxt[i][j]
route.append(i)
return route
print(path(nxt, 1, 0)) # [1, 2, 3, 0]Nếu có nhiều nút và muốn nhanh hơn trong Python, cùng ba vòng lặp đó có thể viết bằng NumPy với một phép cập nhật vector hóa cho mỗi k: d = np.minimum(d, d[:, k, None] + d[None, k, :]).
Độ phức tạp thời gian và không gian
| Đại lượng | Chi phí | Lý do |
|---|---|---|
| Thời gian | O(V³) | Ba vòng lặp lồng nhau trên mọi nút, không phụ thuộc số cạnh. |
| Không gian | O(V²) | Ma trận khoảng cách (cộng ma trận bước kế tiếp tùy chọn). Cập nhật tại chỗ là an toàn. |
| Kích thước thực tế | vài trăm đến vài nghìn nút | V = 1.000 nghĩa là 10⁹ bước lặp trong cùng, chậm với Python thuần nhưng ổn với C hoặc NumPy. |
Với đồ thị thưa có trọng số không âm, chạy Dijkstra từ mọi nút tốn O(V × (V + E) log V), nhanh hơn O(V³) khi E nhỏ hơn nhiều so với V².
Biến thể và lỗi thường gặp
- Chu trình âm: sau các vòng lặp, nếu
dist[i][i] < 0với một i nào đó thì nút i nằm trên một chu trình âm và đường đi ngắn nhất qua nó không xác định. - Bao đóng bắc cầu (Warshall): dùng giá trị boolean và thay min/cộng bằng or/and:
reach[i][j] = reach[i][j] or (reach[i][k] and reach[k][j]). - Đường đi minimax và maximin: thay
+bằngmax(hoặcmin) để tìm, chẳng hạn, đường đi mà cạnh lớn nhất của nó nhỏ nhất có thể. - Chỉ dùng một số hữu hạn lớn thay cho vô cùng nếu chắc chắn tổng của hai số như vậy không tràn; trong Python,
float("inf")là lựa chọn an toàn. - Khởi tạo đường chéo bằng 0, và khi có các cạnh song song thì giữ trọng số nhỏ nhất.
Khi nào dùng và ứng dụng thực tế
| Nhu cầu | Lựa chọn tốt nhất |
|---|---|
| Mọi cặp, đồ thị dày hoặc nhỏ (V tới vài trăm) | Floyd-Warshall |
| Mọi cặp, đồ thị lớn và thưa, trọng số không âm | Dijkstra từ mọi nút |
| Mọi cặp, đồ thị thưa có trọng số âm | Thuật toán Johnson |
| Chỉ một nguồn | Dijkstra hoặc Bellman-Ford |
- Tính trước bảng khoảng cách giữa mọi trạm, kho hoặc thành phố để tra cứu sau.
- Phân tích đồ thị như độ trung tâm gần (closeness centrality) hoặc đường kính của một mạng nhỏ.
- Bài phỏng vấn: tìm thành phố có ít láng giềng nhất trong ngưỡng khoảng cách, evaluate division (tỷ lệ là tích trên đường đi), điều kiện tiên quyết môn học dưới dạng bao đóng bắc cầu.
Câu hỏi thường gặp
Floyd-Warshall có xử lý được trọng số cạnh âm không?
Có, miễn là không có chu trình âm. Nếu có, một phần tử trên đường chéo dist[i][i] sẽ âm, đó là cách bạn phát hiện nó.
Vì sao k phải là vòng lặp ngoài cùng?
Thuật toán xây dựng lời giải theo từng giai đoạn: giai đoạn k chỉ cho phép các nút từ 0 đến k làm điểm dừng trung gian. Mọi cặp phải được cập nhật cho giai đoạn k trước khi bất kỳ đường đi nào được phép dùng nút k + 1.
Floyd-Warshall có phải quy hoạch động không?
Có. Trạng thái là đường đi ngắn nhất từ i đến j dùng các nút trung gian tới k, và mỗi giai đoạn được tính từ giai đoạn trước. Chiều k được bỏ đi vì cập nhật tại chỗ không làm đổi kết quả.
Nên dùng Floyd-Warshall hay Dijkstra?
Dùng Dijkstra cho một nguồn hoặc cho đồ thị thưa lớn có trọng số không âm. Dùng Floyd-Warshall khi cần mọi cặp trên đồ thị nhỏ hoặc dày, hoặc khi có trọng số âm.