Chương 3·Bài 32

Topological Sort

Đồ thị có hướng không có chu trình (DAG) mô tả các ràng buộc “việc aa phải làm trước việc bb”: môn học tiên quyết, tệp phụ thuộc, công thức lắp ráp. Thứ tự tô pô là một cách xếp mọi đỉnh thành hàng sao cho mọi cung (cạnh có hướng) a→ba \to b đều đi từ trái sang phải. Thứ tự này tồn tại khi và chỉ khi đồ thị không có chu trình, và nó là xương sống của quy hoạch động trên DAG: xử lý đỉnh theo thứ tự tô pô thì khi tới vv, mọi đỉnh đi tới vv đã có đáp án.

Thuật toán Kahn

Bậc vào của một đỉnh là số cung đi vào nó. Đỉnh có bậc vào bằng 0 không phải chờ ai, có thể đứng đầu hàng. Thuật toán Kahn lặp: lấy một đỉnh bậc vào 0, đưa vào thứ tự, gỡ mọi cung đi ra từ nó (giảm bậc vào của các đỉnh đích); đỉnh nào về bậc 0 thì đẩy vào hàng đợi. Mỗi đỉnh và mỗi cung được xử lý một lần, O(n+m)O(n + m).

Gõ các cung u v; nhãn trên đỉnh là bậc vào hiện tại hoặc thứ tự đã xếp. Thêm cung 6 2 để tạo chu trình.

Nếu kết thúc mà thứ tự có ít hơn nn đỉnh, các đỉnh còn lại đều có bậc vào dương trong phần đồ thị còn lại: mỗi đỉnh có một đỉnh đi tới nó cũng còn lại, lần ngược mãi phải lặp, nên đồ thị có chu trình. Ngược lại, DAG luôn có ít nhất một đỉnh bậc vào 0 (đỉnh đầu của một đường dài nhất), nên Kahn không bao giờ kẹt khi không có chu trình.

Cách thứ hai dùng DFS: một đỉnh xong (đã duyệt hết các đỉnh đi ra) sau mọi đỉnh nó đi tới, nên đảo ngược thứ tự xong là một thứ tự tô pô. Tìm chu trình bằng DFS cần ba trạng thái: chưa thăm, đang trên ngăn xếp đệ quy, đã xong; gặp cung tới đỉnh đang trên ngăn xếp là có chu trình.

Hỏi nhanh

Kahn kết thúc khi hàng đợi rỗng và thứ tự mới có n−3n - 3 đỉnh. Kết luận nào đúng?

Ví dụLịch Trình Tiên Quyết

Đề. n≤105n \le 10^5 chuyên đề, m≤2⋅105m \le 2 \cdot 10^5 ràng buộc aa trước bb. In một thứ tự hợp lệ, hoặc IMPOSSIBLE.

Lời giải. Thuật toán Kahn: deg[v] là số cung đi vào vv chưa bị gỡ; đỉnh có deg = 0 không còn điều kiện nào phải chờ, xếp tiếp được. Đỉnh nằm trên một chu trình (hay phía sau chu trình) không bao giờ về 0, nên thứ tự thu được thiếu đỉnh nghĩa là có chu trình.

Bước 1: đếm bậc vào.

C++
for (ll i = 1; i <= m; i++) {
    ll a, b;
    cin >> a >> b;
    g[a].push_back(b);
    deg[b]++;
}

Bước 2: lấy dần các đỉnh sẵn sàng. Mọi đỉnh bậc vào 0 vào hàng đợi trước. Lấy uu ra thì gỡ mọi cung u→vu \to v; đỉnh nào về 0 thì vào hàng đợi.

C++
queue<ll> q;
for (ll u = 1; u <= n; u++)
    if (!deg[u]) q.push(u);
vector<ll> ord;
while (!q.empty()) {
    ll u = q.front();
    q.pop();
    ord.push_back(u);
    for (ll v : g[u])
        if (--deg[v] == 0) q.push(v);
}

Bước 3: kiểm chu trình. ord có đủ nn đỉnh thì in, không thì in IMPOSSIBLE.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5;
ll n, m, deg[N];
vector<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;
        cin >> a >> b;
        g[a].push_back(b);
        deg[b]++;
    }
    queue<ll> q;
    for (ll u = 1; u <= n; u++)
        if (!deg[u]) q.push(u);
    vector<ll> ord;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        ord.push_back(u);
        for (ll v : g[u])
            if (--deg[v] == 0) q.push(v);
    }
    if ((ll)ord.size() < n) {
        cout << "IMPOSSIBLE\n";
        return 0;
    }
    for (ll u : ord) cout << u << ' ';
    cout << '\n';
}
6 5
3 1
5 2
1 4
2 4
4 6

Mỗi đỉnh và mỗi cung được xử lý một lần: O(n+m)O(n + m).

Thứ tự nhỏ nhất và thứ tự duy nhất

Kahn được tự do chọn đỉnh nào trong số các đỉnh bậc vào 0. Thay hàng đợi bằng hàng đợi ưu tiên lấy đỉnh nhỏ nhất thì được thứ tự tô pô nhỏ nhất theo từ điển: ở mỗi vị trí, đỉnh được chọn là đỉnh nhỏ nhất có thể đứng ở đó, và chọn nó không làm mất lựa chọn nào về sau. Chi phí thành O((n+m)log⁡n)O((n + m) \log n).

Cùng lý lẽ đó, thứ tự tô pô là duy nhất khi và chỉ khi mỗi bước chỉ có đúng một đỉnh bậc vào 0. Có lúc hai đỉnh cùng sẵn sàng thì đổi chỗ chúng vẫn hợp lệ, vì không cung nào nối hai đỉnh đó (một cung sẽ làm một trong hai còn bậc vào dương). Thứ tự duy nhất cũng là thứ tự mà mọi cặp đỉnh liên tiếp có cung nối nhau.

Ví dụDãy Lệnh Ưu Tiên

Đề. n≤2⋅105n \le 2 \cdot 10^5 lệnh, m≤2⋅105m \le 2 \cdot 10^5 ràng buộc (có thể trùng). In thứ tự thực hiện nhỏ nhất theo từ điển, hoặc −1-1 nếu không có.

Lời giải. Kahn như ví dụ trước, khác đúng một chỗ: trong các đỉnh đang sẵn sàng, luôn lấy đỉnh có số hiệu nhỏ nhất. Vì sao ra thứ tự nhỏ nhất theo từ điển: ở mỗi vị trí, đỉnh đặt được phải là đỉnh đang có bậc vào 0, và chọn đỉnh nhỏ nhất không làm đỉnh nào khác mất khả năng được đặt sau (nó chỉ gỡ thêm cung). Ràng buộc trùng làm deg[v] tăng hai lần và cũng bị gỡ hai lần, nên không cần lọc.

Bước 1: hàng đợi ưu tiên lấy số nhỏ nhất. priority_queue với greater thay cho queue.

C++
priority_queue<ll, vector<ll>, greater<ll>> pq;
for (ll u = 1; u <= n; u++)
    if (!deg[u]) pq.push(u);
vector<ll> ord;
while (!pq.empty()) {
    ll u = pq.top();
    pq.pop();
    ord.push_back(u);
    for (ll v : g[u])
        if (--deg[v] == 0) pq.push(v);
}

Bước 2: kiểm chu trình như ví dụ trước, không đủ nn đỉnh thì in −1-1.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, m, deg[N];
vector<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;
        cin >> a >> b;
        g[a].push_back(b);
        deg[b]++;
    }
    priority_queue<ll, vector<ll>, greater<ll>> pq;
    for (ll u = 1; u <= n; u++)
        if (!deg[u]) pq.push(u);
    vector<ll> ord;
    while (!pq.empty()) {
        ll u = pq.top();
        pq.pop();
        ord.push_back(u);
        for (ll v : g[u])
            if (--deg[v] == 0) pq.push(v);
    }
    if ((ll)ord.size() < n) {
        cout << -1 << '\n';
        return 0;
    }
    for (ll u : ord) cout << u << ' ';
    cout << '\n';
}
6 6
3 1
3 5
2 5
1 4
5 4
4 6

O((n+m)log⁡n)O((n + m) \log n) vì hàng đợi ưu tiên.

Ví dụThứ Hạng Có Duy Nhất

Đề. n≤2⋅105n \le 2 \cdot 10^5 đối tượng, mm quan hệ Ax<AyA_x < A_y, luôn có ít nhất một cách xếp. Nếu hoán vị thứ hạng AA là duy nhất thì in Yes và AA, không thì No.

Lời giải. Kahn; nếu có lúc hàng đợi chứa từ hai đỉnh trở lên thì đổi thứ tự hai đỉnh đó vẫn hợp lệ, nên thứ hạng không duy nhất. Ngược lại mọi lúc chỉ có đúng một lựa chọn, và đỉnh được lấy ra thứ kk có hạng kk. Đề bảo đảm có ít nhất một cách xếp nên không cần kiểm chu trình.

Bước 1: Kahn có kiểm duy nhất. Kiểm q.size() > 1 mỗi vòng, trước khi lấy.

C++
while (!q.empty()) {
    if (q.size() > 1) {
        cout << "No\n";
        return 0;
    }
    ll u = q.front();
    q.pop();
    rk[u] = ++k;
    for (ll v : g[u])
        if (--deg[v] == 0) q.push(v);
}

Bước 2: in theo số hiệu. Đề đòi hạng của từng đối tượng, nên in rk[1..n], không in thứ tự các đối tượng.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, m, deg[N], rk[N];
vector<ll> g[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    for (ll i = 1; i <= m; i++) {
        ll x, y;
        cin >> x >> y;
        g[x].push_back(y);
        deg[y]++;
    }
    queue<ll> q;
    for (ll u = 1; u <= n; u++)
        if (!deg[u]) q.push(u);
    ll k = 0;
    while (!q.empty()) {
        if (q.size() > 1) {
            cout << "No\n";
            return 0;
        }
        ll u = q.front();
        q.pop();
        rk[u] = ++k;
        for (ll v : g[u])
            if (--deg[v] == 0) q.push(v);
    }
    cout << "Yes\n";
    for (ll u = 1; u <= n; u++) cout << rk[u] << ' ';
    cout << '\n';
}
5 4
2 3
3 1
1 5
5 4

O(n+m)O(n + m).

Quy hoạch động theo thứ tự tô pô

Trên DAG, dp[v]\mathrm{dp}[v] phụ thuộc vào các đỉnh đi tới vv (hay các đỉnh vv đi tới) không bao giờ tạo vòng phụ thuộc. Xử lý đỉnh theo thứ tự tô pô, mỗi khi lấy uu ra thì đẩy giá trị sang mọi vv mà u→vu \to v: tới lượt vv, mọi đóng góp đã đủ. Đường dài nhất là dp[v]=max⁡(dp[u]+1)\mathrm{dp}[v] = \max(\mathrm{dp}[u] + 1), số đường từ nguồn là dp[v]=∑dp[u]\mathrm{dp}[v] = \sum \mathrm{dp}[u]. Trên đồ thị tổng quát, đường dài nhất là bài khó; DAG làm nó thành O(n+m)O(n + m).

Có thể lồng thẳng việc đẩy giá trị vào vòng lặp Kahn, không cần lưu thứ tự riêng.

Hỏi nhanh

Đếm số đường từ đỉnh 1 tới đỉnh nn trên DAG, vì sao phải chạy Kahn trên cả đồ thị chứ không chỉ từ đỉnh 1?

Ví dụHành Trình Qua Các Mốc

Đề. DAG n≤105n \le 10^5 đỉnh, m≤105m \le 10^5 cung. Tìm số cung lớn nhất của một đường đi có hướng.

Lời giải. dp[v]\mathrm{dp}[v] là số cung của đường dài nhất kết thúc ở vv, ban đầu 0 vì đường bắt đầu ở đâu cũng được. Đi theo thứ tự tô pô: lúc vv vào hàng đợi, mọi cung vào vv đã bị gỡ, tức mọi đỉnh đi trước vv đã xong, nên dp[v]\mathrm{dp}[v] đã là giá trị cuối cùng.

Bước 1: cập nhật khi gỡ cung. Mỗi khi gỡ cung u→vu \to v, thử kéo dài đường tốt nhất tới uu thêm một cung.

C++
while (!q.empty()) {
    ll u = q.front();
    q.pop();
    ans = max(ans, dp[u]);
    for (ll v : g[u]) {
        dp[v] = max(dp[v], dp[u] + 1);
        if (--deg[v] == 0) q.push(v);
    }
}

ans lấy max lúc lấy đỉnh ra, khi dp[u] chắc chắn đã đủ.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5;
ll n, m, deg[N], dp[N];
vector<ll> g[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    for (ll i = 1; i <= m; i++) {
        ll u, v;
        cin >> u >> v;
        g[u].push_back(v);
        deg[v]++;
    }
    queue<ll> q;
    for (ll u = 1; u <= n; u++)
        if (!deg[u]) q.push(u);
    ll ans = 0;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        ans = max(ans, dp[u]);
        for (ll v : g[u]) {
            dp[v] = max(dp[v], dp[u] + 1);
            if (--deg[v] == 0) q.push(v);
        }
    }
    cout << ans << '\n';
}
6 5
1 3
2 3
3 4
3 5
5 6

O(n+m)O(n + m).

Ví dụNhững Tuyến Đường Qua Cổng Dịch Chuyển

Đề. DAG n≤105n \le 10^5 đỉnh, m≤2⋅105m \le 2 \cdot 10^5 cung (có thể trùng, mỗi cung tính riêng). Đếm số đường từ 1 tới nn theo modulo 109+710^9 + 7.

Lời giải. dp[v]\mathrm{dp}[v] là số đường từ 1 tới vv: dp[1]=1\mathrm{dp}[1] = 1, còn lại 0, và mỗi cung u→vu \to v cộng dp[u]\mathrm{dp}[u] vào dp[v]\mathrm{dp}[v] theo thứ tự tô pô. Cung trùng tự được cộng hai lần, đúng ý đề.

Bước 1: Kahn trên cả đồ thị. Hàng đợi bắt đầu từ mọi đỉnh bậc vào 0, không chỉ đỉnh 1. Một đỉnh không tới được từ 1 vẫn có cung đi vào các đỉnh khác và phải được xử lý để gỡ cung đó; bắt đầu chỉ từ 1 thì những đỉnh phía sau nó giữ deg > 0 mãi. Các đỉnh không tới được từ 1 có dp = 0, đẩy đi chỉ cộng 0.

C++
queue<ll> q;
for (ll u = 1; u <= n; u++)
    if (!deg[u]) q.push(u);
dp[1] = 1;

Bước 2: cộng theo cung.

C++
while (!q.empty()) {
    ll u = q.front();
    q.pop();
    for (ll v : g[u]) {
        dp[v] = (dp[v] + dp[u]) % MOD;
        if (--deg[v] == 0) q.push(v);
    }
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5, MOD = 1e9 + 7;
ll n, m, deg[N], dp[N];
vector<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;
        cin >> a >> b;
        g[a].push_back(b);
        deg[b]++;
    }
    queue<ll> q;
    for (ll u = 1; u <= n; u++)
        if (!deg[u]) q.push(u);
    dp[1] = 1;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        for (ll v : g[u]) {
            dp[v] = (dp[v] + dp[u]) % MOD;
            if (--deg[v] == 0) q.push(v);
        }
    }
    cout << dp[n] << '\n';
}
5 6
1 2
1 3
2 4
3 4
4 5
2 5

O(n+m)O(n + m).

Đường dài nhất từ một nguồn và truy vết

Khi đường phải bắt đầu ở một nguồn cố định, các đỉnh không tới được từ nguồn phải bị loại: đặt dp\mathrm{dp} của chúng là −∞-\infty và chỉ cập nhật từ những đỉnh có giá trị hợp lệ. Lưu par[v] là đỉnh uu cho giá trị tốt nhất của vv để lần ngược đường đi, như với BFS (bài 30).

Mỗi cung u v w có độ dài w; nhãn trên đỉnh là dp. Bước cuối tô đường dài nhất tìm được.

Ví dụTuyến Bay Qua Nhiều Thành Phố

Đề. DAG n≤105n \le 10^5 đỉnh, m≤2⋅105m \le 2 \cdot 10^5 cung. In một đường từ 1 tới nn đi qua nhiều đỉnh nhất, hoặc IMPOSSIBLE.

Lời giải. Gộp hai ví dụ trước: dp[v]\mathrm{dp}[v] là số đỉnh nhiều nhất trên một đường từ 1 tới vv, và par[v] để truy vết như BFS ở bài 30. Đỉnh không tới được từ 1 phải mang giá trị “không hợp lệ” để không kéo theo đỉnh khác.

Bước 1: khởi tạo. dp khởi đầu NEG (không tới được), chỉ dp[1] = 1.

C++
for (ll u = 1; u <= n; u++) dp[u] = NEG;
dp[1] = 1;

Bước 2: Kahn trên cả đồ thị. Như ví dụ đếm đường, hàng đợi bắt đầu từ mọi đỉnh bậc vào 0. Chỉ cập nhật từ đỉnh có dp hợp lệ; mỗi lần dp[v] tăng thì ghi lại par[v].

C++
for (ll v : g[u]) {
    if (dp[u] != NEG && dp[u] + 1 > dp[v]) {
        dp[v] = dp[u] + 1;
        par[v] = u;
    }
    if (--deg[v] == 0) q.push(v);
}

Bước 3: truy vết. dp[n]\mathrm{dp}[n] vẫn NEG thì in IMPOSSIBLE; không thì lần par từ nn về 1 rồi đảo.

C++
vector<ll> path;
for (ll v = n; v != 1; v = par[v]) path.push_back(v);
path.push_back(1);
reverse(path.begin(), path.end());
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5, NEG = -1e18;
ll n, m, deg[N], dp[N], par[N];
vector<ll> g[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    for (ll i = 1; i <= m; i++) {
        ll u, v;
        cin >> u >> v;
        g[u].push_back(v);
        deg[v]++;
    }
    for (ll u = 1; u <= n; u++) dp[u] = NEG;
    dp[1] = 1;
    queue<ll> q;
    for (ll u = 1; u <= n; u++)
        if (!deg[u]) q.push(u);
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        for (ll v : g[u]) {
            if (dp[u] != NEG && dp[u] + 1 > dp[v]) {
                dp[v] = dp[u] + 1;
                par[v] = u;
            }
            if (--deg[v] == 0) q.push(v);
        }
    }
    if (dp[n] == NEG) {
        cout << "IMPOSSIBLE\n";
        return 0;
    }
    vector<ll> path;
    for (ll v = n; v != 1; v = par[v]) path.push_back(v);
    path.push_back(1);
    reverse(path.begin(), path.end());
    cout << path.size() << '\n';
    for (ll v : path) cout << v << ' ';
    cout << '\n';
}
6 5
1 2
2 6
1 3
3 4
4 6

O(n+m)O(n + m).

Định hướng cạnh theo thứ tự tô pô

Một thứ tự tô pô cho mọi đỉnh một vị trí pospos. Định hướng mọi cạnh từ đỉnh có vị trí nhỏ sang đỉnh có vị trí lớn thì không thể có chu trình, vì đi theo bất kỳ cung nào vị trí cũng tăng. Vậy muốn thêm chiều cho các cạnh vô hướng mà không tạo chu trình, chỉ cần các cung có sẵn đã không có chu trình: lấy một thứ tự tô pô của chúng rồi định hướng phần còn lại theo pospos.

Ví dụĐịnh Hướng Các Liên Kết

Đề. Nhiều test, tổng N,M≤2⋅105N, M \le 2 \cdot 10^5. Một số cạnh đã có hướng, số còn lại vô hướng. Định hướng các cạnh vô hướng để cả đồ thị không có chu trình, in YES và mọi cạnh; không được thì in NO.

Lời giải. Nếu các cung đã có hướng tự tạo chu trình thì vô vọng. Ngược lại lấy một thứ tự tô pô của riêng các cung đó, rồi định hướng mọi cạnh vô hướng theo đúng thứ tự ấy, từ vị trí nhỏ sang vị trí lớn. Khi đó mọi cung của kết quả, có sẵn lẫn mới, đều đi từ vị trí nhỏ sang vị trí lớn, nên không thể có chu trình.

Bước 1: chỉ cung có sẵn vào đồ thị. Cạnh vô hướng (et = 0) chờ tới cuối. Nhiều test nên mỗi test xóa g và deg của các đỉnh 1..n1..n trước.

C++
for (ll i = 1; i <= m; i++) {
    cin >> et[i] >> ex[i] >> ey[i];
    if (et[i]) {
        g[ex[i]].push_back(ey[i]);
        deg[ey[i]]++;
    }
}

Bước 2: Kahn ghi vị trí. pos[u] là vị trí của uu trong thứ tự tô pô. Thiếu đỉnh (k < n) là các cung có sẵn đã có chu trình, in NO.

C++
while (!q.empty()) {
    ll u = q.front();
    q.pop();
    pos[u] = ++k;
    for (ll v : g[u])
        if (--deg[v] == 0) q.push(v);
}

Bước 3: định hướng cạnh vô hướng. In theo chiều pos tăng; cung có sẵn in nguyên.

C++
for (ll i = 1; i <= m; i++) {
    ll x = ex[i], y = ey[i];
    if (!et[i] && pos[x] > pos[y]) swap(x, y);
    cout << x << ' ' << y << '\n';
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll deg[N], pos[N], et[N], ex[N], ey[N];
vector<ll> g[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll T;
    cin >> T;
    while (T--) {
        ll n, m;
        cin >> n >> m;
        for (ll u = 1; u <= n; u++) {
            g[u].clear();
            deg[u] = 0;
        }
        for (ll i = 1; i <= m; i++) {
            cin >> et[i] >> ex[i] >> ey[i];
            if (et[i]) {
                g[ex[i]].push_back(ey[i]);
                deg[ey[i]]++;
            }
        }
        queue<ll> q;
        for (ll u = 1; u <= n; u++)
            if (!deg[u]) q.push(u);
        ll k = 0;
        while (!q.empty()) {
            ll u = q.front();
            q.pop();
            pos[u] = ++k;
            for (ll v : g[u])
                if (--deg[v] == 0) q.push(v);
        }
        if (k < n) {
            cout << "NO\n";
            continue;
        }
        cout << "YES\n";
        for (ll i = 1; i <= m; i++) {
            ll x = ex[i], y = ey[i];
            if (!et[i] && pos[x] > pos[y]) swap(x, y);
            cout << x << ' ' << y << '\n';
        }
    }
}
1
5 6
1 4 2
0 1 2
1 3 4
0 5 3
0 1 5
1 4 5

Mỗi test O(N+M)O(N + M).

Khi đi thi

  • Ràng buộc “trước sau”, phụ thuộc, công thức không vòng: thứ tự tô pô. Đề không bảo đảm không có chu trình thì luôn kiểm số đỉnh lấy ra.
  • Quy hoạch động trên DAG đẩy giá trị khi lấy đỉnh ra khỏi hàng đợi; đường phải bắt đầu ở một nguồn thì khởi tạo các đỉnh khác bằng −∞-\infty hoặc 0 tùy bài, đừng để đỉnh không tới được làm sai.
  • Muốn thứ tự nhỏ nhất theo từ điển thì hàng đợi ưu tiên; muốn ưu tiên “đỉnh 1 càng sớm càng tốt, rồi đỉnh 2…” thì làm trên đồ thị ngược với hàng đợi ưu tiên lấy lớn nhất rồi đảo thứ tự.

Tóm tắt

  • Thứ tự tô pô tồn tại khi và chỉ khi đồ thị không có chu trình; Kahn lấy dần các đỉnh bậc vào 0, O(n+m)O(n + m).
  • Lấy ra ít hơn nn đỉnh nghĩa là có chu trình; DFS ba trạng thái cũng tìm được chu trình.
  • Hàng đợi ưu tiên cho thứ tự nhỏ nhất theo từ điển; mỗi bước đúng một đỉnh sẵn sàng thì thứ tự duy nhất.
  • Quy hoạch động theo thứ tự tô pô: đường dài nhất, đếm đường, truy vết bằng par.
  • Định hướng mọi cạnh theo vị trí trong một thứ tự tô pô thì không thể có chu trình.

Bài tập

1
Tháo Các Khối Chặncanuc80k3202Que aa nằm trên que bb là cung a→ba \to b; Kahn in thứ tự nhặt, không lấy hết thì in IMPOSSIBLE.
LQDOJ
2
Lịch Học Theo Từng Ưu Tiêncanuc80k3204Đảo mọi cung, chạy Kahn với hàng đợi ưu tiên lấy số lớn nhất, rồi đảo ngược thứ tự thu được.
LQDOJ
3
Bảng Chữ Cái Bí Mậtcanuc80k3207So từng cặp tên liền nhau: chữ khác nhau đầu tiên cho một cung; tên sau là tiền tố thật của tên trước thì vô nghiệm; Kahn trên 26 chữ.
LQDOJ
4
Biên Dịch Sau Thay Đổicanuc80k3208DFS trên đồ thị ngược từ tệp bị đổi để biết các tệp bị ảnh hưởng, rồi in chúng theo thứ tự tô pô của phần đồ thị đó.
LQDOJ
5
Thay Nhãn Không Trùngcanuc80k3214Mỗi yêu cầu là cung Si→TiS_i \to T_i trên các nhãn; làm được khi và chỉ khi đồ thị này không có chu trình.
LQDOJ
6
Các Chuyên Đề Cần Học Trướccanuc80k3215DFS từ cuốn 1 qua các cuốn tiên quyết, in theo thứ tự xong (cuốn tiên quyết xong trước), bỏ cuốn 1.
LQDOJ
7
Mua Trước, Bán Saucanuc80k3217Cung luôn từ chỉ số nhỏ sang lớn nên thứ tự 1..n1..n là thứ tự tô pô; giữ giá mua nhỏ nhất trên các đỉnh đi tới được vv, lấy max của giá bán trừ giá đó.
LQDOJ
8
Điểm Đích Luôn Có Cách Tớicanuc80k3218Đồ thị mỗi đỉnh một cung ra; đỉnh thỏa là đỉnh nằm trên chu trình, tìm bằng cách gỡ dần các đỉnh bậc vào 0 như Kahn.
LQDOJ
9
Đọc Sách Theo Thứ Tự Chươngcanuc80k3220dp[i]\mathrm{dp}[i] là lượt đọc hiểu chương ii: lấy max theo tiên quyết jj của dp[j]\mathrm{dp}[j], cộng 1 nếu j>ij > i; có chu trình thì in −1-1.
LQDOJ
10
Các Phiên Gọi Bộ Gia Tốccanuc80k3221Hai hàng đợi theo loại; lần lượt xử lý hết mô-đun loại 0 sẵn sàng, rồi gom hết mô-đun loại 1 sẵn sàng vào một phiên, đếm số phiên.
LQDOJ
11
Ký Hiệu Trên Hành Trìnhcanuc80k3224Có chu trình thì in −1-1; ngược lại với mỗi chữ cái cc, dp[v]\mathrm{dp}[v] là số chữ cc nhiều nhất trên đường kết thúc ở vv, theo thứ tự tô pô.
LQDOJ
12
Chi Phí Tạo Từng Linh Kiệncanuc80k3225Theo thứ tự tô pô, chi phí của ii là 0 nếu có sẵn, ngược lại min⁡(ci,tổng chi phıˊ nguyeˆn liệu)\min(c_i, \text{tổng chi phí nguyên liệu}) nếu có công thức.
LQDOJ
13
Thời Điểm Xếp Hạng Được Xác Địnhcanuc80k3236Tìm kiếm nhị phân số quan hệ đầu tiên; với một tiền tố quan hệ, thứ tự duy nhất khi mỗi bước Kahn chỉ có một đỉnh bậc vào 0.
LQDOJ
14
Đảo Chiều Các Tuyến Để Phá Chu Trìnhcanuc80k3238Tìm kiếm nhị phân ngưỡng: các cung nặng hơn ngưỡng phải không có chu trình; rồi đảo các cung nhẹ đang đi ngược thứ tự tô pô.
LQDOJ
15
Nén Hạng Trong Bảng Dữ Liệucanuc80k3245Gộp các ô bằng nhau trên cùng hàng hay cột bằng DSU, nối cung từ ô nhỏ sang ô lớn kề trong hàng và cột đã sắp, rồi hạng là đường dài nhất trên DAG.
LQDOJ

Đọc thêm

  • Sắp xếp tô pôVNOI WikiThuật toán Kahn, DFS và các ứng dụng, viết bằng tiếng Việt.
  • Topological Sortingcp-algorithmsChứng minh bằng thời điểm xong của DFS.
  • Topological SortUSACO GuideKahn, quy hoạch động trên DAG và bài luyện.
  • 拓扑排序OI WikiKahn, DFS và thứ tự nhỏ nhất theo từ điển.