Đi từ nhà tới trường: tuyến đường ít ngã rẽ nhất chưa chắc là tuyến nhanh nhất. BFS (bài 3.1) chỉ tìm đường ít cạnh nhất, đúng khi mọi cạnh dài như nhau. Cạnh có độ dài khác nhau cần thuật toán khác.
Dijkstra cho độ dài không âm, BFS 0-1 khi độ dài chỉ là 0 và 1, Bellman-Ford khi có cạnh âm, Floyd-Warshall khi cần khoảng cách giữa mọi cặp đỉnh. Phần lớn công sức trong bài thi nằm ở chỗ dựng đồ thị: thêm trạng thái vào đỉnh, thêm nguồn ảo, đổi trọng số, để bài toán thành một lần chạy thuật toán chuẩn.
Dijkstra
Giữ là độ dài đường tốt nhất đã biết từ nguồn tới , ban đầu , còn lại . Nới lỏng cạnh dài : nếu thì gán . Dijkstra lặp: lấy đỉnh chưa chốt có nhỏ nhất, chốt nó, rồi nới mọi cạnh đi ra.
Vì sao đỉnh có nhỏ nhất đã đúng? Mọi đường tới nó phải rời tập đã chốt qua một cạnh nào đó tới một đỉnh chưa chốt có ít nhất bằng nó, và phần đường còn lại có độ dài không âm. Lập luận này hỏng khi có cạnh âm: một đường vòng qua cạnh âm có thể quay lại rẻ hơn.
Cài bằng priority_queue các cặp nhỏ nhất trước. Không cần thao tác “giảm khóa”: mỗi lần nới thành công thì đẩy cặp mới, và khi lấy ra một cặp có lớn hơn hiện tại thì bỏ qua. Mỗi cạnh đẩy tối đa một cặp, nên hàng đợi có phần tử, tổng .
Độ dài tới và đường tới cạnh cho tổng tới : dùng ll và .
priority_queue<pair<ll, ll>, vector<pair<ll, ll>>, greater<pair<ll, ll>>> pq;
pq.push({0, s});
while (!pq.empty()) {
ll du = pq.top().first, u = pq.top().second;
pq.pop();
if (du > d[u]) continue;
for (pair<ll, ll> e : g[u])
if (du + e.second < d[e.first]) {
d[e.first] = du + e.second;
pq.push({d[e.first], e.first});
}
}
Hỏi nhanh
Vì sao Dijkstra có thể bỏ qua một cặp lấy ra khỏi hàng đợi khi ?
chỉ giảm, và mỗi lần giảm lại đẩy một cặp mới. Cặp có giá trị lớn hơn được đẩy từ một lần nới cũ; xử lý nó không sai nhưng tốn thời gian, có thể biến thành trên đồ thị xấu.
Ví dụKhoảng Cách Từ Đỉnh Đầu
Đề. Đồ thị có hướng đỉnh, cạnh dài tới (có khuyên và cạnh lặp). In khoảng cách ngắn nhất từ 1 tới mọi đỉnh.
Lời giải. Dijkstra đúng như khung trên. Khuyên và cạnh lặp không cần xử lý riêng: khuyên không bao giờ nới thành công, cạnh lặp chỉ là thêm một lựa chọn.
Bước 1: hàng đợi ưu tiên nhỏ nhất. pq chứa cặp (khoảng cách, đỉnh), greater làm phần tử nhỏ nhất nằm trên đỉnh. INF = 1e18 lớn hơn mọi khoảng cách thật ().
for (ll i = 1; i <= n; i++) d[i] = INF;
priority_queue<pair<ll, ll>, vector<pair<ll, ll>>, greater<pair<ll, ll>>> pq;
d[1] = 0;
pq.push({0, 1});
Bước 2: lấy ra và nới. C++ không có thao tác “giảm khóa”, nên nới được thì đẩy thêm một cặp mới; cặp cũ vẫn nằm trong hàng, và khi lấy ra mà du > d[u] thì nó đã lỗi thời, bỏ qua. Lần đầu một đỉnh được lấy ra với du == d[u], khoảng cách đó đã cuối cùng: mọi đường khác phải đi qua một đỉnh chưa lấy ra, có nhãn không nhỏ hơn, và cạnh không âm chỉ làm dài thêm.
while (!pq.empty()) {
ll du = pq.top().first, u = pq.top().second;
pq.pop();
if (du > d[u]) continue;
for (pair<ll, ll> e : g[u])
if (du + e.second < d[e.first]) {
d[e.first] = du + e.second;
pq.push({d[e.first], e.first});
}
}
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e5 + 5, INF = 1e18;
ll n, m, d[N];
vector<pair<ll, ll>> g[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 1; i <= m; i++) {
ll a, b, c;
cin >> a >> b >> c;
g[a].push_back({b, c});
}
for (ll i = 1; i <= n; i++) d[i] = INF;
priority_queue<pair<ll, ll>, vector<pair<ll, ll>>, greater<pair<ll, ll>>> pq;
d[1] = 0;
pq.push({0, 1});
while (!pq.empty()) {
ll du = pq.top().first, u = pq.top().second;
pq.pop();
if (du > d[u]) continue;
for (pair<ll, ll> e : g[u])
if (du + e.second < d[e.first]) {
d[e.first] = du + e.second;
pq.push({d[e.first], e.first});
}
}
for (ll i = 1; i <= n; i++) cout << d[i] << ' ';
cout << '\n';
}
5 8
1 2 7
1 3 2
3 2 1
2 4 4
3 4 8
4 5 3
3 5 20
2 5 10
0 3 2 7 10
Độ phức tạp: mỗi cạnh đẩy tối đa một cặp: .
Nhiều người quen xóa cặp cũ khỏi set thay vì bỏ qua. Cách đó đúng nhưng chậm hơn:
set chậm hơn khoảng 2,3 lần. Cứ dùng priority_queue và bỏ qua cặp cũ.
Thông tin phụ
Các cạnh thỏa tạo thành đồ thị đường ngắn nhất: một DAG, vì tăng ngặt dọc mỗi cạnh khi . Mọi đường ngắn nhất đều đi trên DAG đó, nên đếm số đường, số cạnh ít nhất hay nhiều nhất trên các đường ngắn nhất là quy hoạch động trên DAG (bài 3.3). Dijkstra lấy đỉnh theo tăng dần, chính là một thứ tự tô pô của DAG này, nên làm luôn trong lúc nới: nới tốt hơn thì đặt lại thông tin phụ của theo ; nới bằng thì gộp thêm. Chẳng hạn với số đường ngắn nhất cnt và số cạnh ít nhất mn:
ll v = e.first, nd = du + e.second;
if (nd < d[v]) {
d[v] = nd;
cnt[v] = cnt[u];
mn[v] = mn[u] + 1;
pq.push({nd, v});
} else if (nd == d[v]) {
cnt[v] = (cnt[v] + cnt[u]) % MOD;
mn[v] = min(mn[v], mn[u] + 1);
}
Cạnh phải dương để mọi có đều được lấy ra trước ; khi được lấy ra thì mọi đóng góp đã gộp xong.
Trạng thái, nguồn ảo
Mở rộng đỉnh. Khi đường đi có thêm một điều kiện hữu hạn (đã dùng phiếu chưa, đang cầm xe nào, đã qua bao nhiêu cạnh đặc biệt), đỉnh mới là cặp (đỉnh cũ, trạng thái). Một phiếu giảm giá dùng đúng một lần cho hai bản sao của đồ thị: tầng 0 chưa dùng, tầng 1 đã dùng, mỗi cạnh có thêm một bản đi từ tầng 0 lên tầng 1 với giá giảm. Số đỉnh nhân với số trạng thái, thuật toán không đổi.
Nguồn ảo. Câu hỏi “với mọi , giá nhỏ nhất của ” là Dijkstra từ một đỉnh ảo nối tới mọi bằng cạnh dài , trên đồ thị đảo chiều nếu cạnh có hướng. Không cần thêm đỉnh thật: khởi tạo và đẩy mọi đỉnh vào hàng đợi.
Ví dụPhiếu Giảm Giá Một Chặng
Đề. Đồ thị có hướng đỉnh, chuyến bay. Phải dùng một phiếu cho đúng một chuyến, chuyến đó chỉ trả . Tìm chi phí nhỏ nhất từ 1 tới .
Lời giải. Trạng thái với là đã dùng phiếu chưa. Cạnh giá cho ba cạnh: giá , giá , giá . Dijkstra trên trạng thái; đáp án là , vì đề bắt buộc dùng phiếu.
Bước 1: đánh số trạng thái. là ô 2 * v + t của một mảng d; đỉnh 1 chưa dùng phiếu là ô 2.
for (ll i = 0; i < 2 * N; i++) d[i] = INF;
d[2] = 0;
pq.push({0, 2});
Bước 2: cạnh sinh tại chỗ. Không cần dựng đồ thị đỉnh thật: lấy trạng thái ra thì sinh cạnh ngay từ danh sách kề gốc của . Luôn đi được cạnh giá sang ; nếu chưa dùng phiếu thì thêm lựa chọn giá sang .
ll u = s / 2, t = s % 2;
for (pair<ll, ll> e : g[u]) {
ll v = e.first, c = e.second;
if (du + c < d[2 * v + t]) {
d[2 * v + t] = du + c;
pq.push({d[2 * v + t], 2 * v + t});
}
if (!t && du + c / 2 < d[2 * v + 1]) {
d[2 * v + 1] = du + c / 2;
pq.push({d[2 * v + 1], 2 * v + 1});
}
}
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e5 + 5, INF = 1e18;
ll n, m, d[2 * N];
vector<pair<ll, ll>> g[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 1; i <= m; i++) {
ll a, b, c;
cin >> a >> b >> c;
g[a].push_back({b, c});
}
for (ll i = 0; i < 2 * N; i++) d[i] = INF;
priority_queue<pair<ll, ll>, vector<pair<ll, ll>>, greater<pair<ll, ll>>> pq;
d[2] = 0;
pq.push({0, 2});
while (!pq.empty()) {
ll du = pq.top().first, s = pq.top().second;
pq.pop();
if (du > d[s]) continue;
ll u = s / 2, t = s % 2;
for (pair<ll, ll> e : g[u]) {
ll v = e.first, c = e.second;
if (du + c < d[2 * v + t]) {
d[2 * v + t] = du + c;
pq.push({d[2 * v + t], 2 * v + t});
}
if (!t && du + c / 2 < d[2 * v + 1]) {
d[2 * v + 1] = du + c / 2;
pq.push({d[2 * v + 1], 2 * v + 1});
}
}
}
cout << d[2 * n + 1] << '\n';
}
4 5
1 2 8
2 4 7
1 3 5
3 4 10
2 3 1
10
Độ phức tạp: đồ thị trạng thái có đỉnh và cạnh: .
BFS 0-1
Khi mọi cạnh dài 0 hoặc 1, không cần hàng đợi ưu tiên. Dùng một hàng đợi hai đầu: nới thành công qua cạnh 0 thì đẩy vào đầu, qua cạnh 1 thì đẩy vào cuối. Hàng đợi luôn chứa các đỉnh có bằng ở phía trước và ở phía sau, giống BFS theo lớp, nên lấy ra từ đầu luôn là đỉnh nhỏ nhất. Một đỉnh có thể vào hàng nhiều lần, nhưng như Dijkstra, bỏ qua lần lấy ra cũ. Tổng .
Ví dụMê Cung Hai Kiểu Di Chuyển
Đề. Lưới có tường. Đi bộ sang ô kề cạnh miễn phí; dịch chuyển tới một ô trống trong hình vuông quanh mình tốn 1 phép. Tìm số phép ít nhất từ ô đầu tới ô đích, hoặc .
Lời giải. Mỗi ô là một đỉnh; bốn ô kề cạnh nối bằng cạnh 0 (đi bộ), các ô trong hình vuông nối bằng cạnh 1 (dịch chuyển). Trọng số chỉ là 0 hoặc 1 nên dùng BFS 0-1: hàng đợi hai đầu, cạnh 0 đẩy vào đầu, cạnh 1 đẩy vào cuối. Hàng đợi luôn có dạng “các ô khoảng cách , rồi các ô ”, nên lấy ra từ đầu luôn là ô nhỏ nhất, như Dijkstra mà không cần heap.
Bước 1: khởi tạo. Lưới đánh số từ 1 nhờ thêm một ký tự đệm; d khởi tạo INF, ô xuất phát 0.
for (ll i = 1; i <= h; i++) {
cin >> s[i];
s[i] = " " + s[i];
for (ll j = 1; j <= w; j++) d[i][j] = INF;
}
Bước 2: đi bộ, cạnh 0. Nới được thì đẩy vào đầu, vì ô đó cùng khoảng cách với ô hiện tại.
for (ll k = 0; k < 4; k++) {
ll nx = x + dx[k], ny = y + dy[k];
if (nx < 1 || ny < 1 || nx > h || ny > w || s[nx][ny] == '#') continue;
if (d[x][y] < d[nx][ny]) {
d[nx][ny] = d[x][y];
dq.push_front({nx, ny});
}
}
Bước 3: dịch chuyển, cạnh 1. Duyệt hình vuông quanh ô, nới được thì đẩy vào cuối.
for (ll nx = x - 2; nx <= x + 2; nx++)
for (ll ny = y - 2; ny <= y + 2; ny++) {
if (nx < 1 || ny < 1 || nx > h || ny > w || s[nx][ny] == '#') continue;
if (d[x][y] + 1 < d[nx][ny]) {
d[nx][ny] = d[x][y] + 1;
dq.push_back({nx, ny});
}
}
Một ô có thể vào hàng nhiều lần; lần lấy ra thứ hai chỉ nới lại với nhãn đã đúng, không làm sai.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1005, INF = 1e18;
ll h, w, d[N][N];
ll dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
string s[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll sr, sc, tr, tc;
cin >> h >> w >> sr >> sc >> tr >> tc;
for (ll i = 1; i <= h; i++) {
cin >> s[i];
s[i] = " " + s[i];
for (ll j = 1; j <= w; j++) d[i][j] = INF;
}
deque<pair<ll, ll>> dq;
d[sr][sc] = 0;
dq.push_back({sr, sc});
while (!dq.empty()) {
ll x = dq.front().first, y = dq.front().second;
dq.pop_front();
for (ll k = 0; k < 4; k++) {
ll nx = x + dx[k], ny = y + dy[k];
if (nx < 1 || ny < 1 || nx > h || ny > w || s[nx][ny] == '#') continue;
if (d[x][y] < d[nx][ny]) {
d[nx][ny] = d[x][y];
dq.push_front({nx, ny});
}
}
for (ll nx = x - 2; nx <= x + 2; nx++)
for (ll ny = y - 2; ny <= y + 2; ny++) {
if (nx < 1 || ny < 1 || nx > h || ny > w || s[nx][ny] == '#') continue;
if (d[x][y] + 1 < d[nx][ny]) {
d[nx][ny] = d[x][y] + 1;
dq.push_back({nx, ny});
}
}
}
cout << (d[tr][tc] == INF ? -1 : d[tr][tc]) << '\n';
}
5 5
1 1
5 5
..#..
..#..
#####
..#..
..#..
1
Mỗi ô xét hướng: khoảng bước cho ô.
Floyd-Warshall
Khi cỡ vài trăm và cần khoảng cách giữa mọi cặp, Floyd-Warshall gọn nhất: ban đầu là cạnh trực tiếp ngắn nhất, . Với mỗi từ 1 tới , thử đi qua : . Sau vòng , là đường ngắn nhất chỉ dùng các đỉnh trung gian trong . Vòng phải ở ngoài cùng.
Thời gian , với là phép đơn giản. Thuật toán chạy được với cạnh âm; có chu trình âm thì một nào đó thành âm.
Hỏi nhanh
Trong Floyd-Warshall, vì sao vòng lặp theo đỉnh trung gian phải nằm ngoài cùng?
Bất biến của thuật toán là: sau khi xử lý xong , mọi cặp đã tính đường tốt nhất qua các đỉnh . Đặt ở trong thì có lúc dùng chưa được cập nhật đủ, và kết quả có thể sai.
Ví dụBảng Khoảng Cách
Đề. Đồ thị vô hướng đỉnh, có cạnh lặp và khuyên, câu hỏi khoảng cách giữa hai đỉnh; không có đường thì in .
Lời giải. còn câu hỏi tới : tính khoảng cách mọi cặp một lần bằng Floyd-Warshall, mỗi câu hỏi tra bảng.
Bước 1: khởi tạo bảng. , còn lại INF. Có cạnh lặp nên chỉ giữ cạnh ngắn nhất giữa mỗi cặp; đồ thị vô hướng nên cập nhật cả hai chiều.
for (ll i = 1; i <= n; i++)
for (ll j = 1; j <= n; j++) d[i][j] = (i == j ? 0 : INF);
for (ll i = 1; i <= m; i++) {
ll a, b, c;
cin >> a >> b >> c;
d[a][b] = min(d[a][b], c);
d[b][a] = min(d[b][a], c);
}
Bước 2: ba vòng lặp, ở ngoài cùng. Sau vòng , là đường ngắn nhất chỉ đi qua các đỉnh trung gian trong . Dòng continue bỏ cả hàng khi chưa tới được : vừa tránh cộng INF + INF tràn số, vừa nhanh hơn.
for (ll k = 1; k <= n; k++)
for (ll i = 1; i <= n; i++) {
if (d[i][k] == INF) continue;
for (ll j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 505, INF = 1e18;
ll n, m, q, d[N][N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> q;
for (ll i = 1; i <= n; i++)
for (ll j = 1; j <= n; j++) d[i][j] = (i == j ? 0 : INF);
for (ll i = 1; i <= m; i++) {
ll a, b, c;
cin >> a >> b >> c;
d[a][b] = min(d[a][b], c);
d[b][a] = min(d[b][a], c);
}
for (ll k = 1; k <= n; k++)
for (ll i = 1; i <= n; i++) {
if (d[i][k] == INF) continue;
for (ll j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
while (q--) {
ll a, b;
cin >> a >> b;
cout << (d[a][b] == INF ? -1 : d[a][b]) << '\n';
}
}
5 5 6
1 2 8
1 2 3
2 3 4
3 4 2
1 4 20
1 4
4 1
1 5
5 5
2 4
1 2
9
9
-1
0
6
3
Độ phức tạp: phép, mỗi câu hỏi .
Thứ tự hai vòng trong cũng có giá, vì d[i][j] nằm liên tiếp theo :
Để ở vòng trong cùng nhanh hơn khoảng 1,7 lần.
Bellman-Ford
Bellman-Ford nới mọi cạnh, lặp vòng. Một đường ngắn nhất không lặp đỉnh có tối đa cạnh, và sau vòng thứ mọi đường ngắn nhất dùng tối đa cạnh đã đúng, nên vòng là đủ khi không có chu trình âm. Tổng .
Nếu vòng thứ vẫn nới được cạnh thì tới được từ một chu trình âm. Đáp án chỉ không xác định khi chu trình đó vừa tới được từ nguồn vừa tới được đích. Kiểm điều đó bằng hai lần BFS: từ nguồn trên đồ thị gốc và từ đích trên đồ thị ngược.
Bài lớn nhất đổi dấu mọi trọng số, hoặc giữ nguyên và nới theo chiều tăng: chu trình dương làm điểm tăng vô hạn.
Ví dụĐiểm Số Trên Hành Trình
Đề. Đồ thị có hướng đỉnh, cạnh có điểm . Tìm tổng điểm lớn nhất từ 1 tới , đi lại cạnh tùy ý; điểm không bị chặn thì in .
Lời giải. Bellman-Ford theo chiều lớn nhất. Điểm không bị chặn khi có một chu trình dương vừa tới được từ 1, vừa đi tiếp tới được . Sau vòng nới, một cạnh còn tăng được nghĩa là nằm sau một chu trình dương tới được từ 1; nếu thêm tới được thì đi vòng chu trình bao nhiêu lần cũng được.
Bước 1: đỉnh tới được . BFS trên đồ thị ngược từ đánh dấu ok[v].
queue<ll> q;
q.push(n);
ok[n] = 1;
while (!q.empty()) {
ll u = q.front();
q.pop();
for (ll v : rg[u])
if (!ok[v]) {
ok[v] = 1;
q.push(v);
}
}
Bước 2: vòng nới. d khởi tạo NEG, chỉ nới từ đỉnh đã có giá trị hữu hạn, tức đã tới được từ 1.
for (ll i = 1; i <= n; i++) d[i] = NEG;
d[1] = 0;
for (ll it = 1; it < n; it++)
for (ll i = 1; i <= m; i++)
if (d[ea[i]] != NEG) d[eb[i]] = max(d[eb[i]], d[ea[i]] + ex[i]);
Bước 3: vòng kiểm thứ .
for (ll i = 1; i <= m; i++)
if (d[ea[i]] != NEG && d[ea[i]] + ex[i] > d[eb[i]] && ok[eb[i]]) {
cout << -1 << '\n';
return 0;
}
NEG = -4e18 để cộng một trọng số âm vào thật (tới khoảng ) vẫn xa giới hạn; không bao giờ cộng vào chính NEG.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2505, M = 5005, NEG = -4e18;
ll n, m, ea[M], eb[M], ex[M], d[N], ok[N];
vector<ll> rg[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 1; i <= m; i++) {
cin >> ea[i] >> eb[i] >> ex[i];
rg[eb[i]].push_back(ea[i]);
}
queue<ll> q;
q.push(n);
ok[n] = 1;
while (!q.empty()) {
ll u = q.front();
q.pop();
for (ll v : rg[u])
if (!ok[v]) {
ok[v] = 1;
q.push(v);
}
}
for (ll i = 1; i <= n; i++) d[i] = NEG;
d[1] = 0;
for (ll it = 1; it < n; it++)
for (ll i = 1; i <= m; i++)
if (d[ea[i]] != NEG) d[eb[i]] = max(d[eb[i]], d[ea[i]] + ex[i]);
for (ll i = 1; i <= m; i++)
if (d[ea[i]] != NEG && d[ea[i]] + ex[i] > d[eb[i]] && ok[eb[i]]) {
cout << -1 << '\n';
return 0;
}
cout << d[n] << '\n';
}
4 5
1 2 4
2 4 -1
1 3 -2
3 4 8
2 3 1
13
Độ phức tạp: .
Khi đi thi
- Chọn theo trọng số và kích thước: bằng nhau thì BFS; 0 và 1 thì BFS 0-1; không âm thì Dijkstra; có âm thì Bellman-Ford; và cần mọi cặp thì Floyd-Warshall.
- Đường dài tới trở lên: dùng
ll, , và không bao giờ cộng thêm vào . - Điều kiện lạ trên đường đi (phiếu, xe, số lần, thời điểm chờ) thường là thêm trạng thái vào đỉnh; “với mọi đỉnh, gần nhất tới một tập” là nguồn ảo.
Tóm tắt
- Dijkstra: lấy đỉnh có nhãn nhỏ nhất, nới cạnh đi ra, bỏ qua cặp cũ; đúng khi trọng số không âm, .
- Đồ thị đường ngắn nhất là một DAG; đếm đường và thông tin phụ cập nhật lúc nới.
- Trạng thái mở rộng đỉnh; nguồn ảo là khởi tạo nhiều đỉnh trong hàng đợi.
- BFS 0-1 dùng hàng đợi hai đầu; Floyd-Warshall với ngoài cùng.
- Bellman-Ford vòng, vòng thứ còn nới được là có chu trình âm; chỉ quan tâm chu trình nằm giữa nguồn và đích.
Bài tập
Đọc thêm
- Các thuật toán tìm đường đi ngắn nhấtVNOI Wiki
- BFS 0-1VNOI Wiki
- Dijkstra on sparse graphscp-algorithms
- Shortest Paths with Non-Negative Edge WeightsUSACO Guide