Chương 4·Bài 49

Network Flow

Một mạng là đồ thị có hướng với sức chứa trên mỗi cung, một đỉnh nguồn ss và một đỉnh đích tt. Luồng gán cho mỗi cung một lượng không vượt sức chứa, với mọi đỉnh trung gian lượng vào bằng lượng ra. Bài toán luồng cực đại tìm luồng lớn nhất đi từ ss tới tt. Nó quan trọng không chỉ vì chính nó: lát cắt nhỏ nhất, ghép cặp hai phía, đường đi không chung cạnh, chọn tập có lợi nhất với ràng buộc kéo theo đều là luồng cực đại trên một mạng dựng khéo.

Đồ thị dư và thuật toán Dinic

Với một luồng ff, đồ thị dư có cung u→vu \to v sức chứa c−fc - f (phần còn đẩy thêm được) và cung ngược v→uv \to u sức chứa ff (phần có thể rút lại). Một đường tăng luồng là đường từ ss tới tt trong đồ thị dư; đẩy thêm theo nó một lượng bằng sức chứa nhỏ nhất trên đường. Cung ngược cho phép “hối hận”: luồng đã đi sai đường được đẩy ngược về để đi đường khác. Luồng là cực đại khi và chỉ khi không còn đường tăng luồng.

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.

Dinic làm theo pha. Mỗi pha BFS từ ss tính mức (khoảng cách) của mọi đỉnh trong đồ thị dư; chỉ dùng cung đi từ mức dd lên mức d+1d + 1. Trên đồ thị phân mức đó, DFS đẩy luồng cho tới khi không còn đường nào (luồng chặn), với con trỏ it[u] nhớ cung đã thử để không thử lại cung đã hết. Có O(V)O(V) pha, tổng O(V2E)O(V^2 E); trên đồ thị hai phía sức chứa đơn vị chỉ O(EV)O(E \sqrt V). Thực tế Dinic chạy nhanh hơn cận này rất nhiều.

Cài bằng danh sách cạnh: cung thứ ee và cung ngược của nó là ee và e⊕1e \oplus 1, nên cập nhật một cặp chỉ là cap[e] -= d; cap[e ^ 1] += d.

C++
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;
}

Hỏi nhanh

Vì sao đồ thị dư cần cung ngược với sức chứa bằng luồng đang có?

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

Đề. n≤500n \le 500 máy, m≤1000m \le 1000 kết nối một chiều có băng thông tới 10910^9. Tìm tốc độ truyền lớn nhất từ máy 1 tới máy nn.

Lời giải. Luồng cực đại từ 1 tới nn bằng Dinic. Mỗi pha: BFS từ nguồn trên đồ thị dư để gán mức cho từng đỉnh; rồi DFS chỉ đi theo cung từ mức ℓ\ell sang mức ℓ+1\ell + 1, đẩy luồng tới khi không đẩy được nữa. Sau mỗi pha, khoảng cách từ nguồn tới đích trên đồ thị dư tăng ít nhất 1, nên không quá nn pha. Kết nối lặp chỉ là cung song song, không cần gộp. Luồng tới 1000⋅109=10121000 \cdot 10^9 = 10^{12}, dùng long long.

Bước 1: cung và cung ngược. Mỗi cung có một cung ngược sức chứa 0 đặt ngay sau nó, nên hai cung là e và e ^ 1. Đẩy d đơn vị qua e thì cap[e] -= d, cap[e ^ 1] += d: cung ngược cho phép “trả lại” luồng ở pha sau.

C++
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++);
}

Bước 2: BFS gán mức. lv là khoảng cách theo số cung, chỉ đi cung còn cap > 0. Trả về đích có tới được hay không.

C++
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;
}

Bước 3: DFS đẩy luồng. dfs(u, t, f) tìm một đường từ u tới đích trên đồ thị mức, mang tối đa f đơn vị, trả về lượng đẩy được. it[u] nhớ cung đang xét của u: cung đã thử mà không đẩy được (bão hòa hoặc dẫn vào ngõ cụt) thì pha này không thử lại, nên mỗi pha tốn O(VE)O(VE).

C++
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;
}

Bước 4: các pha. Mỗi pha đặt lại it, rồi gọi dfs liên tiếp tới khi trả 0. INF là luồng mang vào từ nguồn, không giới hạn.

C++
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;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using 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

O(V2E)O(V^2 E) trong trường hợp xấu nhất, thực tế nhanh hơn nhiều.

Lát cắt nhỏ nhất

Một lát cắt chia đỉnh thành hai phần S∋sS \ni s và T∋tT \ni t; giá trị của nó là tổng sức chứa các cung từ SS sang TT. Mọi luồng đều phải đi qua lát cắt, nên luồng không vượt lát cắt nào. Định lý luồng cực đại bằng lát cắt nhỏ nhất: khi luồng cực đại, gọi SS là các đỉnh tới được từ ss trong đồ thị dư; mọi cung từ SS sang TT đều bão hòa (nếu không, đầu kia đã tới được), mọi cung từ TT về SS đều rỗng, nên giá trị lát cắt này đúng bằng luồng. Vậy chạy luồng cực đại xong, một lần BFS cho luôn lát cắt nhỏ nhất.

Bài “bỏ ít cạnh nhất để tách ss khỏi tt” là lát cắt nhỏ nhất với sức chứa 1 mỗi cạnh. Cạnh vô hướng là hai cung ngược chiều; cài gọn bằng một cặp cung mà cả hai chiều đều có sức chứa 1. “Tắt” một đỉnh đặc biệt cũng là một cạnh để cắt: nối nó tới một đích chung bằng cung sức chứa 1.

Hỏi nhanh

Luồng cực đại vừa chạy xong. Lấy SS là các đỉnh tới được từ ss trong đồ thị dư. Vì sao lát cắt (S,T)(S, T) có giá trị đúng bằng luồng?

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

Đề. Đồ 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 giữa 1 và nn; in số cạnh và các cạnh đó.

Lời giải. Đóng ít cạnh nhất để tách 1 khỏi nn là lát cắt nhỏ nhất với sức chứa 1 mỗi cạnh, và giá trị đó bằng luồng cực đại. Khi luồng đã cực đại, các đỉnh còn tới được từ 1 trên đồ thị dư là phía nguồn SS; cạnh có đúng một đầu trong SS là cạnh bị cắt. bfs, dfs như ví dụ trước.

Bước 1: cạnh vô hướng. Hai cung ec và ec + 1 đều có cap = 1, đặt liền nhau nên e ^ 1 vẫn là cung ngược của nhau. Đẩy 1 đơn vị theo một chiều thì chiều kia có cap = 2, đúng với việc trả lại luồng cũ rồi đi tiếp chiều ngược.

C++
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++);

Bước 2: tìm phía SS. Sau vòng Dinic gọi bfs(1, n) thêm một lần. Lúc này không tới được nn, nhưng lv[v] >= 0 vẫn đánh dấu đúng các đỉnh tới được từ 1.

Bước 3: in cạnh cắt. Cạnh có một đầu trong SS, một đầu ngoài. Mọi cạnh như vậy đều bão hòa theo chiều từ SS ra, nên số của chúng đúng bằng luồng.

C++
for (ll i = 1; i <= m; i++)
    if ((lv[ea[i]] >= 0) != (lv[eb[i]] >= 0)) cout << ea[i] << ' ' << eb[i] << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using 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], vis[N];
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

Luồng không quá mm nên Dinic chạy rất nhanh, cỡ O(EE)O(E\sqrt{E}).

Ví dụKhóa Các Bộ Thu

Đề. Mạng n≤100n \le 100 trạm, trạm 0 phát tín hiệu, gg trạm đặc biệt đang nhận. Mỗi thao tác tắt một liên kết hoặc tắt chức năng nhận của một trạm đặc biệt (tín hiệu vẫn đi qua được). Tìm số thao tác ít nhất để không trạm đặc biệt nào đang nhận còn tới được từ trạm 0.

Lời giải. Hai loại thao tác đều thành “cắt một cung sức chứa 1”. Thêm đỉnh đích T=nT = n (trạm đánh số 0..n−10..n-1 nên không trùng); trạm đặc biệt nối tới TT bằng cung sức chứa 1, cắt cung đó là tắt chức năng nhận mà tín hiệu vẫn đi qua trạm được. Mỗi liên kết là cặp cung sức chứa 1 cả hai chiều như ví dụ trước. Không trạm đặc biệt nào còn nhận tín hiệu nghĩa là không còn đường từ 0 tới TT, nên đáp án là lát cắt nhỏ nhất, bằng luồng cực đại. bfs, dfs như trên.

Bước 1: một hàm thêm cặp cung. Cung u→vu \to v sức chứa c1, cung ngược sức chứa c2. Trạm đặc biệt dùng (1, 0) vì tín hiệu chỉ đi vào TT; liên kết dùng (1, 1) vì vô hướng.

C++
void addPair(ll u, ll v, ll c1, ll c2) {
    to[ec] = v, cap[ec] = c1, g[u].push_back(ec++);
    to[ec] = u, cap[ec] = c2, g[v].push_back(ec++);
}
C++
addPair(p, n, 1, 0);
addPair(u, v, 1, 1);

Chỉ cần in giá trị luồng.

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

const ll N = 105, M = 20005, INF = 4e18;
ll n, gg, e, ec, to[M], cap[M], lv[N], it[N];
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 x : g[u])
            if (cap[x] > 0 && lv[to[x]] < 0) {
                lv[to[x]] = lv[u] + 1;
                q.push(to[x]);
            }
    }
    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 x = g[u][it[u]], v = to[x];
        if (cap[x] > 0 && lv[v] == lv[u] + 1) {
            ll d = dfs(v, t, min(f, cap[x]));
            if (d > 0) {
                cap[x] -= d;
                cap[x ^ 1] += d;
                return d;
            }
        }
    }
    return 0;
}

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

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

    cin >> n >> gg >> e;
    for (ll i = 1; i <= gg; i++) {
        ll p;
        cin >> p;
        addPair(p, n, 1, 0);
    }
    for (ll i = 1; i <= e; i++) {
        ll u, v;
        cin >> u >> v;
        addPair(u, v, 1, 1);
    }
    ll flow = 0;
    while (bfs(0, n)) {
        for (ll i = 0; i <= n; i++) it[i] = 0;
        ll f;
        while ((f = dfs(0, n, INF)) > 0) flow += f;
    }
    cout << flow << '\n';
}
5 2 4
3 4
0 1
1 3
0 2
2 4

Luồng không quá gg, mỗi pha O(E)O(E) cho đồ thị sức chứa đơn vị.

Ghép cặp hai phía

Đồ thị hai phía AA, BB: chọn nhiều cạnh nhất sao cho không hai cạnh chung đỉnh. Thêm nguồn nối tới mọi đỉnh của AA bằng cung sức chứa 1, mọi đỉnh của BB nối tới đích bằng cung sức chứa 1, các cạnh A→BA \to B sức chứa 1. Một luồng nguyên tương ứng đúng một cặp ghép: mỗi đỉnh nhận hoặc gửi nhiều nhất 1 đơn vị. Cặp ghép đọc ra từ các cung A→BA \to B đã bão hòa. Dinic trên mạng này chạy trong O(EV)O(E \sqrt V), cùng độ phức tạp với Hopcroft-Karp (bài 64).

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

Đề. n,m≤500n, m \le 500 người ở hai nhóm, k≤1000k \le 1000 cặp hợp lệ. Chọn nhiều cặp nhất, mỗi người tối đa một cặp, in các cặp.

Lời giải. Mạng ghép cặp: nguồn nối tới mỗi người nhóm AA bằng cung sức chứa 1, mỗi người nhóm BB nối tới đích bằng cung sức chứa 1, mỗi cặp hợp lệ là cung A→BA \to B sức chứa 1. Luồng nguyên trên mạng này ứng đúng với một cặp ghép, vì mỗi người nhận hay gửi nhiều nhất 1 đơn vị. Đánh số: nguồn 0, nhóm AA là 1..n1..n, nhóm BB là n+1..n+mn + 1..n + m, đích n+m+1n + m + 1. bfs, dfs như trên.

Bước 1: nhớ cung của từng cặp. addEdge trả về chỉ số cung vừa thêm; eid[i] giữ cung của cặp thứ ii để đọc lại sau. Thứ tự thêm cung không ảnh hưởng kết quả.

C++
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;
}
C++
eid[i] = addEdge(ea[i], n + eb[i], 1);

Bước 2: đọc cặp ghép. Mọi sức chứa nguyên nên Dinic cho luồng nguyên: mỗi cung A→BA \to B mang 0 hoặc 1, và cap[eid[i]] == 0 là cặp được chọn. Cặp lặp trong input tạo hai cung song song; tối đa một cung được dùng, vì người ở nhóm AA chỉ nhận 1 đơn vị từ nguồn.

C++
for (ll i = 1; i <= k; i++)
    if (cap[eid[i]] == 0) cout << ea[i] << ' ' << eb[i] << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using 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

Dinic trên mạng ghép cặp chạy O(EV)O(E\sqrt{V}).

Chọn dự án bằng lát cắt

Có các phần tử với lợi nhuận dương hoặc chi phí, và ràng buộc kéo theo: chọn xx thì phải chọn yy. Tìm tập hợp lệ có tổng lớn nhất. Dựng mạng: nguồn nối tới mỗi phần tử có lợi p>0p > 0 bằng cung sức chứa pp; mỗi phần tử có chi phí cc nối tới đích bằng cung sức chứa cc; ràng buộc x⇒yx \Rightarrow y là cung x→yx \to y sức chứa vô hạn. Một lát cắt hữu hạn ứng với tập SS (phía nguồn) là tập được chọn, thỏa mọi ràng buộc vì không cung vô hạn nào bị cắt. Giá trị lát cắt là (lợi của phần tử không chọn) cộng (chi phí của phần tử được chọn), nên đáp án là tổng mọi lợi trừ lát cắt nhỏ nhất.

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

Đề. Đồ thị n≤1000n \le 1000 đỉnh có chi phí aia_i, m≤1000m \le 1000 cạnh có trọng số ww. Chọn một số cạnh (phải chọn cả hai đầu), giá trị là tổng trọng số cạnh trừ tổng chi phí đỉnh đã chọn. Tìm giá trị lớn nhất.

Lời giải. Bài chọn dự án: mỗi cạnh là một dự án lợi ww, chọn nó thì phải chọn hai đầu mút; mỗi đỉnh là một khoản chi phí aia_i. Dựng mạng: nguồn nối tới mỗi cạnh bằng cung sức chứa ww, mỗi đỉnh nối tới đích bằng cung sức chứa aia_i, mỗi cạnh nối tới hai đầu mút bằng cung sức chứa vô hạn. Lát cắt hữu hạn không cắt cung vô hạn nào, nên phía nguồn là một lựa chọn hợp lệ; giá trị lát cắt là lợi của các cạnh không chọn cộng chi phí các đỉnh được chọn. Đáp án là ∑w\sum w trừ lát cắt nhỏ nhất. Nút: nguồn 0, cạnh 1..m1..m, đỉnh m+1..m+nm + 1..m + n, đích m+n+1m + n + 1.

Bước 1: chi phí đỉnh.

C++
addEdge(m + i, V, a);

Bước 2: lợi và ràng buộc của cạnh. Cung từ nguồn mang lợi ww; hai cung INF tới hai đầu mút là ràng buộc “chọn cạnh thì phải chọn đỉnh”.

C++
sum += w;
addEdge(0, i, w);
addEdge(i, m + u, INF);
addEdge(i, m + v, INF);

INF = 4e18 lớn hơn hẳn mọi lát cắt hữu hạn (tổng lợi tới 101210^{12}), nên cung vô hạn không bao giờ bão hòa; dfs lấy min với sức chứa thật nên không tràn.

Bước 3: đáp án sum - flow: lấy tất cả lợi rồi trừ phần phải bỏ. Không chọn cạnh nào cho lát cắt bằng ∑w\sum w, nên đáp án không bao giờ âm.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using 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

Dinic trên n+m+2n + m + 2 đỉnh, 3m+n3m + n cung.

Khi đi thi

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

Tóm tắt

  • Đồ thị dư gồm cung xuôi c−fc - f và cung ngược ff; luồng cực đại khi không còn đường tăng luồng.
  • Dinic: BFS phân mức, DFS đẩy luồng chặn với con trỏ cung, lặp tới khi tt không tới được.
  • Luồng cực đại bằng lát cắt nhỏ nhất; phía nguồn là các đỉnh tới được trong đồ thị dư cuối.
  • Ghép cặp hai phía: nguồn → AA → BB → đích, sức chứa 1; cặp là cung bão hòa.
  • Chọn dự án: lợi nối từ nguồn, chi phí nối tới đích, kéo theo là cung vô hạn; đáp án tổng lợi trừ lát cắt.

Bài tập

1
Trạm Trung Chuyểncanuc80k4900Luồng cực đại trực tiếp trên mạng có hướng với nguồn 1 và đích nn.
LQDOJ
2
Mạng Cáp Hai Chiềucanuc80k4902Mạng vô hướng, mỗi cạnh là cặp cung cùng sức chứa hai chiều; nn tới 5000 nên Dinic với con trỏ cung.
LQDOJ
3
Biên Bản Dòng Chảycanuc80k4903Chạy luồng rồi in luồng trên từng cung gốc có luồng dương, bằng sức chứa ban đầu trừ sức chứa còn lại.
LQDOJ
4
Vành Đai Phòng Thủcanuc80k4904Lát cắt nhỏ nhất giữa hai trạm cho trước trên mạng có hướng.
LQDOJ
5
Bảng Đánh Dấucanuc80k4907Hàng nối nguồn với sức chứa số ô cần đánh dấu của hàng, cột nối đích theo số của cột, ô là cung hàng → cột sức chứa 1; có lời giải khi luồng bằng tổng.
LQDOJ
6
Phân Bổ Sau Một Chặngcanuc80k4908Mạng hai lớp: nguồn → thành phố ii sức chứa aia_i, thành phố ii → bản sao jj khi i=ji = j hoặc kề nhau, bản sao jj → đích sức chứa bjb_j; đủ khi luồng bằng cả hai tổng.
LQDOJ
7
Các Tuyến Không Trùng Cạnhcanuc80k4909Số đường không chung cạnh là luồng với sức chứa 1; in đường bằng cách đi theo các cung có luồng từ 1, đánh dấu đã dùng.
LQDOJ
8
Phía Nguồn Của Lát Cắtcanuc80k4911Sau luồng cực đại, in các đỉnh tới được từ nguồn trong đồ thị dư.
LQDOJ
9
Kho Quà Theo Nhómcanuc80k4912Nguồn → trẻ em (1) → đồ chơi (1) → nhóm (giới hạn nhóm) → đích; đồ chơi không thuộc nhóm nào nối thẳng tới đích.
LQDOJ
10
Kết Quả Không Trùngcanuc80k4913Ghép mỗi cặp số với một kết quả khác nhau trong ba phép tính: ghép cặp hai phía giữa cặp và các giá trị kết quả đã nén.
LQDOJ
11
Dọn Sạch Lưới Xucanuc80k4914Mỗi đồng xu nối hàng với cột; số lần ít nhất là phủ đỉnh nhỏ nhất của đồ thị hai phía, bằng ghép cực đại (định lý König), in phủ từ lát cắt.
LQDOJ
12
Lát Domino Tối Đacanuc80k4915Tô ô theo tính chẵn lẻ của i+ji + j, ghép ô đen với ô trắng kề; domino là cặp ghép, in theo hướng của cặp.
LQDOJ
13
Quân Cờ Trong Khungcanuc80k4916Nguồn → hàng → quân cờ (tách thành hai nút sức chứa 1) → cột → đích; mỗi quân cờ nối các hàng và cột trong khung của nó.
LQDOJ
14
Bàn Tròn Tổng Nguyên Tốcanuc80k4924Tổng hai số nguyên tố lớn hơn 2 thì một chẵn một lẻ: số chẵn và lẻ ở hai phía, mỗi đỉnh có đúng hai hàng xóm (sức chứa 2), rồi tách thành các vòng.
LQDOJ
15
Đặt Chốt Trên Mạngcanuc80k4932Lát cắt đỉnh nhỏ nhất: tách mỗi đỉnh thành vào và ra với sức chứa bằng chi phí, cạnh là cung vô hạn hai chiều; in các đỉnh có cung vào ra bị cắt.
LQDOJ

Đọc thêm