Dijkstra¶
Khái quát¶
Dijkstra (1) là thuật toán tìm đường đi ngắn nhất từ một đỉnh đến tất cả đỉnh còn lại trong đồ thị có trọng số không âm.
- Đọc là /ˈdaɪkstrəz/ DYKE-strəz. Giang hồ thường đọc /đís sờ tra/.
Thuật toán này được nhà khoa học máy tính người Hà Lan Edsger Wybe Dijkstra thai nghén năm 1956 và công bố năm 1959.
Ý tưởng chính của Dijkstra là dùng kỹ thuật tham lam (greedy) để tìm đỉnh tiếp theo tốt nhất và hy vọng rằng kết quả cuối cùng là giải pháp tốt nhất cho bài toán.
Dijkstra có thể áp dụng cho đồ thị:
- Có hướng hoặc vô hướng
- Có trọng số không âm
- Có thể có hoặc không có chu trình
Bài toán¶
Yêu cầu¶
Tìm đường đi ngắn nhất từ đỉnh start đến đỉnh finish bằng thuật toán Dijkstra.1
Input¶
Output¶
Giải thích¶
Input:
- Dòng đầu tiên chứa bốn số lần lượt là: 6 đỉnh, 10 cạnh, đỉnh xuất phát là 1 và đỉnh đích là 5.
- Mỗi dòng tiếp theo chứa hai đỉnh theo chiều từ u đến v và khoảng cách giữa chúng.
Output:
- Dòng đầu tiên là khoảng cách ngắn nhất từ đỉnh start đến đỉnh finish. Cụ thể, khoảng cách ngắn nhất từ đỉnh 1 đến đỉnh 5 là 6.
- Dòng thứ hai liệt kê các đỉnh nằm trên đường đi tìm được: 1 -> 2 -> 4 -> 3 -> 6 -> 5.
Phác thảo đồ thị theo input như sau:
Minh hoạ thuật toán¶
Dựa theo ý tưởng đã nêu ở đầu bài, ta tìm đỉnh v tiếp theo sao cho khoảng cách từ đỉnh xuất phát đến đỉnh v là ngắn nhất.
Bảng 1 dưới đây minh họa từng bước ý tưởng này:
-
Hàng tiêu đề là các đỉnh v = 1..6
-
Trong mỗi ô, cặp số (d, u) bao gồm:
- d là khoảng cách (được liên tục cộng dồn) từ đỉnh start đến đỉnh v tương ứng trên hàng tiêu đề.
- u là đỉnh liền trước đỉnh v tương ứng trên hàng tiêu đề.
-
Dấu * dùng để đánh dấu đỉnh v được chọn để đi tiếp, mà tại v đó, giá trị d là nhỏ nhất trong số các d cùng hàng.
Bảng 1. Thực hiện thuật toán Dijkstra
1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|
(0, -)* | (∞, -) | (∞, -) | (∞, -) | (∞, -) | (∞, -) |
- | (1, 1)* | (∞, -) | (∞, -) | (∞, -) | (∞, -) |
- | - | (6, 2) | (3, 2)* | (∞, -) | (8, 2) |
- | - | (4, 4)* | - | (7, 4) | (8, 2) |
- | - | - | - | (7, 4) | (5, 3)* |
- | - | - | - | (6, 6)* | - |
Viết chương trình¶
Đọc input¶
Lưu dòng đầu tiên của input vào bốn biến number_of_vertices
, number_of_edges
, start
, finish
.
Đọc các dòng còn lại của input và biểu diễn các cạnh của đồ thị bằng ma trận kề graph
, với graph[u][v]
là trọng số của cạnh u -> v.
Thực hiện Dijkstra¶
Bước 0: Khởi tạo
Khởi tạo mảng D
dùng để lưu khoảng cách, trong đó D[v]
là khoảng cách ngắn nhất từ đỉnh start
đến đỉnh v
.
Khởi tạo mảng trace
dùng để truy vết đường đi, trong đó trace[v] = u
nghĩa là trước đỉnh v
là đỉnh u
, với v
và u
đề cập trong bảng 1.
Bước 1: Khởi tạo hàng đợi
Đầu tiên, khởi tạo biến q
đóng vai trò hàng đợi. Mỗi phần tử của q
là một cặp số nguyên:
- Số thứ nhất là khoảng cách
d
. - Số thứ hai là đỉnh
v
.
Cặp số này có ý nghĩa: tại đỉnh v
đang xét, khoảng cách ngắn nhất từ đỉnh start
đến đỉnh v
là d
.
Đối với C++, ta tận dụng kiểu set
cho q
vì hai lẽ:
- Mỗi phần tử trong
set
là duy nhất. - Kiểu
set
tự động sắp xếp tăng dần. Điều này giúp cho phần tử đầu tiên luôn là phần tử nhỏ nhất, đồng nghĩa khoảng cáchd
là nhỏ nhất (so với cácd
khác trongq
).
Bước 2: Duyệt đỉnh và cập nhật đường đi
Duyệt hàng đợi q
, lặp các thao tác sau:
Xét phần tử đầu tiên của hàng đợi q
. Lưu đỉnh của phần tử đầu tiên này vào biến tạm u
.
Thiết lập điều kiện để dừng vòng lặp: đó là khi đến được đích, tức u == finish
.
Duyệt các đỉnh kề với đỉnh u
:
Dùng biến v
để biểu thị đỉnh v, lưu tại vị trí i
của graph[u]
.
Dùng biến w
để biểu thị khoảng cách từ u
đến v
.
Xét xem một trong hai khoảng cách sau, cái nào là ngắn hơn:
1. D[v]
là khoảng cách ngắn nhất từ start
đến v
, hiện đang lưu trong mảng d
.
2. D[u] + w
là khoảng cách từ start
đến u
cộng với khoảng cách từ u
đến v
.
Nói cách khác, để đến được đỉnh v
, ta xét xem đường đi mới "quá cảnh" tại u
có tốt hơn không. Nếu có, ta thực hiện:
- Gỡ bỏ phần tử {D[v], v}
ra khỏi hàng đợi q
.
- Cập nhật lại giá trị của D[v]
.
- Ghi nhận vào mảng trace
, nghĩa là con đường đến v
mà ghé qua u
thì tốt hơn.
- Thêm phần tử mới {D[v], v}
vào hàng đợi q
.
Ví dụ:
Giá trị đang lưu trong mảng d
là D[6] = 8, nghĩa là khoảng cách từ đỉnh 1 đến đỉnh 6 là 8.
Xét đường đi đến đỉnh 6 mà "quá cảnh" tại đỉnh 3. Ta nhận thấy:
D[3] + w = 4 + 1 = 5 < D[6], với w là khoảng cách từ đỉnh 3 đến đỉnh 6. Điều này đồng nghĩa đường đi đến đỉnh 6 mà qua đỉnh 3 thì tốt hơn.
Như vậy, ta gán giá trị mới cho D[6]
là D[6] = D[3] + w = 5.
Hình sau cho thấy đường đi đến đỉnh 6 mà qua đỉnh 3 là tốt hơn so với đường đi 1 -> 2 -> 6.
Truy vết đường đi¶
Truy vết từ đỉnh start
đến đỉnh finish
và lưu vào ngăn xếp path
.
In kết quả¶
Dựa vào ngăn xếp path
, ta in ra đường đi bằng cách lấy từng phần tử ra khỏi ngăn xếp.
Mã nguồn¶
Code đầy đủ được đặt tại GitHub.
Một vài lưu ý về độ phức tạp¶
Gọi \(V\) là số đỉnh, \(E\) là số cạnh.
Nếu thực thi Dijkstra trên ma trận kề thì độ phức tạp thời gian là \(O(V^2)\).
Nếu thực thi trên danh sách kề với hàng đợi ưu tiên (heapq
hoặc priority_queue
), độ phức tạp là \(O((V+E)logV)\), tốt hơn \(O(V^2)\) khi đồ thị thưa.
Nếu đồ thị quá lớn thì có thể cân nhắc sử dụng những cấu trúc dữ liệu tốt hơn như Fibonacci heap, giúp giảm thao tác cập nhất xuống \(O(1)\).
-
Phần nào đó nội dung của bài toán này được tham khảo từ tài liệu lập trình của Võ Ngọc Hà Sơn vnhason@gmail.com nhưng giả vờ quên xin phép tác giả. ↩