Chương 4·Bài 4.9.2

Flow Modeling

Có nn người và mm việc; mỗi người chỉ làm được một số việc trong danh sách, mỗi người nhận nhiều nhất một việc, mỗi việc giao cho nhiều nhất một người. Giao được nhiều việc nhất là bao nhiêu? Nối nguồn tới từng người, từng người tới các việc họ làm được, từng việc tới đích, mọi sức chứa bằng 11: luồng cực đại chính là số việc giao được nhiều nhất, và Dinic tìm nó trong O(EV)O(E\sqrt{V}) với VV đỉnh, EE cung.

Phần lớn bài luồng khi đi thi không khó ở thuật toán mà ở bước dựng mạng: nhận ra đâu là một đơn vị luồng, đâu là sức chứa, và vì sao mỗi phương án của đề ứng với đúng một luồng hay một lát cắt. Bài này đi qua bốn cách dựng mạng hay gặp nhất: ghép cặp hai phía, tách đỉnh để tìm các đường đi rời nhau, chọn dự án bằng lát cắt nhỏ nhất, và luồng có cận dưới. Mọi chương trình dùng lại nguyên khuôn Dinic của bài 4.9.1.

IGhép cặp hai phía và phủ đỉnh

Một câu lạc bộ có bốn bạn ở nhóm A và ba bạn ở nhóm B, cùng bảy cặp có thể biểu diễn chung: (1,1)(1, 1), (1,2)(1, 2), (2,1)(2, 1), (2,3)(2, 3), (3,2)(3, 2), (4,2)(4, 2), (4,3)(4, 3), số đầu là bạn nhóm A, số sau là bạn nhóm B. Mỗi bạn chỉ vào nhiều nhất một cặp; chọn được nhiều cặp nhất là bao nhiêu? Ba cặp (1,1)(1, 1), (2,3)(2, 3), (3,2)(3, 2) không chung người nào, và không thể chọn bốn cặp vì nhóm B chỉ có ba bạn.

Đồ thị mà các đỉnh chia thành hai nhóm, mỗi cạnh nối một đỉnh nhóm này với một đỉnh nhóm kia, gọi là đồ thị hai phía (bipartite graph). Một tập cạnh không có hai cạnh nào chung đỉnh gọi là một cặp ghép (matching); bài toán trên là tìm cặp ghép lớn nhất, tức có nhiều cạnh nhất.

Ta dựng mạng: nguồn ss nối tới mọi đỉnh nhóm A, mỗi cạnh từ A sang B thành một cung, mọi đỉnh nhóm B nối tới đích tt, tất cả sức chứa bằng 11. Mỗi đỉnh nhóm A nhận nhiều nhất 11 đơn vị từ nguồn, mỗi đỉnh nhóm B gửi nhiều nhất 11 đơn vị tới đích, nên mỗi luồng mà mọi cung chở 00 hoặc 11 ứng với đúng một cặp ghép: cặp ghép gồm các cung từ A sang B đang chở 11, và giá trị của luồng bằng số cặp. Khi mọi sức chứa là số nguyên, Dinic chỉ cộng trừ các số nguyên, nên luồng nó tìm được đúng là một luồng như vậy.

Mạng ghép cặp, mọi cung sức chứa 1: ba đường cam là ba cặp được chọn, (1, 1), (2, 3) và (3, 2).st1234123nguồnđíchnhóm Anhóm B
Mạng ghép cặp, mọi cung sức chứa 1: ba đường cam là ba cặp được chọn, (1, 1), (2, 3) và (3, 2).

Trên mạng mà mọi sức chứa bằng 11, Dinic chạy nhanh hơn hẳn cận chung O(V2E)O(V^2 E): chỉ cần O(V)O(\sqrt V) pha, nên cả thuật toán tốn O(EV)O(E\sqrt V) (xem ô Chứng minh). Với các bài ghép cặp thường gặp khi thi, khuôn Dinic là đủ nhanh.

Chứng minh

Mỗi pha của Dinic trên mạng sức chứa 11 tốn O(E)O(E), vì mỗi cung bị con trỏ bỏ qua nhiều nhất một lần và mỗi lần đẩy luồng làm đầy cả đường. Còn phải đếm số pha. Trên mạng ghép cặp, mỗi đỉnh nhóm A chỉ nhận vào 11 đơn vị và mỗi đỉnh nhóm B chỉ gửi đi 11 đơn vị, nên các đường tăng luồng còn lại sau mỗi pha luôn tách được thành các đường không chung đỉnh trong.

Sau V\sqrt V pha, mỗi đường tăng luồng dài hơn V\sqrt V cung, vì độ dài đường ngắn nhất tăng sau mỗi pha. Gọi FF là luồng cực đại, ff là luồng lúc đó; phần chênh F−fF - f tách được thành F−fF - f đường không chung đỉnh, mỗi đường hơn V\sqrt V đỉnh, nên F−f<VF - f < \sqrt V. Mỗi pha sau đó tăng luồng ít nhất 11, nên chỉ còn thêm không quá V\sqrt V pha. Tổng cộng O(V)O(\sqrt V) pha.

Kết quả này của Hopcroft và Karp, trong bài báo An n^5/2 Algorithm for Maximum Matchings in Bipartite Graphs (1973); Even và Tarjan chứng minh cho Dinic trên mọi mạng sức chứa 11 trong bài Network Flow and Testing Graph Connectivity (1975).

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

Đề. Có n≤500n \le 500 bạn ở nhóm A, m≤500m \le 500 bạn ở nhóm B, và k≤1000k \le 1000 cặp có thể biểu diễn chung, mỗi cặp gồm một bạn mỗi nhóm. Chọn nhiều cặp nhất, mỗi bạn 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. Nguồn là đỉnh 00, nhóm A là các đỉnh 11 tới nn, nhóm B 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 id giữ số hiệu cung của từng cặp. Chạy Dinic xong, cung của một cặp hết sức chứa nghĩa là đang chở 11, tức 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ì bạn ở nhóm A 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;
ll n, m, k, cap[M];
int ec, to[M], lv[N], it[N], ea[M], eb[M], id[M];
vector<int> g[N];

int addEdge(int u, int 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(int s, int t) {
    memset(lv, -1, sizeof(lv));
    queue<int> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int 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(int u, int t, ll f) {
    if (u == t) return f;
    for (; it[u] < (int)g[u].size(); it[u]++) {
        int 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 maxFlow(int s, int t) {
    ll flow = 0, f;
    while (bfs(s, t)) {
        memset(it, 0, sizeof(it));
        while ((f = dfs(s, t, 1e18)) > 0) flow += f;
    }
    return flow;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m >> k;
    int s = 0, t = n + m + 1;
    for (int i = 1; i <= n; i++) addEdge(s, i, 1);
    for (int j = 1; j <= m; j++) addEdge(n + j, t, 1);
    for (int i = 1; i <= k; i++) {
        cin >> ea[i] >> eb[i];
        id[i] = addEdge(ea[i], n + eb[i], 1);
    }
    cout << maxFlow(s, t) << '\n';
    for (int i = 1; i <= k; i++)
        if (cap[id[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 xuôi, cần cỡ 6⋅1046 \cdot 10^4 bước.

Cặp ghép lớn nhất còn trả lời một câu hỏi tưởng như ngược lại. Cho một lưới có vài đồng xu, mỗi thao tác lấy hết xu trên một hàng hay một cột; cần ít nhất bao nhiêu thao tác? Coi mỗi hàng là một đỉnh nhóm A, mỗi cột là một đỉnh nhóm B, mỗi đồng xu ở ô (r,c)(r, c) là một cạnh nối hàng rr với cột cc. Chọn một tập hàng và cột lấy hết xu nghĩa là mọi cạnh có ít nhất một đầu được chọn. Một tập đỉnh như vậy gọi là một phủ đỉnh (vertex cover).

Hỏi nhanh

Lưới 3×33 \times 3 có ba đồng xu nằm trên đường chéo chính, ở các ô (1,1)(1, 1), (2,2)(2, 2), (3,3)(3, 3). Mỗi thao tác lấy hết xu trên một hàng hoặc một cột. Cần ít nhất bao nhiêu thao tác để lấy hết xu?

Lý luận ở cuối câu hỏi đúng với mọi đồ thị: phủ đỉnh nào cũng có ít nhất số đỉnh bằng một cặp ghép bất kỳ, vì các cạnh của cặp ghép không chung đỉnh. Trên đồ thị hai phía, hai số này còn bằng nhau, và đây là định lý König: phủ đỉnh nhỏ nhất có đúng bằng số cạnh của cặp ghép lớn nhất. Cách tìm phủ đỉnh cũng đến từ lát cắt: dựng mạng ghép cặp nhưng cho các cung từ A sang B sức chứa vô hạn, chạy luồng cực đại, rồi gọi SS là các đỉnh tới được từ ss trên đồ thị dư. Phủ đỉnh gồm các đỉnh nhóm A không thuộc SS và các đỉnh nhóm B thuộc SS.

Chứng minh

Các cung từ A sang B có sức chứa vô hạn không làm thay đổi luồng cực đại, vì mỗi đỉnh nhóm A vẫn chỉ nhận 11 từ nguồn; luồng cực đại vẫn bằng cặp ghép lớn nhất. Lát cắt (S,T)(S, T) tìm từ đồ thị dư là lát cắt nhỏ nhất, nên nó không cắt cung vô hạn nào. Các cung bị cắt chỉ có thể là s→as \to a với aa thuộc A nằm ngoài SS, và b→tb \to t với bb thuộc B nằm trong SS; tập đỉnh CC gồm các aa và bb đó có đúng số đỉnh bằng sức chứa lát cắt, tức bằng cặp ghép lớn nhất.

CC là một phủ đỉnh. Xét một cạnh (a,b)(a, b). Nếu aa không thuộc SS thì aa thuộc CC. Nếu aa thuộc SS thì cung a→ba \to b vô hạn còn sức chứa dư, nên bb cũng tới được từ ss, tức bb thuộc SS và thuộc CC. Vậy mọi cạnh đều có một đầu thuộc CC. Kết hợp với chiều ngược lại (phủ đỉnh không ít hơn cặp ghép), CC là phủ đỉnh nhỏ nhất.

Định lý do Kőnig công bố năm 1931 trong bài Gráfok és mátrixok; định lý cùng nhiều cách chứng minh có ở chương 16 của sách Combinatorial Optimization (Schrijver).

Ví dụDọn Sạch Lưới Xu

Đề. Lưới n×nn \times n (n≤100n \le 100), mỗi ô rỗng . hoặc có một đồng xu o. Mỗi thao tác chọn một hàng hay một cột và lấy hết xu trên đó. Tìm số thao tác ít nhất và in các thao tác: 1 x là chọn hàng xx, 2 x là chọn cột xx.

Lời giải. Hàng là nhóm A (đỉnh 11 tới nn), cột là nhóm B (đỉnh n+1n + 1 tới 2n2n), mỗi đồng xu ở ô (r,c)(r, c) là cung từ hàng rr tới cột cc với sức chứa vô hạn. Theo định lý König, số thao tác ít nhất bằng luồng cực đại. Chạy Dinic xong, gọi bfs thêm một lần: hàng có lv âm (không thuộc SS) và cột có lv không âm (thuộc SS) là các thao tác cần làm. Lưới có tới 10410^4 đồng xu, nên mạng có 2n+22n + 2 đỉnh và tới 104+2n10^4 + 2n cung xuôi.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 205, M = 2.1e4;
ll n, cap[M];
int ec, to[M], lv[N], it[N];
string a[N];
vector<int> g[N];

void addEdge(int u, int 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(int s, int t) {
    memset(lv, -1, sizeof(lv));
    queue<int> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int 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(int u, int t, ll f) {
    if (u == t) return f;
    for (; it[u] < (int)g[u].size(); it[u]++) {
        int 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 maxFlow(int s, int t) {
    ll flow = 0, f;
    while (bfs(s, t)) {
        memset(it, 0, sizeof(it));
        while ((f = dfs(s, t, 1e18)) > 0) flow += f;
    }
    return flow;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n;
    int s = 0, t = 2 * n + 1;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        a[i] = " " + a[i];
        addEdge(s, i, 1);
        addEdge(n + i, t, 1);
    }
    for (int r = 1; r <= n; r++)
        for (int c = 1; c <= n; c++)
            if (a[r][c] == 'o') addEdge(r, n + c, 1e18);
    cout << maxFlow(s, t) << '\n';
    bfs(s, t);
    for (int r = 1; r <= n; r++)
        if (lv[r] < 0) cout << 1 << ' ' << r << '\n';
    for (int c = 1; c <= n; c++)
        if (lv[n + c] >= 0) cout << 2 << ' ' << c << '\n';
}
3
..o
o.o
...

Độ phức tạp: O(EV)O(E\sqrt V) với V=2n+2≤202V = 2n + 2 \le 202 và E≤104+200E \le 10^4 + 200 cung xuôi; cả chương trình chỉ cỡ 10510^5 bước.

IITách đỉnh và đường đi rời nhau

Mạng ở hình dưới có 77 nút và 99 liên kết một chiều. Ta muốn đi từ nút 11 tới nút 77 nhiều lần nhất, mỗi liên kết dùng không quá một lần trong tất cả các lần đi. Ba đường 1→2→71 \to 2 \to 7, 1→3→5→71 \to 3 \to 5 \to 7 và 1→4→6→71 \to 4 \to 6 \to 7 không chung liên kết nào, và không có đường thứ tư vì nút 11 chỉ có ba liên kết đi ra.

Cho mỗi liên kết sức chứa 11, mỗi đường đi là một đơn vị luồng, nên số đường không chung cạnh nhiều nhất bằng luồng cực đại. Ngược lại, từ một luồng nguyên giá trị kk ta luôn tách được kk đường như vậy: đi từ nguồn theo các cung đang chở luồng chưa dùng tới, đánh dấu cung đã đi, cho tới khi gặp đích. Đường đi không bao giờ kẹt giữa chừng: ở mỗi đỉnh trung gian, số cung chở luồng đi vào bằng số cung chở luồng đi ra, nên đã vào được thì luôn còn một cung chưa dùng để đi ra.

Ví dụCác Tuyến Không Trùng Cạnh

Đề. Có n≤500n \le 500 nút và m≤1000m \le 1000 liên kết có hướng, không có hai liên kết trùng nhau. Tìm số đường nhiều nhất từ nút 11 tới nút nn sao cho không liên kết nào được dùng ở hai đường, và in các đường.

Lời giải. Mỗi liên kết là một cung sức chứa 11; luồng cực đại kk là số đường. Để in, ta đi kk lần từ nút 11. Mỗi bước ở nút uu, tìm một cung xuôi của uu đang chở luồng mà chưa dùng: cung xuôi có số hiệu chẵn, và nó chở luồng khi sức chứa dư bằng 00. Đánh dấu cung đó, đi sang đầu kia, và dừng khi tới nút nn. Theo lý luận ở trên, mỗi lần đi đều tới được nn.

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

void addEdge(int u, int 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(int s, int t) {
    memset(lv, -1, sizeof(lv));
    queue<int> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int 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(int u, int t, ll f) {
    if (u == t) return f;
    for (; it[u] < (int)g[u].size(); it[u]++) {
        int 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 maxFlow(int s, int t) {
    ll flow = 0, f;
    while (bfs(s, t)) {
        memset(it, 0, sizeof(it));
        while ((f = dfs(s, t, 1e18)) > 0) flow += f;
    }
    return flow;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int a, b;
        cin >> a >> b;
        addEdge(a, b, 1);
    }
    ll k = maxFlow(1, n);
    cout << k << '\n';
    for (int p = 1; p <= k; p++) {
        int u = 1, len = 0;
        path[++len] = 1;
        while (u != n) {
            for (int e : g[u])
                if (e % 2 == 0 && cap[e] == 0 && !used[e]) {
                    used[e] = true;
                    u = to[e];
                    break;
                }
            path[++len] = u;
        }
        cout << len << '\n';
        for (int i = 1; i <= len; i++) cout << path[i] << (i == len ? '\n' : ' ');
    }
}
7 9
1 2
1 3
1 4
2 7
3 5
5 7
4 6
6 7
3 6

Độ phức tạp: Dinic trên mạng sức chứa 11 tốn O(mm)O(m\sqrt m); in các đường tốn O(k(n+m))O(k(n + m)) vì mỗi bước duyệt danh sách cung của một nút; với n=500n = 500, m=1000m = 1000 chương trình chạy tức thì.

Khi giới hạn đặt trên đỉnh thay vì trên cạnh, chẳng hạn mỗi nút chỉ được đi qua một lần, hay mỗi nút chỉ xử lý được c(v)c(v) đơn vị, ta tách đỉnh: thay đỉnh vv bằng hai đỉnh v1v_1 và v2v_2 nối bằng cung v1→v2v_1 \to v_2 sức chứa c(v)c(v); mọi cung đi vào vv đổi thành đi vào v1v_1, mọi cung đi ra khỏi vv đổi thành đi ra từ v2v_2. Đơn vị luồng nào đi qua vv cũng phải qua cung v1→v2v_1 \to v_2, nên tổng luồng qua vv không vượt c(v)c(v).

Tách đỉnh v thành v₁ và v₂: cung vào v đổi thành vào v₁, cung ra đổi thành ra từ v₂, cung giữa mang giới hạn c(v).c(v)v₁v₂
Tách đỉnh v thành v₁ và v₂: cung vào v đổi thành vào v₁, cung ra đổi thành ra từ v₂, cung giữa mang giới hạn c(v).

Với c(v)=1c(v) = 1 cho mọi đỉnh trung gian, luồng cực đại là số đường nhiều nhất từ ss tới tt không chung đỉnh trung gian nào. Bài ngược lại cũng giải được: xóa các đỉnh có tổng chi phí nhỏ nhất để tách ss khỏi tt là lát cắt nhỏ nhất trên mạng đã tách đỉnh, trong đó cung v1→v2v_1 \to v_2 mang chi phí của vv và mọi cung khác có sức chứa vô hạn, để lát cắt chỉ được cắt ở các đỉnh.

Sử dụng kiến thức này cũng có thể giải bài phủ đồ thị có hướng không chu trình bằng ít đường đi nhất, mỗi đỉnh thuộc đúng một đường: tách mỗi đỉnh thành một bản “ra” bên trái và một bản “vào” bên phải, mỗi cung u→vu \to v thành cạnh từ bản ra của uu tới bản vào của vv. Mỗi cạnh của một cặp ghép nối hai đỉnh liên tiếp trên cùng một đường, mỗi lần nối giảm số đường đi một, nên số đường ít nhất bằng nn trừ cặp ghép lớn nhất.

IIIChọn dự án bằng lát cắt nhỏ nhất

Một công ty có hai dự án: dự án D1 lãi 66 và cần công cụ C1; dự án D2 lãi 22 và cần cả hai công cụ C1, C2. Mua C1 tốn 44, mua C2 tốn 33. Làm dự án nào thì phải mua đủ công cụ nó cần; một công cụ đã mua dùng chung được cho nhiều dự án. Thử hết các cách: không làm gì được 00; chỉ làm D1 được 6−4=26 - 4 = 2; chỉ làm D2 được 2−4−3=−52 - 4 - 3 = -5; làm cả hai được 8−7=18 - 7 = 1. Cách tốt nhất là chỉ làm D1, lãi 22.

Tổng quát, có một số dự án, mỗi dự án có một khoản lãi, và một số công cụ, mỗi công cụ có một chi phí; mỗi dự án cần một số công cụ. Chọn các dự án và công cụ sao cho tổng lãi trừ tổng chi phí lớn nhất. Đây là bài toán chọn dự án (project selection), còn gọi là bài toán bao đóng có trọng số lớn nhất. Ta dựng mạng:

  • nguồn ss nối tới mỗi dự án bằng cung có sức chứa bằng lãi 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ụ;
  • dự án xx cần công cụ yy thì thêm cung x→yx \to y sức chứa vô hạn, trong code là 101810^{18}.

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 của mọi dự án trừ đi sức chứa của lát cắt nhỏ nhất. Ở ví dụ trên, tổng lãi là 88, lát cắt nhỏ nhất có S={s,D1,C1}S = \{s, D1, C1\} và sức chứa 66, nên lãi lớn nhất là 22, đúng như khi thử hết.

Phần S gồm ba đỉnh cam: cắt s → D2 và C1 → t tốn 2 + 4 = 6, nên lãi lớn nhất là 8 − 6 = 2, chọn D1 và mua C1.62∞∞∞43sD1D2C1C2t
Phần S gồm ba đỉnh cam: cắt s → D2 và C1 → t tốn 2 + 4 = 6, nên lãi lớn nhất là 8 − 6 = 2, chọn D1 và mua C1.
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 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 của mọi dự án trừ đi số này, ta được đúng (lãi 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ách đưa bài này về lát cắt nhỏ nhất có trong bài báo Maximal Closure of a Graph and Applications to Combinatorial Problems của Picard (1976).

Hỏi nhanh

Trong mạng chọn dự án, cung từ dự án xx tới công cụ yy mà xx cần có sức chứa vô hạn.

Vì sao không dùng sức chứa 11 cho cung này?

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 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 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 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 101810^{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 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 = 1e4 + 5;
ll n, m, cap[M];
int ec, to[M], lv[N], it[N];
vector<int> g[N];

void addEdge(int u, int 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(int s, int t) {
    memset(lv, -1, sizeof(lv));
    queue<int> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int 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(int u, int t, ll f) {
    if (u == t) return f;
    for (; it[u] < (int)g[u].size(); it[u]++) {
        int 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 maxFlow(int s, int t) {
    ll flow = 0, f;
    while (bfs(s, t)) {
        memset(it, 0, sizeof(it));
        while ((f = dfs(s, t, 1e18)) > 0) flow += f;
    }
    return flow;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    int s = 0, t = n + m + 1;
    for (int i = 1; i <= n; i++) {
        ll a;
        cin >> a;
        addEdge(m + i, t, a);
    }
    ll sum = 0;
    for (int i = 1; i <= m; i++) {
        int u, v;
        ll w;
        cin >> u >> v >> w;
        sum += w;
        addEdge(s, i, w);
        addEdge(i, m + u, 1e18);
        addEdge(i, m + v, 1e18);
    }
    cout << sum - maxFlow(s, t) << '\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à 3m+n≈40003m + n \approx 4000 cung xuôi; cận O(V2E)O(V^2 E) rất lớn, nhưng trên thực tế chương trình chạy trong vài mili giây.

IVMở rộng: luồng có cận dưới

Có khi đề bắt mỗi cung u→vu \to v chở ít nhất l(u,v)l(u, v) và nhiều nhất c(u,v)c(u, v), chẳng hạn mỗi tuyến đường phải có tối thiểu một số chuyến. Câu hỏi đầu tiên là có luồng nào thỏa mọi cận dưới không, gọi là luồng khả thi. Ta xét trước trường hợp không có nguồn, đích: mọi đỉnh đều phải cân bằng vào ra.

Ý tưởng là cho mỗi cung chở sẵn phần bắt buộc l(u,v)l(u, v), rồi tìm phần còn lại. Chở sẵn ll trên cung u→vu \to v làm vv thừa ll đơn vị và uu thiếu ll đơn vị; phần còn lại của cung chỉ được chở thêm tối đa c−lc - l. Gọi ex(v)ex(v) là tổng phần bắt buộc đi vào vv trừ tổng phần bắt buộc đi ra khỏi vv. Đỉnh có ex(v)>0ex(v) > 0 đang thừa và phải đẩy phần thừa đi, đỉnh có ex(v)<0ex(v) < 0 đang thiếu và phải nhận thêm. Ta thêm một nguồn phụ S′S' nối tới mỗi đỉnh thừa bằng cung sức chứa ex(v)ex(v), và mỗi đỉnh thiếu nối tới đích phụ T′T' bằng cung sức chứa −ex(v)-ex(v). Có luồng khả thi khi và chỉ khi luồng cực đại từ S′S' tới T′T' làm đầy mọi cung ra khỏi S′S'; khi đó luồng trên cung u→vu \to v là l(u,v)l(u, v) cộng phần chạy trên cung sức chứa c−lc - l.

C++
for (int i = 1; i <= m; i++) {
    addEdge(eu[i], ev[i], hi[i] - lo[i]);
    ex[ev[i]] += lo[i];
    ex[eu[i]] -= lo[i];
}
ll need = 0;
for (int v = 1; v <= n; v++) {
    if (ex[v] > 0) addEdge(S, v, ex[v]), need += ex[v];
    if (ex[v] < 0) addEdge(v, T, -ex[v]);
}
bool feasible = maxFlow(S, T) == need;

Độ phức tạp: một lần chạy Dinic trên mạng thêm hai đỉnh và không quá nn cung; cùng cỡ với một bài luồng cực đại thường.

Khi mạng có nguồn ss và đích tt thật, chỉ ss và tt được lệch vào ra. Ta thêm cung t→st \to s sức chứa vô hạn để luồng chạy vòng về, biến bài toán thành bài không nguồn, đích ở trên. Nếu có luồng khả thi, luồng trên cung t→st \to s là giá trị của một luồng khả thi từ ss tới tt. Muốn luồng lớn nhất, ta xóa cung t→st \to s (đặt sức chứa của nó và cung ngược bằng 00) rồi chạy tiếp luồng cực đại từ ss tới tt trên đồ thị dư, cộng thêm vào giá trị khả thi; muốn luồng nhỏ nhất thì chạy luồng cực đại từ tt về ss và trừ đi.

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ách đỉnh để xóa ít bệ nhất. Nhưng một hàng có tới 100100 bệ thì đã có gần 50005000 cặp, cả bảng có thể tới cỡ 10610^6 cạnh.

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 của bài 4.9.1: 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. Hàm addPair thêm một cạnh vô hướng là một cặp cung cùng sức chứa, như ở bài 4.9.1.

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

void addPair(int u, int 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(int s, int t) {
    memset(lv, -1, sizeof(lv));
    queue<int> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int 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(int u, int t, ll f) {
    if (u == t) return f;
    for (; it[u] < (int)g[u].size(); it[u]++) {
        int 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 maxFlow(int s, int t) {
    ll flow = 0, f;
    while (bfs(s, t)) {
        memset(it, 0, sizeof(it));
        while ((f = dfs(s, t, 1e18)) > 0) flow += f;
    }
    return flow;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> h >> w;
    int si = 0, sj = 0, ti = 0, tj = 0;
    for (int i = 1; i <= h; i++) {
        string row;
        cin >> row;
        row = " " + row;
        for (int j = 1; j <= w; j++) {
            if (row[j] == 'o') addPair(i, h + j, 1);
            if (row[j] == 'S') si = i, sj = j;
            if (row[j] == 'T') ti = i, tj = j;
        }
    }
    if (si == ti || sj == tj) {
        cout << -1 << '\n';
        return 0;
    }
    addPair(si, h + sj, 1e18);
    addPair(ti, h + tj, 1e18);
    cout << maxFlow(si, ti) << '\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 ra khỏi nguồn và mọi cung 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.1).

Ghép thành vòng. Các cung giữa hai nhóm đang chở 11 cho mỗi thẻ đúng hai thẻ kề, lưu vào nb[v][0] và nb[v][1]. Đồ 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: 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 cả hai thẻ kề đều đã thăm. Các thẻ được ghi lần lượt vào mảng ord, độ dài từng vòng vào len, rồi in.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 205, M = 2.1e4, C = 2e4 + 5;
ll n, a[N], cap[M];
int ec, to[M], lv[N], it[N], ea[M], eb[M], id[M], nb[N][2], deg[N], ord[N], len[N];
bool comp[C], vis[N];
vector<int> g[N];

int addEdge(int u, int 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(int s, int t) {
    memset(lv, -1, sizeof(lv));
    queue<int> q;
    q.push(s);
    lv[s] = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int 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(int u, int t, ll f) {
    if (u == t) return f;
    for (; it[u] < (int)g[u].size(); it[u]++) {
        int 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 maxFlow(int s, int t) {
    ll flow = 0, f;
    while (bfs(s, t)) {
        memset(it, 0, sizeof(it));
        while ((f = dfs(s, t, 1e18)) > 0) flow += f;
    }
    return flow;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    for (int i = 2; i < C; i++)
        if (!comp[i])
            for (ll j = 1LL * i * i; j < C; j += i) comp[j] = true;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    int s = 0, t = n + 1, k = 0;
    for (int i = 1; i <= n; i++) {
        if (a[i] % 2 == 0) {
            addEdge(i, t, 2);
            continue;
        }
        addEdge(s, i, 2);
        for (int j = 1; j <= n; j++)
            if (a[j] % 2 == 0 && !comp[a[i] + a[j]]) {
                k++;
                ea[k] = i, eb[k] = j;
                id[k] = addEdge(i, j, 1);
            }
    }
    if (maxFlow(s, t) != n) {
        cout << "Impossible\n";
        return 0;
    }
    for (int i = 1; i <= k; i++)
        if (cap[id[i]] == 0) {
            nb[ea[i]][deg[ea[i]]++] = eb[i];
            nb[eb[i]][deg[eb[i]]++] = ea[i];
        }
    int cnt = 0, pos = 0;
    for (int i = 1; i <= n; i++) {
        if (vis[i]) continue;
        cnt++;
        int u = i;
        while (u) {
            vis[u] = true;
            ord[++pos] = u;
            len[cnt]++;
            int v = 0;
            for (int d = 0; d < 2; d++)
                if (!vis[nb[u][d]] && (!v || nb[u][d] < v)) v = nb[u][d];
            u = v;
        }
    }
    cout << cnt << '\n';
    pos = 0;
    for (int c = 1; c <= cnt; c++) {
        cout << len[c];
        for (int i = 1; i <= len[c]; i++) cout << ' ' << ord[++pos];
        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á 100⋅100+2n≈104100 \cdot 100 + 2n \approx 10^4 cung xuôi, Dinic chạy rất nhanh; ghép vòng tốn O(n)O(n).

Khi đi thi

  • Bài hỏi “nhiều nhất bao nhiêu cặp, bao nhiêu đường” thì nghĩ tới luồng cực đại; bài hỏi “ít nhất bao nhiêu để chặn, để phủ” thì nghĩ tới lát cắt nhỏ nhất. Trước khi code, vẽ mạng trên ví dụ nhỏ của đề và kiểm tra bằng tay rằng mỗi phương án ứng với đúng một luồng hay một lát cắt.
  • Giới hạn đặt trên đỉnh thì tách đỉnh; ràng buộc “chọn A thì phải chọn B” là một cung vô hạn từ A tới B. Lưới thường là đồ thị hai phía khi tô màu ô như bàn cờ.
  • Đọc kết quả từ luồng: cặp ghép là các cung giữa đang chở luồng, phủ đỉnh và cách chọn dự án đọc từ phần SS của đồ thị dư cuối cùng.

Tóm tắt

  • Ghép cặp hai phía: nguồn tới nhóm A, nhóm A tới nhóm B, nhóm B tới đích, mọi sức chứa 11; Dinic chạy trong O(EV)O(E\sqrt V).
  • Định lý König: trên đồ thị hai phía, phủ đỉnh nhỏ nhất bằng cặp ghép lớn nhất; phủ gồm đỉnh nhóm A ngoài SS và đỉnh nhóm B trong SS.
  • Số đường không chung cạnh nhiều nhất bằng luồng cực đại với sức chứa 11; giới hạn trên đỉnh thì tách đỉnh thành hai đỉnh nối bằng một cung.
  • Chọn dự án: lãi 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 trừ lát cắt nhỏ nhất.
  • Luồng có cận dưới: chở sẵn phần bắt buộc, dồn phần thừa thiếu ở mỗi đỉnh vào nguồn phụ, đích phụ, rồi kiểm tra luồng cực đại có làm đầy các cung phụ không.

Bài tập

Bài tập ngoài

Đọc thêm