Chương 3·Bài 3.5.2

Bellman-Ford and Floyd-Warshall

Cho đồ thị có hướng n≤2500n \le 2500 đỉnh, m≤5000m \le 5000 cạnh, trọng số có thể âm. Tìm đường đi ngắn nhất từ đỉnh 11 tới mọi đỉnh, hoặc báo rằng có một chu trình có tổng âm làm đường đi ngắn đi mãi, trong O(nm)O(nm).

Dijkstra (bài 3.5.1) chỉ đúng khi mọi cạnh không âm. Bellman-Ford chậm hơn nhưng không cần điều kiện đó: nó thử nới mọi cạnh của đồ thị, lặp lại n−1n - 1 lượt; sau chừng ấy lượt, mọi khoảng cách đã đúng, và nếu lượt thứ nn vẫn còn nới được thì đồ thị có chu trình âm. Khi nn chỉ vài trăm và cần khoảng cách giữa mọi cặp đỉnh, Floyd-Warshall làm việc đó bằng ba vòng lặp lồng nhau, O(n3)O(n^3).

IBellman-Ford

Nhắc lại từ bài 3.5.1: gọi dvd_v là độ dài đường tốt nhất đã biết từ nguồn tới vv, ban đầu d1=0d_1 = 0 và các đỉnh khác bằng ∞\infty; nới cạnh u→vu \to v dài ww là gán dv=du+wd_v = d_u + w khi du+w<dvd_u + w < d_v. Dijkstra chốt đỉnh có dd nhỏ nhất với lý do mọi đường vòng tới nó đều dài thêm. Khi có cạnh âm, lý do này không còn đúng. Chẳng hạn với ba cạnh 1→21 \to 2 dài 22, 1→31 \to 3 dài 55, 3→23 \to 2 dài −4-4, Dijkstra lấy đỉnh 22 ra trước với d2=2d_2 = 2, trong khi đường 1→3→21 \to 3 \to 2 chỉ dài 11.

Bellman-Ford không cố chọn thứ tự đúng. Mỗi lượt, nó nới mọi cạnh của đồ thị theo một thứ tự bất kỳ, và lặp lại n−1n - 1 lượt.

Mỗi bước là một lượt nới mọi cạnh; cạnh cam vừa nới được. Đổi một trọng số thành âm để tạo chu trình âm.
C++
for (ll i = 1; i <= n; i++) d[i] = INF;
d[1] = 0;
for (ll it = 1; it < n; it++)
    for (ll i = 1; i <= m; i++)
        if (d[ea[i]] != INF && d[ea[i]] + ew[i] < d[eb[i]]) d[eb[i]] = d[ea[i]] + ew[i];

Độ phức tạp: O(nm)O(nm), với n=2500n = 2500, m=5000m = 5000 cần khoảng 1,25⋅1071{,}25 \cdot 10^7 phép tính.

Ở đây cạnh thứ ii đi từ ea[i] tới eb[i], dài ew[i]. Điều kiện d[ea[i]] != INF bảo đảm chỉ nới từ những đỉnh đã tới được từ nguồn; nếu bỏ đi, một cạnh âm nối hai đỉnh chưa tới được sẽ làm ∞\infty giảm thành một số “gần vô cùng”, vừa sai vừa dễ tràn số.

Chứng minh

Giả sử đồ thị không có chu trình âm tới được từ nguồn. Khi đó với mỗi đỉnh vv tới được, có một đường ngắn nhất từ 11 tới vv không đi qua đỉnh nào hai lần, vì nếu có chu trình thì bỏ nó đi, đường không dài thêm. Đường đó có nhiều nhất n−1n - 1 cạnh.

Ta chứng minh: sau lượt thứ kk, mọi đỉnh vv có một đường ngắn nhất gồm không quá kk cạnh đều đã có dvd_v đúng. Với k=0k = 0 chỉ có nguồn, đúng. Giả sử đúng sau lượt k−1k - 1; xét vv có đường ngắn nhất 1→⋯→u→v1 \to \dots \to u \to v gồm kk cạnh. Đoạn tới uu cũng là đường ngắn nhất tới uu và có k−1k - 1 cạnh, nên dud_u đã đúng; ở lượt kk, việc nới cạnh u→vu \to v làm dvd_v đúng. Vậy sau n−1n - 1 lượt, mọi khoảng cách đều đúng. Mặt khác dvd_v không bao giờ nhỏ hơn độ dài một đường thật, nên nó không thể giảm quá mức.

Nếu một lượt không nới được cạnh nào thì các lượt sau cũng không, nên có thể dừng sớm. Trên nhiều đồ thị, cách này làm Bellman-Ford chạy nhanh hơn hẳn n−1n - 1 lượt.

Một biến thể quen thuộc là SPFA: thay vì nới mọi cạnh mỗi lượt, chỉ nới cạnh đi ra từ những đỉnh vừa được giảm dd, giữ các đỉnh đó trong một hàng đợi. SPFA thường chạy nhanh, nhưng vẫn có những đồ thị làm nó tốn đúng O(nm)O(nm), và người ra đề hay cố ý đưa vào loại test đó. Khi mọi cạnh không âm, hãy dùng Dijkstra.

Ví dụĐiểm Số Trên Hành Trình

Đề. Đồ thị có hướng n≤2500n \le 2500 đỉnh, m≤5000m \le 5000 cạnh, mỗi cạnh mang một số điểm từ −109-10^9 tới 10910^9. Đi từ 11 tới nn, được đi lại các cạnh bao nhiêu lần cũng được; tìm tổng điểm lớn nhất, hoặc in −1-1 nếu tổng điểm có thể lớn tùy ý.

Lời giải. Bài hỏi lớn nhất, nên ta nới theo chiều tăng: gán dv=du+wd_v = d_u + w khi du+w>dvd_u + w > d_v, với dd ban đầu là “âm vô cùng”. Khi đó vai trò của chu trình âm thuộc về chu trình có tổng dương.

Cần cẩn thận ở chỗ: một chu trình dương chỉ làm đáp án vô hạn khi nó vừa tới được từ 11, vừa đi tiếp tới được nn. Sau n−1n - 1 lượt, nếu cạnh a→ba \to b còn làm dbd_b tăng được thì bb đứng sau một chu trình dương tới được từ 11. Ta chỉ in −1-1 khi thêm bb tới được nn; điều này kiểm bằng một lượt BFS (bài 3.1) từ nn trên đồ thị đảo chiều mọi cạnh, đánh dấu các đỉnh tới được nn.

Hằng “âm vô cùng” chọn là −4⋅1018-4 \cdot 10^{18}: các tổng thật nằm trong khoảng ±2,5⋅1012\pm 2{,}5 \cdot 10^{12}, và vì chỉ nới từ đỉnh đã có giá trị, ta không bao giờ cộng thêm vào chính hằng này.

Code đầy đủ
#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], ew[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] >> ew[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]] + ew[i]);
    for (ll i = 1; i <= m; i++)
        if (d[ea[i]] != NEG && d[ea[i]] + ew[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

Độ phức tạp: O(nm)O(nm), với n=2500n = 2500, m=5000m = 5000 cần khoảng 1,25⋅1071{,}25 \cdot 10^7 phép tính.

IIPhát hiện và in chu trình âm

Khi đồ thị có chu trình âm tới được từ nguồn, đi vòng quanh nó mãi thì đường càng ngắn, nên “đường đi ngắn nhất” không tồn tại. Theo ô chứng minh ở mục trên, nếu không có chu trình như vậy thì sau n−1n - 1 lượt không còn cạnh nào nới được. Vì vậy chỉ cần chạy thêm lượt thứ nn: còn nới được cạnh nào thì có chu trình âm tới được từ nguồn.

Muốn biết đồ thị có chu trình âm ở bất kỳ đâu, kể cả nơi không tới được từ đỉnh 11, ta cho mọi dv=0d_v = 0 ngay từ đầu. Việc này giống thêm một đỉnh ảo nối tới mọi đỉnh bằng cạnh dài 00 rồi lấy nó làm nguồn.

Ví dụChu Trình Giảm Thời Gian

Đề. Có T≤20T \le 20 bộ dữ liệu, mỗi bộ là một đồ thị có hướng n≤1000n \le 1000 đỉnh đánh số từ 00, m≤2000m \le 2000 cạnh, trọng số từ −1000-1000 tới 10001000. Cho biết từ đỉnh 00 có đi tới được một chu trình có tổng âm hay không.

Lời giải. Chạy Bellman-Ford từ đỉnh 00, n−1n - 1 lượt, rồi kiểm lượt thứ nn. Chỉ nới từ các đỉnh đã tới được, nên chu trình âm nằm ở phần không tới được từ 00 không làm sai kết quả, đúng như đề yêu cầu.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1005, M = 2005, INF = 1e18;
ll t, n, m, ea[M], eb[M], ew[M], d[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> t;
    while (t--) {
        cin >> n >> m;
        for (ll i = 1; i <= m; i++) cin >> ea[i] >> eb[i] >> ew[i];
        for (ll i = 0; i < n; i++) d[i] = INF;
        d[0] = 0;
        for (ll it = 1; it < n; it++)
            for (ll i = 1; i <= m; i++)
                if (d[ea[i]] != INF && d[ea[i]] + ew[i] < d[eb[i]]) d[eb[i]] = d[ea[i]] + ew[i];
        bool neg = false;
        for (ll i = 1; i <= m; i++)
            if (d[ea[i]] != INF && d[ea[i]] + ew[i] < d[eb[i]]) neg = true;
        cout << (neg ? "possible" : "not possible") << '\n';
    }
}
1
4 4
0 1 5
1 2 2
2 1 -4
2 3 7

Độ phức tạp: O(nm)O(nm) cho mỗi bộ dữ liệu, với n=1000n = 1000, m=2000m = 2000 cần khoảng 2⋅1062 \cdot 10^6 phép tính.

Nhiều bài còn yêu cầu in ra một chu trình âm. Ta lưu thêm mảng cha: mỗi lần nới cạnh u→vu \to v thành công thì ghi parv=upar_v = u. Gọi xx là đỉnh được nới ở lượt thứ nn. Đỉnh xx chưa chắc nằm trên chu trình âm, nhưng nó nằm sau chu trình đó trên đường cha. Đi ngược theo cha đúng nn bước từ xx thì chắc chắn đã đi vào trong chu trình, vì đường cha không thể dài hơn nn đỉnh mà không lặp. Từ đỉnh vừa tới, đi theo cha cho tới khi gặp lại nó, ta được cả chu trình theo chiều ngược.

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2505, M = 5005;
ll n, m, ea[M], eb[M], ew[M], d[N], par[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] >> ew[i];
    ll x = 0;
    for (ll it = 1; it <= n; it++) {
        x = 0;
        for (ll i = 1; i <= m; i++)
            if (d[ea[i]] + ew[i] < d[eb[i]]) {
                d[eb[i]] = d[ea[i]] + ew[i];
                par[eb[i]] = ea[i];
                x = eb[i];
            }
    }
    if (!x) {
        cout << "NO\n";
        return 0;
    }
    for (ll i = 1; i <= n; i++) x = par[x];
    vector<ll> cyc;
    cyc.push_back(x);
    for (ll v = par[x]; v != x; v = par[v]) cyc.push_back(v);
    cyc.push_back(x);
    reverse(cyc.begin(), cyc.end());
    cout << "YES\n";
    for (ll i = 0; i < (ll)cyc.size(); i++) cout << cyc[i] << (i + 1 == (ll)cyc.size() ? '\n' : ' ');
}
4 5
1 2 1
2 3 1
3 4 1
4 2 -3
3 1 2

Độ phức tạp: O(nm)O(nm) cho nn lượt nới, phần lần theo cha chỉ O(n)O(n); với n=2500n = 2500, m=5000m = 5000 cần khoảng 1,25⋅1071{,}25 \cdot 10^7 phép tính.

Ở chương trình này mọi dvd_v bắt đầu bằng 00, như đã nói, nên chu trình âm ở đâu cũng được tìm thấy.

Hỏi nhanh

Ở lượt thứ nn, đỉnh xx vừa được nới. Vì sao phải đi ngược theo cha nn bước trước khi in chu trình, thay vì in luôn chu trình bắt đầu từ xx?

IIIFloyd-Warshall

Khi nn cỡ vài trăm và cần khoảng cách giữa mọi cặp đỉnh, Floyd-Warshall ngắn gọn nhất. Ban đầu dijd_{ij} là độ dài cạnh trực tiếp ngắn nhất từ ii tới jj (không có thì ∞\infty), và dii=0d_{ii} = 0. Lần lượt với mỗi kk từ 11 tới nn, ta thử đi vòng qua kk:

dij=min⁡(dij,dik+dkj).d_{ij} = \min(d_{ij}, d_{ik} + d_{kj}).

Sau khi xét xong kk, dijd_{ij} là độ dài đường ngắn nhất từ ii tới jj mà các đỉnh nằm giữa đường chỉ thuộc {1,2,…,k}\{1, 2, \dots, k\}. Vì vậy vòng lặp theo kk phải nằm ngoài cùng: khi xét kk, các giá trị dikd_{ik} và dkjd_{kj} cần đã tính xong với mọi đỉnh trung gian nhỏ hơn kk. Thuật toán vẫn đúng khi có cạnh âm; nếu có chu trình âm thì sẽ có một diid_{ii} nhỏ hơn 00.

Mỗi bước là một vòng k: ô cam là ô vừa giảm nhờ đi qua đỉnh k, hàng và cột k tô xanh.

Hỏi nhanh

Một bạn viết Floyd-Warshall với vòng kk nằm trong cùng: for i, for j, for k: d[i][j] = min(d[i][j], d[i][k] + d[k][j]). Điều gì có thể xảy ra?

Ví dụBảng Khoảng Cách

Đề. Đồ thị vô hướng n≤500n \le 500 đỉnh, có thể có cạnh lặp và khuyên, cùng q≤105q \le 10^5 câu hỏi khoảng cách giữa hai đỉnh; không có đường thì in −1-1.

Lời giải. Số đỉnh nhỏ còn câu hỏi rất nhiều, nên tính khoảng cách mọi cặp một lần bằng Floyd-Warshall rồi mỗi câu hỏi chỉ tra bảng. 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 ghi cả hai chiều. Trong vòng lặp, khi dik=∞d_{ik} = \infty thì bỏ qua cả hàng ii: vừa tránh phép cộng ∞+∞\infty + \infty làm tràn số, vừa nhanh hơn.

Code đầy đủ
#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

Độ phức tạp: O(n3+q)O(n^3 + q), với n=500n = 500 cần khoảng 1,25⋅1081{,}25 \cdot 10^8 phép tính, mỗi câu hỏi O(1)O(1).

Thứ tự hai vòng trong cũng ảnh hưởng tới tốc độ, vì các ô d[i][j] với cùng ii nằm liền nhau trong bộ nhớ:

Floyd-Warshall, 600 đỉnh.k, i, j (j trong cùng)193 msk, j, i (i trong cùng)322 ms · gấp 1,7 lần
Floyd-Warshall với 600 đỉnh.

Để jj ở vòng trong cùng nhanh hơn khoảng 1,7 lần.

Cùng ba vòng lặp còn trả lời được câu hỏi “từ ii có đi tới được jj không” cho mọi cặp, gọi là bao đóng bắc cầu (transitive closure): thay phép cộng và min⁡\min bằng phép “và”, “hoặc” trên giá trị đúng sai, r[i][j] = r[i][j] || (r[i][k] && r[k][j]). Khi mỗi hàng là một bitset (bài 2.7), cả hàng jj được xử lý cùng lúc và thuật toán nhanh lên khoảng 64 lần.

IVChu trình nhỏ nhất

Bài toán: cho đồ thị vô hướng có trọng số dương, tìm chu trình có tổng trọng số nhỏ nhất (chu trình có ít nhất ba đỉnh, không đi lại cạnh). Ta chỉ cần sửa Floyd-Warshall một chút.

Xét một chu trình nhỏ nhất và gọi kk là đỉnh có số hiệu lớn nhất trên đó; hai đỉnh kề kk trên chu trình là ii và jj. Phần còn lại của chu trình là một đường từ ii tới jj chỉ đi qua các đỉnh nhỏ hơn kk. Đúng lúc chuẩn bị xét kk trong Floyd-Warshall, dijd_{ij} chính là độ dài đường ngắn nhất như vậy. Vì thế, trước khi dùng kk làm đỉnh trung gian, ta thử mọi cặp i<j<ki < j < k có cạnh tới kk:

ans=min⁡(ans,dij+wjk+wki),ans = \min(ans, d_{ij} + w_{jk} + w_{ki}),

với ww là trọng số cạnh trực tiếp (không có cạnh thì ∞\infty).

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 105, INF = 1e18;
ll n, m, w[N][N], d[N][N];

signed main() {
    cin >> n >> m;
    for (ll i = 1; i <= n; i++)
        for (ll j = 1; j <= n; j++) w[i][j] = (i == j ? 0 : INF);
    for (ll i = 1; i <= m; i++) {
        ll a, b, c;
        cin >> a >> b >> c;
        w[a][b] = w[b][a] = min(w[a][b], c);
    }
    for (ll i = 1; i <= n; i++)
        for (ll j = 1; j <= n; j++) d[i][j] = w[i][j];
    ll ans = INF;
    for (ll k = 1; k <= n; k++) {
        for (ll i = 1; i < k; i++)
            for (ll j = i + 1; j < k; j++)
                if (d[i][j] != INF && w[j][k] != INF && w[k][i] != INF) ans = min(ans, d[i][j] + w[j][k] + w[k][i]);
        for (ll i = 1; i <= n; i++)
            for (ll j = 1; j <= n; j++)
                if (d[i][k] != INF && d[k][j] != INF) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
    }
    if (ans == INF) cout << "No solution\n";
    else cout << ans << '\n';
}
4 5
1 2 1
2 3 1
3 1 5
3 4 1
4 1 1

Độ phức tạp: O(n3)O(n^3), với n=100n = 100 cần khoảng 2⋅1062 \cdot 10^6 phép tính.

Với dãy cạnh trên, chu trình 1→2→3→4→11 \to 2 \to 3 \to 4 \to 1 dài 44, ngắn hơn chu trình 1→2→3→11 \to 2 \to 3 \to 1 dài 77. Khi đồ thị không có trọng số, chạy BFS (bài 3.1) từ mỗi đỉnh cũng tìm được chu trình ngắn nhất trong O(nm)O(nm).

VHệ điều kiện hiệu

Một lớp bài nhìn bề ngoài không giống đồ thị: tìm các số nguyên x1,…,xnx_1, \dots, x_n thỏa mm điều kiện, mỗi điều kiện có dạng xv−xu≤wx_v - x_u \le w. Hệ như vậy gọi là hệ ràng buộc hiệu (difference constraints).

Hãy để ý rằng điều kiện xv≤xu+wx_v \le x_u + w giống hệt tính chất của khoảng cách ngắn nhất sau khi không còn nới được: dv≤du+wd_v \le d_u + w với mọi cạnh u→vu \to v dài ww. Vì vậy ta dựng mỗi điều kiện thành một cạnh u→vu \to v dài ww, thêm một đỉnh ảo nối tới mọi đỉnh bằng cạnh dài 00, và chạy Bellman-Ford từ đỉnh ảo. Nếu có chu trình âm thì hệ vô nghiệm: cộng các điều kiện dọc chu trình, vế trái triệt tiêu còn 0≤0 \le một số âm. Nếu không, xv=dvx_v = d_v là một nghiệm.

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1005, M = 5005;
ll n, m, ea[M], eb[M], ew[M], d[N];

signed main() {
    cin >> n >> m;
    for (ll i = 1; i <= m; i++) cin >> ea[i] >> eb[i] >> ew[i];
    for (ll it = 1; it <= n; it++)
        for (ll i = 1; i <= m; i++)
            if (d[ea[i]] + ew[i] < d[eb[i]]) d[eb[i]] = d[ea[i]] + ew[i];
    for (ll i = 1; i <= m; i++)
        if (d[ea[i]] + ew[i] < d[eb[i]]) {
            cout << "NO\n";
            return 0;
        }
    for (ll i = 1; i <= n; i++) cout << d[i] << (i == n ? '\n' : ' ');
}
3 3
1 2 3
2 3 -2
3 1 -1

Độ phức tạp: O(nm)O(nm), với n=1000n = 1000, m=5000m = 5000 cần khoảng 5⋅1065 \cdot 10^6 phép tính.

Mỗi dòng u v w của input là điều kiện xv−xu≤wx_v - x_u \le w. Cho mọi dv=0d_v = 0 từ đầu chính là đi trước một lượt từ đỉnh ảo. Nghiệm in ra thỏa cả ba điều kiện: 0−(−3)≤30 - (-3) \le 3, −2−0≤−2-2 - 0 \le -2, −3−(−2)≤−1-3 - (-2) \le -1. Điều kiện dạng xv−xu≥wx_v - x_u \ge w thì đổi thành xu−xv≤−wx_u - x_v \le -w trước khi dựng cạnh.

Bài nâng cao

Xem 1 bài nâng cao

Ví dụThành Phố Trở Lại

Đề. Có n≤500n \le 500 thành phố, giữa mọi cặp có hướng có một đường trực tiếp với thời gian từ 11 tới 10510^5. Các thành phố lần lượt bị đóng cửa theo thứ tự x1,x2,…,xnx_1, x_2, \dots, x_n. Ngay trước mỗi lần đóng, tính tổng khoảng cách ngắn nhất giữa mọi cặp có thứ tự của các thành phố còn mở, chỉ đi qua các thành phố còn mở.

Lời giải. Đóng dần thì khó, nhưng nhìn ngược thời gian thì các thành phố được mở dần theo thứ tự xn,xn−1,…,x1x_n, x_{n-1}, \dots, x_1. Floyd-Warshall vốn thêm dần từng đỉnh làm đỉnh trung gian, và không cần thứ tự 1,2,…,n1, 2, \dots, n: thứ tự nào cũng được, miễn đỉnh đã thêm thì dùng được. Vậy ta chạy vòng kk theo thứ tự mở xn,xn−1,…x_n, x_{n-1}, \dots; sau khi thêm xkx_k, ma trận dd cho đúng khoảng cách chỉ đi qua các thành phố đã mở, và ta cộng dijd_{ij} trên mọi cặp đỉnh đã mở để được đáp án của bước kk.

Đường trực tiếp i→ji \to j chỉ có hai đầu là ii và jj, nên dùng được ngay khi cả hai đã mở, không phụ thuộc thứ tự. Tổng có thể tới 5002⋅500⋅105500^2 \cdot 500 \cdot 10^5, nên dùng long long.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 505;
ll n, d[N][N], x[N], ans[N], on[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n;
    for (ll i = 1; i <= n; i++)
        for (ll j = 1; j <= n; j++) cin >> d[i][j];
    for (ll i = 1; i <= n; i++) cin >> x[i];
    for (ll t = n; t >= 1; t--) {
        ll k = x[t];
        on[k] = 1;
        for (ll i = 1; i <= n; i++)
            for (ll j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
        ll s = 0;
        for (ll i = 1; i <= n; i++)
            if (on[i])
                for (ll j = 1; j <= n; j++)
                    if (on[j]) s += d[i][j];
        ans[t] = s;
    }
    for (ll t = 1; t <= n; t++) cout << ans[t] << (t == n ? '\n' : ' ');
}
3
0 4 10
3 0 2
7 1 0
2 1 3

Độ phức tạp: O(n3)O(n^3), với n=500n = 500 cần khoảng 2,5⋅1082{,}5 \cdot 10^8 phép tính đơn giản.

Khi đi thi

  • Chọn theo trọng số và kích thước: không âm thì Dijkstra; có cạnh âm thì Bellman-Ford; nn cỡ vài trăm mà cần mọi cặp thì Floyd-Warshall. Đề cố ý có test làm SPFA chậm thì đừng dùng SPFA.
  • Với chu trình âm (hay chu trình dương khi tìm lớn nhất), đọc kỹ đề: chu trình phải tới được từ nguồn, và nhiều đề còn đòi nó đi tiếp tới được đích.
  • Khởi tạo dii=0d_{ii} = 0, vô cùng không quá 101810^{18}, và không bao giờ cộng thêm vào vô cùng: luôn kiểm != INF trước khi cộng.

Tóm tắt

  • Bellman-Ford nới mọi cạnh n−1n - 1 lượt, O(nm)O(nm), đúng cả khi có cạnh âm; lượt thứ nn còn nới được nghĩa là có chu trình âm tới được từ nguồn.
  • Muốn in chu trình âm thì lưu cha, đi ngược nn bước từ đỉnh vừa nới ở lượt nn để vào chu trình rồi đi tiếp cho tới khi gặp lại.
  • Floyd-Warshall O(n3)O(n^3) với vòng kk ở ngoài cùng; thứ tự thêm đỉnh trung gian có thể tùy ý, như khi mở dần các đỉnh.
  • Chu trình nhỏ nhất: trước khi dùng kk làm trung gian, thử dij+wjk+wkid_{ij} + w_{jk} + w_{ki} với i,j<ki, j < k.
  • Hệ xv−xu≤wx_v - x_u \le w là bài đường đi ngắn nhất với cạnh u→vu \to v dài ww từ một đỉnh ảo; có chu trình âm thì vô nghiệm.

Bài tập

Đọc thêm