Dijkstra¶
Khái quát¶
Một trong những thuật toán tìm đường cơ bản mà người học Computer Science nào cũng trải qua là Dijkstra, đọ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.
Đặc điểm của thuật toán Dijkstra là xử lý được mọi đồ thị có hướng và có trọng số không âm, kể cả đồ thị có chu trình.
Ý tưởng chủ yếu của Dijkstra là sử 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.
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¶
Phác thảo đồ thị theo input như sau:
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.
Minh hoạ thực thi 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 input vào biến graph
.
graph
là danh sách các đỉnh kề, trong đó graph[u]
chứa các đỉnh kề với đỉnh u
.
Mỗi phần tử của graph
cũng là một cặp số: số thứ nhất là đỉnh kề v
và số thứ hai là khoảng cách từ u
đến v
.
Thực hiện Dijkstra¶
Bước 0: Khởi tạo
Khai báo d
là mảng lưu khoảng cách, trong đó d[v]
là khoảng cách ngắn nhất từ start
đến v
.
Khởi tạo giá trị vô cực cho cả mảng d
, ngoại trừ d[start] = 0
.
Khai báo trace
là mảng dùng để truy vết đường đi. Cụ thể, 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:
Đầu tiên, khai báo biến q
đóng vai trò hàng đợi, lần lượt lấy ra và xử lý phần tử đầu tiên cho đến khi không còn phần tử nào nữa.
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
, mang ý nghĩa tại đỉnhv
đang xét, khoảng cách ngắn nhất từstart
đếnv
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 hàng đợi q
bằng cách 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.
Output¶
d[finish]
là khoảng cách ngắn nhất cần tìm.
Để xuất đường đi, ta dựa theo mảng trace
và cho finish
"lùi" dần về start
.
Toàn bộ chương trình¶
Code đầy đủ được đặt tại GitHub.
Một vài lưu ý¶
-
Về đồ thị:
-
Thuật toán Dijkstra chỉ áp dụng được cho đồ thị có trọng số không âm. Nếu gặp phải trọng số âm, ta nên sử dụng thuật toán Bellman-Ford hoặc Johnson.
-
Thuật toán Dijkstra yêu cầu đồ thị phải liên thông. Nếu có đỉnh cô lập, thì phải xử lý riêng.
-
Thuật toán Dijkstra không xử lý các đường đi khác nhau có trọng số bằng nhau. Do đó, nếu muốn tìm tất cả đường đi thỏa yêu cầu, ta nên điều chỉnh thuật toán hoặc thử thuật toán khác.
-
Thuật toán Dijkstra tìm đường đi ngắn nhất đến tất cả các đỉnh trong đồ thị. Cho nên, nếu muốn cải tiến, ta cho thuật toán dừng khi đã đến được đỉnh đích. Bài toán ví dụ trên đã thực hiện điều này.
-
-
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ề, thì độ phức tạp thời gian là \(O(V^2 + E)\).
Nếu đồ thị quá lớn, nên cân nhắc sử dụng những cấu trúc dữ liệu tốt hơn như Fibonacci heap hoặc
priority_queue
.
-
Phần lớn 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ả. ↩