Chương 4·Bài 4.10

Network Flow

Cho một đồ thị có hướng, mỗi cung có một sức chứa, cùng hai đỉnh ss và tt. Tìm lượng hàng lớn nhất có thể chuyển từ ss tới tt, sao cho hàng đi trên mỗi cung không vượt quá sức chứa của cung đó; với đồ thị V≤500V \le 500 đỉnh và E≤1000E \le 1000 cung, thuật toán Dinic giải bài này trong O(V2E)O(V^2 E), và trong thực tế còn nhanh hơn nhiều.

Đây là bài toán luồng cực đại (maximum flow). Ý tưởng chung là lặp lại hai việc: tìm một đường đi từ ss tới tt còn chở thêm được hàng, rồi đẩy thêm hàng theo đường đó. Điểm mấu chốt là cho phép rút lại phần hàng đã đẩy theo hướng không tốt. Bài học đi từ cách cơ bản nhất (Ford-Fulkerson), qua cách chọn đường thông minh hơn (Edmonds-Karp), tới Dinic, cách thường dùng khi đi thi. Rất nhiều bài trông chẳng liên quan cũng đưa được về luồng cực đại: tách hai đỉnh bằng ít cạnh nhất, ghép cặp, chọn tập có lợi nhất với các ràng buộc kéo theo.

IMạng, luồng và cách tham lam

Một mạng (network) là một đồ thị có hướng, trong đó mỗi cung u→vu \to v có một sức chứa c(u,v)≥0c(u, v) \ge 0, và có hai đỉnh đặc biệt: nguồn ss và đích tt. Ta gọi số đỉnh là VV và số cung là EE.

Một luồng (flow) gán cho mỗi cung u→vu \to v một lượng f(u,v)f(u, v), gọi là luồng trên cung đó, thỏa hai điều kiện:

  • luồng trên mỗi cung không âm và không vượt quá sức chứa: 0≤f(u,v)≤c(u,v)0 \le f(u, v) \le c(u, v);
  • ở mỗi đỉnh khác ss và tt, tổng luồng đi vào bằng tổng luồng đi ra: hàng không tự sinh ra, cũng không mất đi ở giữa đường.

Giá trị của luồng là tổng luồng đi ra khỏi ss trừ tổng luồng đi vào ss, cũng bằng lượng hàng tới được tt. Bài toán luồng cực đại là tìm một luồng có giá trị lớn nhất.

Hình dưới là một mạng bốn đỉnh, mọi cung có sức chứa 11, với nguồn là đỉnh 11 và đích là đỉnh 44. Luồng trong hình chở 11 đơn vị theo đường 1→2→41 \to 2 \to 4 và 11 đơn vị theo đường 1→3→41 \to 3 \to 4, nên có giá trị 22. Đây cũng là giá trị lớn nhất, vì hai cung đi ra khỏi đỉnh 11 chỉ chở được tổng cộng 22 đơn vị.

Một luồng giá trị 2 trên mạng bốn đỉnh; nhãn mỗi cung là luồng trên sức chứa.1/11/10/11/11/11234nguồn sđích t
Một luồng giá trị 2 trên mạng bốn đỉnh; nhãn mỗi cung là luồng trên sức chứa.

Cách dễ nghĩ ra nhất là tham lam: tìm một đường đi từ ss tới tt mà mọi cung trên đường còn chở thêm được, đẩy thêm theo đường đó một lượng bằng sức chứa còn lại nhỏ nhất trên đường, rồi lặp lại cho tới khi không còn đường nào. Câu hỏi dưới đây thử cách này trên chính mạng ở hình trên.

Hỏi nhanh

Chương trình dưới đây làm đúng cách tham lam trên: mỗi lần tìm một đường bằng DFS, ưu tiên đỉnh có số nhỏ trước, đẩy thêm luồng theo đường đó, và lặp lại cho tới khi không còn đường. Bạn có thể bấm Chạy với mạng bốn đỉnh ở hình trên:

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 105;
ll n, m, s, t, cap[N][N], vis[N];

ll dfs(ll u, ll f) {
    if (u == t) return f;
    vis[u] = 1;
    for (ll v = 1; v <= n; v++)
        if (cap[u][v] > 0 && !vis[v]) {
            ll d = dfs(v, min(f, cap[u][v]));
            if (d > 0) {
                cap[u][v] -= d;
                return d;
            }
        }
    return 0;
}

signed main() {
    cin >> n >> m >> s >> t;
    for (ll i = 1; i <= m; i++) {
        ll u, v, c;
        cin >> u >> v >> c;
        cap[u][v] += c;
    }
    ll flow = 0, f;
    do {
        for (ll i = 1; i <= n; i++) vis[i] = 0;
        f = dfs(s, 1e18);
        flow += f;
    } while (f > 0);
    cout << flow << '\n';
}
4 5 1 4
1 2 1
1 3 1
2 3 1
2 4 1
3 4 1

Luồng cực đại là 22, nhưng chương trình in ra 11. Vì sao?

IICung ngược và Ford-Fulkerson

Ví dụ trên cho thấy cách tham lam có thể đẩy luồng theo hướng không tốt rồi bị kẹt. Cách sửa là cho phép rút lại luồng đã đẩy: nếu cung u→vu \to v đang chở ff đơn vị, ta coi như có thêm một cung v→uv \to u chở được ff đơn vị. Đẩy hàng theo cung v→uv \to u này nghĩa là giảm bớt luồng trên cung u→vu \to v.

Với một luồng đang có, đồ thị dư (residual graph) gồm các cung sau:

  • với mỗi cung u→vu \to v của mạng, một cung u→vu \to v có sức chứa c(u,v)−f(u,v)c(u, v) - f(u, v), là lượng còn đẩy thêm được;
  • và một cung ngược v→uv \to u có sức chứa f(u,v)f(u, v), là lượng có thể rút lại.

Cung nào có sức chứa 00 thì coi như không có. Một đường tăng luồng (augmenting path) là một đường đi từ ss tới tt trong đồ thị dư. Đẩy thêm theo đường tăng luồng một lượng bằng sức chứa nhỏ nhất trên đường, giá trị luồng tăng đúng chừng ấy.

Quay lại mạng bốn đỉnh. Sau khi đẩy 11 đơn vị theo đường 1→2→3→41 \to 2 \to 3 \to 4, đồ thị dư có cung ngược 3→23 \to 2. Nhờ cung này, ta tìm được đường tăng luồng 1→3→2→41 \to 3 \to 2 \to 4: đẩy theo nó thì luồng trên cung 2→32 \to 3 được rút về 00, phần hàng đi tới đỉnh 22 chuyển sang cung 2→42 \to 4, và luồng tăng lên 22.

Đồ thị dư sau khi đẩy 1 theo đường 1 → 2 → 3 → 4; nét đứt là cung ngược, đường cam 1 → 3 → 2 → 4 đi ngược cung 2 → 3.111111234nguồn sđích t
Đồ thị dư sau khi đẩy 1 theo đường 1 → 2 → 3 → 4; nét đứt là cung ngược, đường cam 1 → 3 → 2 → 4 đi ngược cung 2 → 3.

Ford-Fulkerson chính là cách tham lam ở trên, nhưng tìm đường trong đồ thị dư: lặp lại việc tìm một đường tăng luồng bất kỳ và đẩy theo nó, cho tới khi không còn đường tăng luồng. Khi lưu sức chứa bằng ma trận, cung ngược v→uv \to u chính là ô cap[v][u], nên chỉ cần thêm đúng một dòng vào hàm dfs của chương trình trong câu hỏi trên; phần còn lại giữ nguyên:

C++
ll dfs(ll u, ll f) {
    if (u == t) return f;
    vis[u] = 1;
    for (ll v = 1; v <= n; v++)
        if (cap[u][v] > 0 && !vis[v]) {
            ll d = dfs(v, min(f, cap[u][v]));
            if (d > 0) {
                cap[u][v] -= d;
                cap[v][u] += d;
                return d;
            }
        }
    return 0;
}

Độ phức tạp: mỗi lần tìm đường tốn O(V2)O(V^2) với ma trận, và mỗi lần đẩy thêm ít nhất 11 đơn vị khi các sức chứa là số nguyên, nên cả thuật toán tốn O(F⋅V2)O(F \cdot V^2), với FF là giá trị luồng cực đại.

Khi không còn đường tăng luồng, luồng đang có là luồng cực đại. Điều này không hiển nhiên: có thể nào luồng chưa cực đại mà đã hết đường tăng? Câu trả lời là không (xem ô Chứng minh). Chứng minh dùng một khái niệm quan trọng: một lát cắt (cut) là một cách chia các đỉnh thành hai phần, phần SS chứa ss và phần TT chứa tt; sức chứa của lát cắt là tổng sức chứa các cung đi từ SS sang TT.

Chứng minh

Luồng không vượt quá lát cắt nào. Cộng (luồng ra trừ luồng vào) trên mọi đỉnh của SS: đỉnh ss góp đúng giá trị luồng, các đỉnh khác góp 00. Luồng trên các cung nằm trọn trong SS được cộng một lần và trừ một lần nên triệt tiêu. Vậy giá trị luồng bằng tổng luồng trên các cung từ SS sang TT, trừ tổng luồng trên các cung từ TT về SS. Số này không vượt quá tổng sức chứa các cung từ SS sang TT, tức sức chứa của lát cắt.

Hết đường tăng thì có một lát cắt bằng luồng. Giả sử không còn đường tăng luồng. Gọi SS là tập các đỉnh tới được từ ss trong đồ thị dư, TT là các đỉnh còn lại; vì không còn đường tăng luồng nên tt thuộc TT. Với cung u→vu \to v từ SS sang TT: nếu nó chưa đầy thì đồ thị dư còn cung u→vu \to v, và vv tới được, trái với vv thuộc TT; vậy cung này đầy. Với cung v→uv \to u từ TT về SS: nếu nó có luồng thì đồ thị dư có cung ngược u→vu \to v, và vv lại tới được; vậy cung này không có luồng. Theo ý trên, giá trị luồng bằng đúng sức chứa của lát cắt (S,T)(S, T).

Hai ý cộng lại: không luồng nào vượt quá sức chứa của lát cắt (S,T)(S, T), mà luồng đang có đã bằng nó, nên luồng đang có là cực đại.

Ford-Fulkerson có thể chạy rất chậm khi giá trị luồng lớn. Trên mạng ở hình dưới, luồng cực đại là 2⋅1092 \cdot 10^9. Nếu không may, các đường tăng luồng cứ lần lượt đi qua cung giữa theo hai chiều, s→a→b→ts \to a \to b \to t rồi s→b→a→ts \to b \to a \to t, mỗi lần chỉ tăng được 11, và thuật toán cần tới 2⋅1092 \cdot 10^9 lần tìm đường.

Nếu đường tăng luồng cứ đi qua cung a, b lần lượt theo hai chiều, mỗi lần chỉ tăng được 1, cần tới 2 · 10⁹ lần.10⁹10⁹10⁹10⁹1sabt
Nếu đường tăng luồng cứ đi qua cung a, b lần lượt theo hai chiều, mỗi lần chỉ tăng được 1, cần tới 2 · 10⁹ lần.

IIIĐường tăng ngắn nhất: Edmonds-Karp

Edmonds-Karp sửa điểm yếu trên bằng một thay đổi nhỏ: luôn chọn đường tăng luồng ngắn nhất, tức là đi qua ít cung nhất, tìm bằng BFS thay vì DFS. Khi đó số lần tăng luồng không quá O(VE)O(VE), không còn phụ thuộc vào giá trị luồng (xem ô Chứng minh). Trên mạng ở hình trên, hai đường ngắn nhất s→a→ts \to a \to t và s→b→ts \to b \to t được chọn trước, và thuật toán xong sau đúng hai lần tăng.

Trong code, BFS ghi lại đỉnh đứng trước của mỗi đỉnh trên đường đi; tới được tt thì đi ngược từ tt về ss hai lượt: lượt đầu tìm sức chứa nhỏ nhất trên đường, lượt sau đẩy luồng. Hàm dưới đây trả về lượng vừa đẩy, hoặc 00 khi không còn đường tăng luồng; vòng lặp chính gọi nó cho tới khi nhận về 00.

C++
ll bfs() {
    for (ll i = 1; i <= n; i++) par[i] = 0;
    queue<ll> q;
    q.push(s);
    par[s] = s;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        for (ll v = 1; v <= n; v++)
            if (cap[u][v] > 0 && !par[v]) {
                par[v] = u;
                q.push(v);
            }
    }
    if (!par[t]) return 0;
    ll d = 1e18;
    for (ll v = t; v != s; v = par[v]) d = min(d, cap[par[v]][v]);
    for (ll v = t; v != s; v = par[v]) {
        cap[par[v]][v] -= d;
        cap[v][par[v]] += d;
    }
    return d;
}

Độ phức tạp: mỗi lần BFS trên ma trận tốn O(V2)O(V^2) và có không quá O(VE)O(VE) lần tăng luồng, nên cả thuật toán tốn O(V3E)O(V^3 E); nếu lưu đồ thị bằng danh sách cạnh như ở mục sau, mỗi lần BFS chỉ tốn O(E)O(E) và cả thuật toán tốn O(VE2)O(VE^2). Trong thực tế, số lần tăng luồng thường ít hơn cận này rất nhiều.

Chứng minh

Gọi d(v)d(v) là khoảng cách, tính bằng số cung, từ ss tới vv trong đồ thị dư.

Khoảng cách không bao giờ giảm. Giả sử sau một lần tăng luồng, có đỉnh vv mà khoảng cách mới d′(v)d'(v) nhỏ hơn d(v)d(v); chọn vv có d′(v)d'(v) nhỏ nhất. Gọi uu là đỉnh đứng ngay trước vv trên một đường ngắn nhất mới, thì d′(v)=d′(u)+1d'(v) = d'(u) + 1 và d′(u)≥d(u)d'(u) \ge d(u). Nếu cung u→vu \to v đã có trong đồ thị dư cũ thì d(v)≤d(u)+1≤d′(v)d(v) \le d(u) + 1 \le d'(v), mâu thuẫn. Vậy cung u→vu \to v mới xuất hiện, tức là nó là cung ngược của cung v→uv \to u vừa được đẩy luồng. Đường tăng luồng là đường ngắn nhất nên d(u)=d(v)+1d(u) = d(v) + 1, suy ra d′(v)=d′(u)+1≥d(v)+2d'(v) = d'(u) + 1 \ge d(v) + 2, cũng mâu thuẫn.

Mỗi cung chỉ làm nghẽn không quá V/2V / 2 lần. Gọi cung có sức chứa nhỏ nhất trên đường tăng luồng là cung nghẽn; sau khi đẩy, nó hết sức chứa và biến mất khỏi đồ thị dư. Lúc cung u→vu \to v làm nghẽn, d(v)=d(u)+1d(v) = d(u) + 1. Muốn nó xuất hiện lại, phải có một lần tăng luồng sau đó đi qua cung v→uv \to u; lúc đó d′′(u)=d′′(v)+1≥d(v)+1=d(u)+2d''(u) = d''(v) + 1 \ge d(v) + 1 = d(u) + 2. Vậy giữa hai lần cung u→vu \to v làm nghẽn, khoảng cách tới uu tăng ít nhất 22. Khoảng cách không quá VV, nên mỗi cung làm nghẽn không quá V/2V / 2 lần.

Mỗi lần tăng luồng có ít nhất một cung nghẽn, và đồ thị dư có không quá 2E2E cung, nên có không quá 2E⋅V/2=VE2E \cdot V / 2 = VE lần tăng luồng.

IVTăng luồng theo từng pha: Dinic

Edmonds-Karp mỗi lần BFS chỉ dùng được một đường. Ta có thể thấy được điểm lãng phí: thường có nhiều đường tăng luồng cùng ngắn nhất, nhưng mỗi đường lại cần một lần BFS riêng. Dinic dùng một lần BFS cho tất cả các đường ngắn nhất cùng độ dài.

Dinic chạy theo từng pha. Đầu mỗi pha, BFS từ ss trong đồ thị dư để tính mức của mỗi đỉnh, là khoảng cách từ ss tới đỉnh đó. Trong pha này, ta chỉ dùng những cung đi từ một đỉnh ở mức dd tới một đỉnh ở mức d+1d + 1; mọi đường từ ss tới tt chỉ đi theo các cung này đều là đường ngắn nhất. Sau đó DFS đẩy luồng theo các đường như vậy, hết đường này tới đường khác, cho tới khi không còn đường nào. Hết pha, ta BFS lại để bắt đầu pha mới; khi BFS không tới được tt nữa thì luồng đã cực đại.

Để DFS không thử đi thử lại những cung vô ích, mỗi đỉnh uu giữ một con trỏ cung: chỉ số của cung đầu tiên đáng thử. Một cung đã hết sức chứa, hay dẫn tới một đỉnh không còn đường tới tt, thì con trỏ bỏ qua nó luôn cho tới hết pha.

Với đồ thị lớn, ma trận kề tốn V2V^2 ô nhớ và mỗi lần duyệt các đỉnh kề tốn O(V)O(V). Vì vậy từ đây ta lưu đồ thị bằng danh sách cạnh: các cung được đánh số 0,1,2,…0, 1, 2, \dots, mỗi cung lưu đỉnh cuối và sức chứa dư, và mỗi đỉnh giữ danh sách số hiệu các cung đi ra. Khi thêm cung u→vu \to v, ta thêm luôn cung ngược v→uv \to u với sức chứa 00 ngay sau nó. Hai cung một cặp mang số hiệu 2k2k và 2k+12k + 1, nên cung ngược của cung ee là cung e⊕1e \oplus 1, viết trong C++ là e ^ 1.

Trong mô phỏng dưới đây, mỗi pha bắt đầu bằng BFS ghi mức lên các đỉnh, rồi đẩy luồng theo các đường cam; nhãn mỗi cung là luồng trên sức chứa. Bước cuối tô lát cắt nhỏ nhất, sẽ nói ở mục sau.

Nhãn cạnh là luồng/sức chứa. Mỗi pha BFS cho mức, rồi đẩy theo các đường tăng (cam). Bước cuối tô lát cắt nhỏ nhất.

Hỏi nhanh

Ở một pha của Dinic, BFS vừa tính xong mức của các đỉnh, và đỉnh uu có mức 22. Cung u→vu \to v trong đồ thị dư còn sức chứa.

Trong pha này, DFS đi theo cung u→vu \to v khi nào?

Chứng minh

Không quá VV pha. Gọi LL là mức của tt ở đầu pha. Cuối pha, mọi đường từ ss tới tt chỉ đi theo cung từ mức dd sang mức d+1d + 1 đều có ít nhất một cung đã hết sức chứa. Các cung mới xuất hiện trong pha đều là cung ngược, đi từ mức d+1d + 1 về mức dd. Trong đồ thị dư mới, mỗi cung tăng mức thêm nhiều nhất 11, nên một đường từ ss tới tt dài đúng LL phải chỉ gồm các cung tăng đúng 11 mức, tức là cung cũ của pha, mà không còn đường nào như vậy. Vì vậy khoảng cách từ ss tới tt tăng ít nhất 11 sau mỗi pha. Khoảng cách không quá V−1V - 1, nên có không quá VV pha.

Mỗi pha tốn O(VE)O(VE). Mỗi lần DFS tới được tt, có ít nhất một cung trên đường hết sức chứa, và con trỏ cung sẽ bỏ qua nó; mỗi cung bị bỏ qua vì dẫn vào ngõ cụt cũng không bị thử lại trong pha. Mỗi đường dài không quá VV, có không quá EE đường, và tổng số lần bỏ qua cung là O(E)O(E), nên cả pha tốn O(VE)O(VE). Cộng lại, Dinic tốn O(V2E)O(V^2 E).

Ví dụBăng Thông Cực Đại

Đề. Có n≤500n \le 500 máy tính và m≤1000m \le 1000 kết nối một chiều, mỗi kết nối có băng thông không quá 10910^9. Tìm tốc độ truyền dữ liệu lớn nhất từ máy 11 tới máy nn.

Lời giải. Đây đúng là bài toán luồng cực đại từ đỉnh 11 tới đỉnh nn. Trong code, to[e] và cap[e] là đỉnh cuối và sức chứa dư của cung ee, g[u] là danh sách cung đi ra từ uu, lv[u] là mức của đỉnh uu, và it[u] là con trỏ cung của uu. Mỗi pha gọi bfs để tính mức, đặt lại con trỏ cung, rồi gọi dfs liên tiếp cho tới khi nó trả về 00. Nhiều kết nối giữa cùng hai máy chỉ là các cung song song, không cần gộp. Luồng có thể tới 1000⋅109=10121000 \cdot 10^9 = 10^{12}, nên cần dùng kiểu long long.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 505, M = 2005, INF = 4e18;
ll n, m, ec, to[M], cap[M], lv[N], it[N];
vector<ll> g[N];

void addEdge(ll u, ll v, ll c) {
    to[ec] = v, cap[ec] = c, g[u].push_back(ec++);
    to[ec] = u, cap[ec] = 0, g[v].push_back(ec++);
}

bool bfs(ll s, ll t) {
    for (ll i = 0; i <= n; i++) lv[i] = -1;
    queue<ll> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        for (ll e : g[u])
            if (cap[e] > 0 && lv[to[e]] < 0) {
                lv[to[e]] = lv[u] + 1;
                q.push(to[e]);
            }
    }
    return lv[t] >= 0;
}

ll dfs(ll u, ll t, ll f) {
    if (u == t) return f;
    for (; it[u] < (ll)g[u].size(); it[u]++) {
        ll e = g[u][it[u]], v = to[e];
        if (cap[e] > 0 && lv[v] == lv[u] + 1) {
            ll d = dfs(v, t, min(f, cap[e]));
            if (d > 0) {
                cap[e] -= d;
                cap[e ^ 1] += d;
                return d;
            }
        }
    }
    return 0;
}

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;
        addEdge(a, b, c);
    }
    ll flow = 0;
    while (bfs(1, n)) {
        for (ll i = 0; i <= n; i++) it[i] = 0;
        ll f;
        while ((f = dfs(1, n, INF)) > 0) flow += f;
    }
    cout << flow << '\n';
}
5 7
1 2 4
1 3 7
2 4 3
3 2 2
3 4 4
4 5 6
2 5 1

Độ phức tạp: O(V2E)O(V^2 E) trong trường hợp xấu nhất; với V=500V = 500 và E=2000E = 2000 cung (tính cả cung ngược), cận này là 5⋅1085 \cdot 10^8, nhưng thực tế Dinic chạy nhanh hơn cận này rất nhiều.

Nếu bỏ con trỏ cung, chương trình vẫn đúng nhưng chậm hơn: mỗi lần dfs lại thử lại từ đầu mọi cung đã vô ích. Hình dưới đo trên một mạng ghép cặp lớn:

Ghép cặp 5000 + 5000 đỉnh, 60 nghìn cạnh, bằng Dinic.mỗi lần DFS thử lại từ cung đầu34 ms · gấp 13 lầncon trỏ it[u] nhớ cung đang thử2,7 ms
Ghép cặp 5000 + 5000 đỉnh với 60 nghìn cạnh bằng Dinic, có và không có con trỏ cung.

Chỉ thêm một mảng con trỏ mà chương trình nhanh hơn khoảng 1313 lần, và chênh lệch thường càng lớn khi đồ thị càng dày.

VLát cắt nhỏ nhất

Nhắc lại, một lát cắt chia các đỉnh thành phần SS chứa ss và phần TT chứa tt, và sức chứa của lát cắt là tổng sức chứa các cung từ SS sang TT. Bài toán lát cắt nhỏ nhất (minimum cut) là tìm lát cắt có sức chứa nhỏ nhất. Nói cách khác: xóa các cung có tổng sức chứa nhỏ nhất để không còn đường đi từ ss tới tt.

Ô Chứng minh ở mục Cung ngược và Ford-Fulkerson đã cho ta một kết quả quan trọng, gọi là định lý luồng cực đại bằng lát cắt nhỏ nhất: giá trị luồng cực đại đúng bằng sức chứa của lát cắt nhỏ nhất. Chứng minh đó cũng cho cách tìm lát cắt: chạy luồng cực đại xong, BFS thêm một lần từ ss trong đồ thị dư; các đỉnh tới được là phần SS, và các cung từ SS sang TT là các cung cần xóa.

Hỏi nhanh

Luồng cực đại vừa chạy xong. Gọi SS là các đỉnh tới được từ ss trong đồ thị dư, TT là các đỉnh còn lại. Xét một cung u→vu \to v của mạng, với uu thuộc SS và vv thuộc TT.

Luồng trên cung u→vu \to v bằng bao nhiêu?

Bài “xóa ít cạnh nhất để tách ss khỏi tt” là bài lát cắt nhỏ nhất với sức chứa 11 cho mỗi cạnh. Với đồ thị vô hướng, mỗi cạnh uu, vv được thay bằng hai cung u→vu \to v và v→uv \to u cùng sức chứa 11. Trong cài đặt bằng danh sách cạnh, ta chỉ cần một cặp cung số hiệu 2k2k và 2k+12k + 1, cả hai đều có sức chứa 11: cặp cung này vừa là hai chiều của cạnh, vừa là cung ngược của nhau.

Sử dụng kiến thức này cũng có thể giải bài: ngoài việc xóa cạnh, được phép tắt một số đỉnh đặc biệt để chúng không còn nhận hàng, nhưng hàng vẫn đi qua chúng được. Thêm một đích chung, nối mỗi đỉnh đặc biệt tới đích chung bằng một cung sức chứa 11; cắt cung đó nghĩa là tắt đỉnh đó.

Ví dụChốt Chặn Tối Thiểu

Đề. Cho đồ thị vô hướng n≤500n \le 500 đỉnh, m≤1000m \le 1000 cạnh. Đóng ít cạnh nhất để không còn đường đi giữa đỉnh 11 và đỉnh nn; in số cạnh cần đóng và các cạnh đó.

Lời giải. Đây là bài lát cắt nhỏ nhất với sức chứa 11 cho mỗi cạnh, nên số cạnh cần đóng bằng luồng cực đại. Mỗi cạnh là một cặp cung cùng sức chứa 11 như trên. Chạy Dinic xong, gọi bfs(1, n) thêm một lần: lúc này không tới được nn, nhưng các đỉnh có lv[v] >= 0 chính là các đỉnh tới được từ 11, tức là phần SS. Cạnh có đúng một đầu thuộc SS là cạnh cần đóng; theo câu hỏi trên, các cạnh này đều đầy theo chiều từ SS sang TT, nên số cạnh đúng bằng luồng.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 505, M = 2005, INF = 4e18;
ll n, m, ec, to[M], cap[M], lv[N], it[N], ea[M], eb[M];
vector<ll> g[N];

bool bfs(ll s, ll t) {
    for (ll i = 0; i <= n; i++) lv[i] = -1;
    queue<ll> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        for (ll e : g[u])
            if (cap[e] > 0 && lv[to[e]] < 0) {
                lv[to[e]] = lv[u] + 1;
                q.push(to[e]);
            }
    }
    return lv[t] >= 0;
}

ll dfs(ll u, ll t, ll f) {
    if (u == t) return f;
    for (; it[u] < (ll)g[u].size(); it[u]++) {
        ll e = g[u][it[u]], v = to[e];
        if (cap[e] > 0 && lv[v] == lv[u] + 1) {
            ll d = dfs(v, t, min(f, cap[e]));
            if (d > 0) {
                cap[e] -= d;
                cap[e ^ 1] += d;
                return d;
            }
        }
    }
    return 0;
}

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];
        to[ec] = eb[i], cap[ec] = 1, g[ea[i]].push_back(ec++);
        to[ec] = ea[i], cap[ec] = 1, g[eb[i]].push_back(ec++);
    }
    ll flow = 0;
    while (bfs(1, n)) {
        for (ll i = 0; i <= n; i++) it[i] = 0;
        ll f;
        while ((f = dfs(1, n, INF)) > 0) flow += f;
    }
    bfs(1, n);
    cout << flow << '\n';
    for (ll i = 1; i <= m; i++)
        if ((lv[ea[i]] >= 0) != (lv[eb[i]] >= 0)) cout << ea[i] << ' ' << eb[i] << '\n';
}
6 8
1 2
1 3
2 4
3 4
2 5
3 5
4 6
5 6

Độ phức tạp: luồng không quá mm, mỗi pha tốn O(m)O(m) khi mọi sức chứa bằng 11, và số pha cũng nhỏ; với m=1000m = 1000 chương trình chạy rất nhanh, cỡ O(mm)≈3⋅104O(m \sqrt{m}) \approx 3 \cdot 10^4 bước.

VIGhép cặp hai phía

Bài toán: có hai nhóm đỉnh AA và BB, và một số cạnh nối một đỉnh của AA với một đỉnh của BB. Chọn nhiều cạnh nhất sao cho không có hai cạnh được chọn nào chung đỉnh. Các cạnh được chọn gọi là một cặp ghép (matching).

Ta dựng một mạng: thêm nguồn ss nối tới mọi đỉnh của AA bằng cung sức chứa 11; mọi đỉnh của BB nối tới đích tt bằng cung sức chứa 11; mỗi cạnh giữa AA và BB thành cung từ AA sang BB sức chứa 11. Mỗi đỉnh của AA nhận nhiều nhất 11 đơn vị từ nguồn, mỗi đỉnh của BB gửi nhiều nhất 11 đơn vị tới đích, nên mỗi luồng mà luồng trên mọi cung là số nguyên ứng với đúng một cặp ghép: cặp ghép gồm các cung từ AA sang BB có luồng 11. Khi mọi sức chứa là số nguyên, Ford-Fulkerson, Edmonds-Karp và Dinic đều cho luồng như vậy, vì mỗi lần đẩy chỉ cộng trừ các số nguyên.

Trên mạng ghép cặp, Dinic chạy trong O(EV)O(E \sqrt{V}), nhanh hơn nhiều so với cận chung O(V2E)O(V^2 E).

Ví dụGhép Đôi Sân Khấu

Đề. Có n≤500n \le 500 người ở nhóm thứ nhất, m≤500m \le 500 người ở nhóm thứ hai, và k≤1000k \le 1000 cặp hợp lệ, mỗi cặp gồm một người mỗi nhóm. Chọn nhiều cặp nhất, mỗi người thuộc nhiều nhất một cặp, và in các cặp được chọn.

Lời giải. Dựng mạng ghép cặp như trên. Đánh số: nguồn là đỉnh 00, nhóm thứ nhất là các đỉnh 11 tới nn, nhóm thứ hai là các đỉnh n+1n + 1 tới n+mn + m, đích là đỉnh n+m+1n + m + 1. Hàm addEdge trả về số hiệu cung vừa thêm, và mảng eid giữ số hiệu cung của từng cặp hợp lệ. Chạy Dinic xong, cung của một cặp hết sức chứa nghĩa là có luồng 11 đi qua, tức là cặp đó được chọn. Nếu một cặp xuất hiện hai lần trong dữ liệu, ta có hai cung song song, nhưng nhiều nhất một cung được dùng, vì mỗi người ở nhóm thứ nhất chỉ nhận 11 đơn vị từ nguồn.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1005, M = 6005, INF = 4e18;
ll n, m, k, V, ec, to[M], cap[M], lv[N], it[N], ea[M], eb[M], eid[M];
vector<ll> g[N];

bool bfs(ll s, ll t) {
    for (ll i = 0; i <= V; i++) lv[i] = -1;
    queue<ll> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        for (ll e : g[u])
            if (cap[e] > 0 && lv[to[e]] < 0) {
                lv[to[e]] = lv[u] + 1;
                q.push(to[e]);
            }
    }
    return lv[t] >= 0;
}

ll dfs(ll u, ll t, ll f) {
    if (u == t) return f;
    for (; it[u] < (ll)g[u].size(); it[u]++) {
        ll e = g[u][it[u]], v = to[e];
        if (cap[e] > 0 && lv[v] == lv[u] + 1) {
            ll d = dfs(v, t, min(f, cap[e]));
            if (d > 0) {
                cap[e] -= d;
                cap[e ^ 1] += d;
                return d;
            }
        }
    }
    return 0;
}

ll addEdge(ll u, ll v, ll c) {
    to[ec] = v, cap[ec] = c, g[u].push_back(ec++);
    to[ec] = u, cap[ec] = 0, g[v].push_back(ec++);
    return ec - 2;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m >> k;
    V = n + m + 1;
    for (ll i = 1; i <= k; i++) {
        cin >> ea[i] >> eb[i];
        eid[i] = addEdge(ea[i], n + eb[i], 1);
    }
    for (ll i = 1; i <= n; i++) addEdge(0, i, 1);
    for (ll j = 1; j <= m; j++) addEdge(n + j, V, 1);
    ll flow = 0;
    while (bfs(0, V)) {
        for (ll i = 0; i <= V; i++) it[i] = 0;
        ll f;
        while ((f = dfs(0, V, INF)) > 0) flow += f;
    }
    cout << flow << '\n';
    for (ll i = 1; i <= k; i++)
        if (cap[eid[i]] == 0) cout << ea[i] << ' ' << eb[i] << '\n';
}
4 3 7
1 1
1 2
2 1
2 3
3 2
4 2
4 3

Độ phức tạp: O(EV)O(E \sqrt{V}) trên mạng ghép cặp; với V≈1000V \approx 1000 đỉnh và E≈2000E \approx 2000 cung, cần cỡ 6⋅1046 \cdot 10^4 bước.

VIIChọn dự án

Bài toán: có một số dự án, mỗi dự án mang lại một khoản lợi nhuận, và một số công cụ, mỗi công cụ tốn một khoản chi phí. Mỗi dự án cần một số công cụ: chọn làm dự án thì phải mua đủ các công cụ nó cần. Chọn các dự án và công cụ để tổng lợi nhuận trừ tổng chi phí lớn nhất.

Dựng mạng như sau:

  • nguồn ss nối tới mỗi dự án bằng cung có sức chứa bằng lợi nhuận của dự án;
  • mỗi công cụ nối tới đích tt bằng cung có sức chứa bằng chi phí của công cụ;
  • nếu dự án xx cần công cụ yy, thêm cung x→yx \to y có sức chứa vô hạn, trong code là một số rất lớn.

Với một lát cắt (S,T)(S, T) có sức chứa hữu hạn, ta chọn các dự án và công cụ thuộc phần SS. Đáp án là tổng lợi nhuận của mọi dự án trừ đi sức chứa của lát cắt nhỏ nhất (xem ô Chứng minh).

Chứng minh

Lát cắt có sức chứa hữu hạn không cắt cung vô hạn nào. Vì vậy nếu dự án xx thuộc SS và cần công cụ yy, thì yy cũng thuộc SS: cách chọn ứng với SS luôn hợp lệ. Ngược lại, mọi cách chọn hợp lệ cho một lát cắt hữu hạn, với SS gồm ss và các dự án, công cụ được chọn.

Các cung bị cắt, tức đi từ SS sang TT, gồm: cung từ ss tới các dự án không được chọn, và cung từ các công cụ được chọn tới tt. Vậy sức chứa của lát cắt bằng (lợi nhuận của các dự án không chọn) cộng (chi phí của các công cụ được chọn). Lấy tổng lợi nhuận của mọi dự án trừ đi số này, ta được đúng (lợi nhuận của các dự án được chọn) trừ (chi phí của các công cụ được chọn). Vì vậy lát cắt càng nhỏ thì cách chọn càng tốt.

Câu hỏi nhanh dành cho tài khoản Pro

Mở khóa bằng tài khoản Pro

Ví dụChọn Dự Án Trên Đồ Thị

Đề. Cho đồ thị n≤1000n \le 1000 đỉnh, đỉnh thứ ii có chi phí aia_i, và m≤1000m \le 1000 cạnh, mỗi cạnh có trọng số ww. Chọn một số cạnh; chọn cạnh nào thì phải chọn cả hai đầu mút của nó. Giá trị là tổng trọng số các cạnh được chọn trừ tổng chi phí các đỉnh được chọn. Tìm giá trị lớn nhất.

Lời giải. Mỗi cạnh là một dự án với lợi nhuận ww, mỗi đỉnh là một công cụ với chi phí aia_i, và mỗi cạnh cần hai đầu mút của nó. Đánh số: nguồn là đỉnh 00, các cạnh là đỉnh 11 tới mm, các đỉnh của đồ thị là đỉnh m+1m + 1 tới m+nm + n, đích là đỉnh m+n+1m + n + 1. Đáp án là tổng trọng số mọi cạnh trừ đi luồng cực đại. Sức chứa vô hạn đặt bằng 4⋅10184 \cdot 10^{18}, lớn hơn hẳn mọi lát cắt hữu hạn (tổng trọng số không quá 101210^{12}), và vì hàm dfs luôn lấy min với sức chứa thật nên không bị tràn số. Không chọn cạnh nào thì giá trị là 00, nên đáp án không bao giờ âm.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2005, M = 10005, INF = 4e18;
ll n, m, V, ec, to[M], cap[M], lv[N], it[N];
vector<ll> g[N];

void addEdge(ll u, ll v, ll c) {
    to[ec] = v, cap[ec] = c, g[u].push_back(ec++);
    to[ec] = u, cap[ec] = 0, g[v].push_back(ec++);
}

bool bfs(ll s, ll t) {
    for (ll i = 0; i <= V; i++) lv[i] = -1;
    queue<ll> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        for (ll e : g[u])
            if (cap[e] > 0 && lv[to[e]] < 0) {
                lv[to[e]] = lv[u] + 1;
                q.push(to[e]);
            }
    }
    return lv[t] >= 0;
}

ll dfs(ll u, ll t, ll f) {
    if (u == t) return f;
    for (; it[u] < (ll)g[u].size(); it[u]++) {
        ll e = g[u][it[u]], v = to[e];
        if (cap[e] > 0 && lv[v] == lv[u] + 1) {
            ll d = dfs(v, t, min(f, cap[e]));
            if (d > 0) {
                cap[e] -= d;
                cap[e ^ 1] += d;
                return d;
            }
        }
    }
    return 0;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    V = n + m + 1;
    for (ll i = 1; i <= n; i++) {
        ll a;
        cin >> a;
        addEdge(m + i, V, a);
    }
    ll sum = 0;
    for (ll i = 1; i <= m; i++) {
        ll u, v, w;
        cin >> u >> v >> w;
        sum += w;
        addEdge(0, i, w);
        addEdge(i, m + u, INF);
        addEdge(i, m + v, INF);
    }
    ll flow = 0;
    while (bfs(0, V)) {
        for (ll i = 0; i <= V; i++) it[i] = 0;
        ll f;
        while ((f = dfs(0, V, INF)) > 0) flow += f;
    }
    cout << sum - flow << '\n';
}
2 1
3 4
1 2 10

Độ phức tạp: Dinic trên n+m+2≈2000n + m + 2 \approx 2000 đỉnh và 2(3m+n)≈80002(3m + n) \approx 8000 cung; cận O(V2E)O(V^2 E) rất lớn, nhưng thực tế chương trình chạy trong vài mili giây.

Bài nâng cao

Xem 2 bài nâng cao

Ví dụNgắt Các Bệ Nhảy

Đề. Một bảng H×WH \times W (H,W≤100H, W \le 100) gồm ô trống ., bệ thường o, bệ xuất phát S và bệ đích T. Từ bệ đang đứng, có thể nhảy thẳng tới bất kỳ bệ nào nằm cùng hàng hoặc cùng cột, bất kể các ô ở giữa. Tháo ít bệ o nhất để không thể đi từ S tới T; không được tháo S và T. In số bệ ít nhất cần tháo, hoặc −1-1 nếu không thể.

Lời giải. Nếu S và T cùng hàng hoặc cùng cột thì có thể nhảy thẳng, đáp án là −1-1. Ngược lại, cách dựng mạng tự nhiên nhất là mỗi bệ một đỉnh, nối hai bệ cùng hàng hoặc cùng cột, rồi tìm cách xóa ít đỉnh nhất. Nhưng một hàng có tới 100100 bệ thì đã có gần 50005000 cặp, và xóa đỉnh lại cần thêm một bước biến đổi.

Ta đổi cách nhìn: thay vì đỉnh là bệ, cho mỗi hàng và mỗi cột là một đỉnh, còn mỗi bệ là một cạnh nối hàng của nó với cột của nó. Đứng trên một bệ nghĩa là có thể nhảy tới mọi bệ khác cùng hàng hoặc cùng cột, tức là mọi cạnh có chung một đầu với cạnh của bệ đó. Vì vậy một cách nhảy từ S tới T ứng với một đường đi trong đồ thị mới, đi từ cạnh của S qua các cạnh của các bệ tới cạnh của T, và ngược lại. Tháo một bệ là xóa một cạnh.

Như vậy bài toán trở thành: xóa ít cạnh nhất để tách hàng của S khỏi hàng của T, trong đó cạnh của S và cạnh của T không được xóa. Đó là lát cắt nhỏ nhất: mỗi bệ o là cạnh vô hướng sức chứa 11, còn hai bệ S và T là cạnh vô hướng sức chứa vô hạn. Đồ thị chỉ có H+W≤200H + W \le 200 đỉnh và không quá 10410^4 cạnh.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 205, M = 20005, INF = 1e9;
ll h, w, n, ec, to[M], cap[M], lv[N], it[N];
vector<ll> g[N];

void addEdge(ll u, ll v, ll c) {
    to[ec] = v, cap[ec] = c, g[u].push_back(ec++);
    to[ec] = u, cap[ec] = c, g[v].push_back(ec++);
}

bool bfs(ll s, ll t) {
    for (ll i = 0; i <= n; i++) lv[i] = -1;
    queue<ll> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        for (ll e : g[u])
            if (cap[e] > 0 && lv[to[e]] < 0) {
                lv[to[e]] = lv[u] + 1;
                q.push(to[e]);
            }
    }
    return lv[t] >= 0;
}

ll dfs(ll u, ll t, ll f) {
    if (u == t) return f;
    for (; it[u] < (ll)g[u].size(); it[u]++) {
        ll e = g[u][it[u]], v = to[e];
        if (cap[e] > 0 && lv[v] == lv[u] + 1) {
            ll d = dfs(v, t, min(f, cap[e]));
            if (d > 0) {
                cap[e] -= d;
                cap[e ^ 1] += d;
                return d;
            }
        }
    }
    return 0;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> h >> w;
    n = h + w;
    ll si = 0, sj = 0, ti = 0, tj = 0;
    for (ll i = 1; i <= h; i++) {
        string row;
        cin >> row;
        for (ll j = 1; j <= w; j++) {
            if (row[j - 1] == 'o') addEdge(i, h + j, 1);
            if (row[j - 1] == 'S') si = i, sj = j;
            if (row[j - 1] == 'T') ti = i, tj = j;
        }
    }
    if (si == ti || sj == tj) {
        cout << -1 << '\n';
        return 0;
    }
    addEdge(si, h + sj, INF);
    addEdge(ti, h + tj, INF);
    ll flow = 0;
    while (bfs(si, ti)) {
        for (ll i = 0; i <= n; i++) it[i] = 0;
        ll f;
        while ((f = dfs(si, ti, INF)) > 0) flow += f;
    }
    cout << flow << '\n';
}
3 4
S.o.
....
..T.

Độ phức tạp: Dinic trên H+W≤200H + W \le 200 đỉnh và không quá 2⋅1042 \cdot 10^4 cung; luồng không quá số bệ, nên chương trình chạy rất nhanh.

Ví dụBàn Tròn Tổng Nguyên Tố

Đề. Có nn thẻ (3≤n≤2003 \le n \le 200), thẻ thứ ii mang giá trị aia_i (2≤ai≤1042 \le a_i \le 10^4). Chia mọi thẻ thành một hoặc nhiều nhóm, mỗi nhóm ít nhất ba thẻ và được xếp trên một vòng tròn, sao cho mọi cặp thẻ kề nhau trên cùng vòng tròn có tổng là số nguyên tố. In số vòng tròn và các thẻ trên mỗi vòng theo thứ tự, hoặc in Impossible nếu không xếp được.

Lời giải. Nhận xét về số nguyên tố. Mọi giá trị đều ít nhất 22, nên tổng hai thẻ ít nhất 44; số nguyên tố lớn hơn 22 đều lẻ, nên hai thẻ kề nhau phải có một thẻ lẻ và một thẻ chẵn. Vì vậy các thẻ chia thành hai nhóm, thẻ lẻ và thẻ chẵn, và mỗi thẻ có đúng hai thẻ kề, đều thuộc nhóm kia.

Mạng. Nguồn nối tới mỗi thẻ lẻ bằng cung sức chứa 22; mỗi thẻ chẵn nối tới đích bằng cung sức chứa 22; thẻ lẻ ii nối tới thẻ chẵn jj bằng cung sức chứa 11 khi ai+aja_i + a_j là số nguyên tố. Mỗi thẻ cần đúng hai thẻ kề, nên ta cần một luồng làm đầy mọi cung đi ra khỏi nguồn và mọi cung đi vào đích. Điều đó chỉ xảy ra khi số thẻ lẻ bằng số thẻ chẵn và luồng cực đại bằng đúng nn; nếu không thì in Impossible. Kiểm tra số nguyên tố tới 2⋅1042 \cdot 10^4 bằng sàng Eratosthenes (bài 0.9).

Ghép thành vòng. Các cung giữa hai nhóm có luồng 11 cho mỗi thẻ đúng hai thẻ kề. Đồ thị gồm các cạnh này có mọi đỉnh bậc 22, nên tách thành các chu trình rời nhau. Mỗi chu trình xen kẽ thẻ lẻ và thẻ chẵn, và không có hai cạnh song song giữa cùng hai thẻ, nên mỗi chu trình có ít nhất bốn thẻ. Ta đi theo từng chu trình để in các vòng tròn: bắt đầu ở một thẻ chưa thăm, mỗi bước đi tới thẻ kề chưa thăm có số nhỏ hơn, cho tới khi quay về gần điểm đầu.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 205, M = 25005, INF = 4e18, C = 20005;
ll n, V, ec, a[N], to[M], cap[M], lv[N], it[N], vis[N], ea[M], eb[M], eid[M];
bool comp[C];
vector<ll> g[N], adj[N];

ll addEdge(ll u, ll v, ll c) {
    to[ec] = v, cap[ec] = c, g[u].push_back(ec++);
    to[ec] = u, cap[ec] = 0, g[v].push_back(ec++);
    return ec - 2;
}

bool bfs(ll s, ll t) {
    for (ll i = 0; i <= V; i++) lv[i] = -1;
    queue<ll> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        for (ll e : g[u])
            if (cap[e] > 0 && lv[to[e]] < 0) {
                lv[to[e]] = lv[u] + 1;
                q.push(to[e]);
            }
    }
    return lv[t] >= 0;
}

ll dfs(ll u, ll t, ll f) {
    if (u == t) return f;
    for (; it[u] < (ll)g[u].size(); it[u]++) {
        ll e = g[u][it[u]], v = to[e];
        if (cap[e] > 0 && lv[v] == lv[u] + 1) {
            ll d = dfs(v, t, min(f, cap[e]));
            if (d > 0) {
                cap[e] -= d;
                cap[e ^ 1] += d;
                return d;
            }
        }
    }
    return 0;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    for (ll i = 2; i < C; i++)
        if (!comp[i])
            for (ll j = i * i; j < C; j += i) comp[j] = 1;
    cin >> n;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    V = n + 1;
    ll k = 0;
    for (ll i = 1; i <= n; i++) {
        if (a[i] % 2 == 0) {
            addEdge(i, V, 2);
            continue;
        }
        addEdge(0, i, 2);
        for (ll j = 1; j <= n; j++)
            if (a[j] % 2 == 0 && !comp[a[i] + a[j]]) {
                k++;
                ea[k] = i, eb[k] = j;
                eid[k] = addEdge(i, j, 1);
            }
    }
    ll flow = 0;
    while (bfs(0, V)) {
        for (ll i = 0; i <= V; i++) it[i] = 0;
        ll f;
        while ((f = dfs(0, V, INF)) > 0) flow += f;
    }
    if (flow != n) {
        cout << "Impossible\n";
        return 0;
    }
    for (ll i = 1; i <= k; i++)
        if (cap[eid[i]] == 0) {
            adj[ea[i]].push_back(eb[i]);
            adj[eb[i]].push_back(ea[i]);
        }
    vector<vector<ll> > cycles;
    for (ll i = 1; i <= n; i++) {
        if (vis[i]) continue;
        vector<ll> cyc;
        ll u = i;
        while (u) {
            vis[u] = 1;
            cyc.push_back(u);
            ll nxt = 0;
            for (ll v : adj[u])
                if (!vis[v] && (!nxt || v < nxt)) nxt = v;
            u = nxt;
        }
        cycles.push_back(cyc);
    }
    cout << cycles.size() << '\n';
    for (ll i = 0; i < (ll)cycles.size(); i++) {
        cout << cycles[i].size();
        for (ll v : cycles[i]) cout << ' ' << v;
        cout << '\n';
    }
}
4
3 2 5 8

Độ phức tạp: sàng O(Clog⁡log⁡C)O(C \log \log C) với C=2⋅104C = 2 \cdot 10^4; mạng có n+2n + 2 đỉnh và không quá 2⋅100⋅100+2n≈2⋅1042 \cdot 100 \cdot 100 + 2n \approx 2 \cdot 10^4 cung, Dinic chạy rất nhanh; ghép vòng tốn O(n)O(n).

Khi đi thi

  • Gặp “xóa ít nhất để tách ss khỏi tt”, “ghép được nhiều cặp nhất”, “nhiều đường đi không chung cạnh nhất”, hay “chọn tập có lợi nhất với ràng buộc kéo theo” thì nghĩ tới luồng. Phần khó là dựng đúng mạng: nên vẽ mạng ra trên ví dụ nhỏ của đề và kiểm tra bằng tay.
  • Giới hạn đặt trên đỉnh thì tách mỗi đỉnh thành hai đỉnh, một đỉnh vào và một đỉnh ra, nối bằng một cung có sức chứa bằng giới hạn đó. Cạnh vô hướng là một cặp cung cùng sức chứa theo hai chiều.
  • Dinic cài bằng danh sách cạnh, cung ngược của cung ee là cung e⊕1e \oplus 1, có con trỏ cung, thường đủ nhanh cho mạng cỡ 10410^4 đỉnh và 10510^5 cung. Sức chứa vô hạn đặt bằng một số rất lớn như 4⋅10184 \cdot 10^{18}, và không cộng hai số như vậy với nhau.

Tóm tắt

  • Đồ thị dư gồm cung xuôi với sức chứa còn lại và cung ngược với lượng có thể rút lại; luồng là cực đại khi và chỉ khi không còn đường tăng luồng.
  • Ford-Fulkerson tìm đường tăng bất kỳ, có thể chậm khi luồng lớn; Edmonds-Karp tìm đường ngắn nhất bằng BFS, không quá O(VE)O(VE) lần tăng; Dinic tăng luồng theo từng pha trên đồ thị phân mức, O(V2E)O(V^2 E).
  • Luồng cực đại bằng lát cắt nhỏ nhất; phần SS của lát cắt là các đỉnh tới được từ ss trong đồ thị dư cuối cùng.
  • Ghép cặp hai phía: nguồn tới AA, AA tới BB, BB tới đích, mọi sức chứa bằng 11; cặp ghép là các cung từ AA sang BB có luồng.
  • Chọn dự án: lợi nhuận nối từ nguồn, chi phí nối tới đích, ràng buộc là cung vô hạn; đáp án là tổng lợi nhuận trừ lát cắt nhỏ nhất.

Bài tập

Bài tập ngoài

Cung ngược và Ford-Fulkerson

IIDelivery BearsCodeforces

Lát cắt nhỏ nhất

VTreasure IslandCodeforces

Đọc thêm