Lưu ý: Thuật toán Dijkstra chỉ đúng khi trọng số của tất cả các cạnh trong đồ thị đều không âm.
Dijkstra là một trong những thuật toán tiêu biểu nhất và cũng được sử dụng nhiều nhất trong các bài toán liên quan tới tìm đường đi ngắn nhất trên đồ thị có trọng số không âm. Ý tưởng cốt lõi của thuật toán này chính là việc so sánh đường đi từ \(S → v\) đã được xét từ trước và \(S → u → v\) đang xét ở hiện tại để tối ưu hóa dần. Khác với thuật toán Bellman-Ford, Dijkstra hoạt động bằng cách duy trì tập hợp các đỉnh \(d[u]\) mà ta chắc chắn \(d[u]\) chính là đường đi ngắn nhất từ \(S\) tới \(u\).
1. Bài toán 1
Cho đơn đồ thị vô hướng \(n\) đỉnh, \(m\) cạnh. Hai đỉnh \(S, T\) lần lượt là đỉnh xuất phát và kết thúc.
Yêu cầu: Tìm đường đi ngắn nhất giữa 2 đỉnh \(S, T\) đã cho.
Input:
- Dòng đầu tiên ghi 4 số nguyên dương \(n, m, S, T.\) \((n, m ≤ 100000)\)
- \(m\) dòng tiếp theo, mỗi dòng ghi 3 số nguyên \(u, v, c\) với ý nghĩa: tồn tại con đường đi từ \(u\) đến \(v\) và ngược lại có quãng đường là \(c\). \((u, v ≤ 100000, c ≤ 10000000)\)
Output:
- Dòng đầu tiên in ra quãng đường nhỏ nhất đi từ \(S\) tới \(T.\)
- Dòng tiếp theo ghi hành trình thỏa mãn tìm được: Bắt đầu từ \(S\), tiếp theo là danh sách đỉnh tồn tại trên quãng đường ngắn nhất từ \(S\) tới \(T\), cuối cùng là đỉnh \(T\).
Sample Input:
6 9 1 6 1 3 2 1 4 3 1 5 6 4 5 7 4 6 2 1 2 1 3 2 4 2 5 3 6 3 10
Sample Output:
5 1 4 6
Phân tích bài toán:
- Gọi \(d[v]\) là khoảng cách ngắn nhất từ \(u\) tới \(v\). Khởi tạo tất cả các giá trị \(d[v]\) là \(+∞\). Xuất phát từ \(u\), coi quãng đường từ \(u\) đến chính nó là 0.
- Chọn \(v\) với \(v\) là \(d[v]\) nhỏ nhất trong mảng \(d\) (sau này, ta sẽ không xét tới đỉnh \(v\) nữa). Để làm được việc này, ta phải sử dụng priority_queue hoặc set để kiểm tra (*). Ta có thể khai báo priority_queue như sau:
#define pii pair<int, int> priority_queue<pii, vector<pii>, greater<pii>> q; // các phần tử của priority_queue sẽ được lưu dưới dạng pair {d[v], v} // và được tự động sort theo chiều tăng dần của d[v].
(*) Do thao tác của set chậm hơn priority_queue, nên ở đây mình sẽ chỉ nhắc tới cài đặt Dijkstra bằng priority_queue.
- Lần lượt xét tới các đỉnh \(x\) kề \(v\). Nếu khoảng cách từ \(S → v → x\) đang xét ngắn hơn khoảng cách từ \(S → x\) nhỏ nhất đang được ghi nhận thì ta sẽ cập nhật như sau:
if (d[x] > d[v] + c) { d[x] = d[v] + c; pq.push(make_pair(d[v], v)); }
- Ta có mã giả Dijkstra như sau:
// quy ước: // push(d[x], x): cho (d[x], x) vào priority_queue // top(): phần tử đầu tiên của priority_queue // pop(): xóa phần tử đầu tiên ở priority_queue void dijkstra(s): gán tất cả giá trị d[u] là +∞ d[s] = 0 // coi khoảng cách từ s tới chính nó là 0 push(d[s], s) trong khi priority_queue chưa rỗng: u = front().second c = front().first // xét đỉnh có d[u] nhỏ nhất trong priority_queue pop() if (c > d[u]) bỏ qua // nếu như đỉnh u đã cố định thì bỏ qua for v kề u: if (d[v] > d[u] + c): d[v] = d[u] + c push(d[v], v)
- Để truy vết, ta sử dụng kỹ thuật giống kỹ thuật đã từng nhắc tới ở bài DFS.
- Thứ tự duyệt đỉnh của Dijkstra được hình dung như sau:
- Gán \(d[1] = 0\), tất cả các đỉnh còn lại là \(+∞\)
-
- Cố định nhãn đỉnh 1 lại, các đỉnh 4, 3, 2, 5 lần lượt được sửa nhãn như sau:
-
- Cố định 2, sửa đỉnh 5 như sau:
-
- Cố định 3, sửa đỉnh 6 như sau:
-
- Cố định 4, sửa đỉnh 6 như sau:
-
- Cố định 6:
→ Đường đi ngắn nhất từ 1 đến 6 là 5.
Code mẫu:
#include<bits/stdc++.h> using namespace std; #define pii pair<int, int> #define inf 1e18 #define int long long const int N = 1e5 + 2; int n, m, S, T; vector<pii> adj[N]; int trace[N], d[N]; void dijkstra() { for (int i = 1; i <= n; i++) { d[i] = inf; // gán toàn bộ mảng d có giá trị là +∞ } d[S] = 0; // gán đỉnh bắt đầu là 0 priority_queue<pii, vector<pii>, greater<pii>> q; // các phần tử của priority_queue sẽ được lưu dưới dạng pair {d[v], v} // và được tự động sort theo chiều giảm dần của d[v]. q.push({d[S], S}); // đẩy đỉnh ban đầu và quãng đường tới chính nó vào priority_queue while (q.size()) { // khi priority_queue chưa rỗng int c = q.top().first; int u = q.top().second; q.pop(); // xét đỉnh có d[u] nhỏ nhất trong priority_queue if (c > d[u]) { continue; // nếu d[u] đã được cố định -> bỏ qua đỉnh này } for (auto v : adj[u]) { if (d[v.first] > d[u] + v.second) { d[v.first] = d[u] + v.second; trace[v.first] = u; q.push({d[v.first], v.first}); } } } cout << d[T] << 'n'; } void findPath() { vector<int> path; while (true) { path.push_back(T); if (T == S) { break; } T = trace[T]; } for (int i = path.size() - 1; i >= 0; i--) { cout << path[i] << " "; } } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> m >> S >> T; for (int i = 1; i <= m; i++) { int u, v, c; cin >> u >> v >> c; adj[u].push_back(make_pair(v, c)); // đỉnh v kề u có trọng số c adj[v].push_back(make_pair(u, c)); // đỉnh u kề v có trọng số c } dijkstra(); findPath(); }
→ Độ phức tạp thuật toán: \(O((V + E) * log(V))\)
2. Bài toán 2
Nguồn bài: VM7WC ’15 #4 Gold – Chain Rule
Tóm tắt đề:
Cho đồ thị vô hướng có trọng số \(n\) đỉnh, \(m\) cạnh. Xuất phát từ đỉnh \(0\), gọi \(d[u]\) là thời gian ngắn nhất đi từ \(0\) đến \(n -1\) \((0 ≤ u ≤ n -1)\) và phải đi qua đỉnh \(u\). Tìm giá trị lớn nhất của \(d[0], d[1]….d[n -1].\)
Input:
- Dòng đầu tiên ghi 2 số \(n, m.\)
- \(m\) dòng tiếp theo, mỗi dòng ghi 3 số \(u, v, t\) với ý nghĩa: tồn tại con đường 2 chiều nối giữa \(u\) và \(v\), và thời gian để đi hết con đường đó (theo cả 2 chiều) đều là \(t\).
Output:
- In ra 1 số duy nhất, biểu diễn giá trị lớn nhất của \(d[0], d[1]…d[n -1].\)
Sample Input:
5 5 0 1 5 1 2 4 0 3 8 2 3 2 4 2 3
Sample Output:
13
Phân tích bài toán:
- Nhận xét: \(d[u]\) = đường đi ngắn nhất từ \(0\) đến \(u\) \(+\) đường đi ngắn nhất từ \(u\) đến \(n -1\).
- Do đây là đồ thị vô hướng, đường đi ngắn nhất từ \(u\) đến \(n -1\) cũng chính là đường đi ngắn nhất từ \(n – 1\) đến \(u\). Qua đó, cách làm bài này có thể được hình dung như sau:
- Gọi \(d1[u]\) là đường đi ngắn nhất bắt đầu từ đỉnh \(0\) đến \(u\)
- Gọi \(d2[u]\) là đường đi ngắn nhất bắt đầu từ đỉnh \(n -1\) đến \(u\)
- Kết quả bài toán chính là \(max(d1[0] + d2[0], d1[1] + d2[1], d1[2] + d2[2]…d1[n -1] + d2[n -1])\)
#include<bits/stdc++.h> using namespace std; #define pii pair<int, int> #define inf 1e18 #define int long long const int N = 1e5 + 2; int n, m; vector<pii> adj[N]; int d1[N], d2[N]; void dijkstra1() { for (int i = 0; i < n; i++) { d1[i] = inf; } d1[0] = 0; priority_queue<pii, vector<pii>, greater<pii>> q; q.push({d1[0], 0}); while (q.size()) { int c = q.top().first; int u = q.top().second; q.pop(); if (c > d1[u]) { continue; } for (auto v : adj[u]) { if (d1[v.first] > d1[u] + v.second) { d1[v.first] = d1[u] + v.second; q.push({d1[v.first], v.first}); } } } } void dijkstra2() { for (int i = 0; i < n; i++) { d2[i] = inf; } d2[n - 1] = 0; priority_queue<pii, vector<pii>, greater<pii>> q; q.push({d2[n - 1], n - 1}); while (q.size()) { int c = q.top().first; int u = q.top().second; q.pop(); if (c > d2[u]) { continue; } for (auto v : adj[u]) { if (d2[v.first] > d2[u] + v.second) { d2[v.first] = d2[u] + v.second; q.push({d2[v.first], v.first}); } } } } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> m; for (int i = 1; i <= m; i++) { int u, v, c; cin >> u >> v >> c; adj[u].push_back(make_pair(v, c)); adj[v].push_back(make_pair(u, c)); } dijkstra1(); // tìm đường đi ngắn nhất từ 0 đến u dijkstra2(); // tìm đường đi ngắn nhất từ n - 1 đến u int res = -inf; for (int i = 0; i < n; i++) { res = max(res, d1[i] + d2[i]); } cout << res; }
3. Khi nào sử dụng thuật toán Dijkstra?
- Một trong những ứng dụng tiêu biểu nhất của thuật toán Dijkstra chính là tìm đường đi ngắn nhất giữa 2 đỉnh bất kỳ trong đồ thị có trọng số không âm với độ phức tạp không lớn và có thể linh hoạt biến tấu đi để phù hợp với yêu cầu đề bài. Thế nhưng, nhược điểm của thuật toán này chính là việc nó chỉ có thể hoạt động khi phải cố định được đỉnh bắt đầu, nên tùy theo yêu cầu đề cũng như mục đích sử dụng ta sẽ cân nhắc xem liệu có nên sử dụng thuật toán này cho bài hay không.
4. Luyện tập
Đây là một số bài tập luyện tập trong sách Competitive Programming Advanced của Code Dream, đăng ký mua ngay để được luyện tập nhiều dạng bài về thuật toán khác, chấm bài online tại http://oj.codedream.edu.vn
ÁP DỤNG GIẢI BÀI SCHOOL
Phân tích bài toán:
- Để giải quyết được bài này, ta sẽ phải sử dụng quy hoạch động và Dijkstra.
- Gọi \(d[u]\) là đường đi ngắn nhất từ \(1\) đến \(u\), và \(f[u]\) chính là số cách đi từ \(1\) đến \(u\) ngắn nhất.
- Ta có công thức QHĐ như sau:
- Bài toán con: \(f[1] = 1\), \(d[1] = 0\).
- Gọi \(v\) là đỉnh đang xét, \(c\) là trọng số của cạnh \(u-v\) thì:
- Nếu \(d[v] > d[u] + c\) thì gán lại \(d[v] = d[u] + c\) và \(f[v] = f[u]\).
- Nếu \(d[v] = d[u] + c\) thì \(f[v] += f[u]\)
Code mẫu:
#include<bits/stdc++.h> using namespace std; #define int long long #define inf 1e18 #define pii pair<int, int> const int N = 5005; int d[N], f[N]; int n, m; vector<pii> adj[N]; void dijkstra() { priority_queue<pii, vector<pii>, greater<pii>> q; for (int i = 1; i <= n; i++) d[i] = inf; d[1] = 0; f[1] = 1; // bài toán con q.push({d[1], 1}); while (q.size()) { int u = q.top().second, c = q.top().first; q.pop(); if (c > d[u]) continue; for (auto v : adj[u]) { if (d[v.first] > d[u] + v.second) { d[v.first] = d[u] + v.second; f[v.first] = f[u]; q.push({d[v.first], v.first}); } else if (d[v.first] == d[u] + v.second) { d[v.first] = d[u] + v.second; f[v.first] += f[u]; } } } } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> m; for (int i = 1; i <= m; i++) { int k, u, v, l; cin >> k >> u >> v >> l; if (k == 1) { adj[u].push_back({v, l}); // tồn tại đường đi 1 chiều từ u tới v có độ dài l } else { adj[u].push_back({v, l}); adj[v].push_back({u, l}); // tồn tại đường đi 2 chiều từ u tới v } } dijkstra(); cout << d[n] << " " << f[n]; return 0; }














