Có người và 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 : luồng cực đại chính là số việc giao được nhiều nhất, và Dinic tìm nó trong với đỉnh, 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: , , , , , , , 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 , , 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 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 , tất cả sức chứa bằng . Mỗi đỉnh nhóm A nhận nhiều nhất đơn vị từ nguồn, mỗi đỉnh nhóm B gửi nhiều nhất đơn vị tới đích, nên mỗi luồng mà mọi cung chở hoặc ứ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ở , 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.
Trên mạng mà mọi sức chứa bằng , Dinic chạy nhanh hơn hẳn cận chung : chỉ cần pha, nên cả thuật toán tốn (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 tốn , 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 đơn vị và mỗi đỉnh nhóm B chỉ gửi đi đơ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 pha, mỗi đường tăng luồng dài hơn cung, vì độ dài đường ngắn nhất tăng sau mỗi pha. Gọi là luồng cực đại, là luồng lúc đó; phần chênh tách được thành đường không chung đỉnh, mỗi đường hơn đỉnh, nên . Mỗi pha sau đó tăng luồng ít nhất , nên chỉ còn thêm không quá pha. Tổng cộng 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 trong bài Network Flow and Testing Graph Connectivity (1975).
Ví dụGhép Đôi Sân Khấu
Đề. Có bạn ở nhóm A, bạn ở nhóm B, và 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 , nhóm A là các đỉnh tới , nhóm B là các đỉnh tới , đích là đỉnh . 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ở , 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 đơn vị từ nguồn.
#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
3
1 1
2 3
3 2
Độ phức tạp: trên mạng ghép cặp; với đỉnh và cung xuôi, cần cỡ 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 ở ô là một cạnh nối hàng với cột . 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 có ba đồng xu nằm trên đường chéo chính, ở các ô , , . 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?
Ba đồng xu nằm ở ba hàng khác nhau và ba cột khác nhau, nên mỗi thao tác lấy được nhiều nhất một đồng: cần ít nhất thao tác, và chọn ba hàng là đủ. Đường chéo không phải hàng hay cột, nên không lấy được trong một thao tác; hàng và cột bỏ sót đồng xu ở ô .
Ba cạnh , , cũng là một cặp ghép: mỗi cạnh của cặp ghép cần một đỉnh phủ riêng, nên phủ đỉnh không thể ít hơn cặp ghép.
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 là các đỉnh tới được từ trên đồ thị dư. Phủ đỉnh gồm các đỉnh nhóm A không thuộc và các đỉnh nhóm B thuộc .
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 từ nguồn; luồng cực đại vẫn bằng cặp ghép lớn nhất. Lát cắ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à với thuộc A nằm ngoài , và với thuộc B nằm trong ; tập đỉnh gồm các và đó 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.
là một phủ đỉnh. Xét một cạnh . Nếu không thuộc thì thuộc . Nếu thuộc thì cung vô hạn còn sức chứa dư, nên cũng tới được từ , tức thuộc và thuộc . Vậy mọi cạnh đều có một đầu thuộc . Kết hợp với chiều ngược lại (phủ đỉnh không ít hơn cặp ghép), 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 (), 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 , 2 x là chọn cột .
Lời giải. Hàng là nhóm A (đỉnh tới ), cột là nhóm B (đỉnh tới ), mỗi đồng xu ở ô là cung từ hàng tới cột 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 ) và cột có lv không âm (thuộc ) là các thao tác cần làm. Lưới có tới đồng xu, nên mạng có đỉnh và tới cung xuôi.
#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
...
2
1 1
1 2
Độ phức tạp: với và cung xuôi; cả chương trình chỉ cỡ bước.
IITách đỉnh và đường đi rời nhau
Mạng ở hình dưới có nút và liên kết một chiều. Ta muốn đi từ nút tới nút 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 , và không chung liên kết nào, và không có đường thứ tư vì nút chỉ có ba liên kết đi ra.
Cho mỗi liên kết sức chứa , 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ị ta luôn tách được đườ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út và 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 tới nút 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 ; luồng cực đại là số đường. Để in, ta đi lần từ nút . Mỗi bước ở nút , tìm một cung xuôi của đ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 . Đánh dấu cung đó, đi sang đầu kia, và dừng khi tới nút . Theo lý luận ở trên, mỗi lần đi đều tới được .
#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
3
3
1 2 7
4
1 3 5 7
4
1 4 6 7
Độ phức tạp: Dinic trên mạng sức chứa tốn ; in các đường tốn vì mỗi bước duyệt danh sách cung của một nút; với , 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 đơn vị, ta tách đỉnh: thay đỉnh bằng hai đỉnh và nối bằng cung sức chứa ; mọi cung đi vào đổi thành đi vào , mọi cung đi ra khỏi đổi thành đi ra từ . Đơn vị luồng nào đi qua cũng phải qua cung , nên tổng luồng qua không vượt .
Với cho mọi đỉnh trung gian, luồng cực đại là số đường nhiều nhất từ tới 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 khỏi là lát cắt nhỏ nhất trên mạng đã tách đỉnh, trong đó cung mang chi phí của 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 thành cạnh từ bản ra của tới bản vào của . 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 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 và cần công cụ C1; dự án D2 lãi và cần cả hai công cụ C1, C2. Mua C1 tốn , mua C2 tốn . 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 ; chỉ làm D1 được ; chỉ làm D2 được ; làm cả hai được . Cách tốt nhất là chỉ làm D1, lãi .
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 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 bằng cung có sức chứa bằng chi phí của công cụ;
- dự án cần công cụ thì thêm cung sức chứa vô hạn, trong code là .
Với một lát cắ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 . Đá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à , lát cắt nhỏ nhất có và sức chứa , nên lãi lớn nhất là , đúng như khi thử hết.
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 thuộc và cần công cụ , thì cũng thuộc : cách chọn ứng với 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 gồm và các dự án, công cụ được chọn.
Các cung bị cắt, tức đi từ sang , gồm: cung từ tới các dự án không được chọn, và cung từ các công cụ được chọn tới . 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 tới công cụ mà cần có sức chứa vô hạn.
Vì sao không dùng sức chứa cho cung này?
Lát cắt nhỏ nhất có thể cắt bất kỳ cung nào có sức chứa hữu hạn. Nếu cung có sức chứa , lát cắt có thể đặt vào và vào , chỉ tốn thêm , nghĩa là chọn dự án mà không mua công cụ , vi phạm ràng buộc. Sức chứa vô hạn khiến lát cắt nhỏ nhất không bao giờ cắt cung này.
Các lựa chọn khác không đúng: luồng cực đại luôn không vượt quá tổng lãi vì mọi luồng đều xuất phát từ các cung đi ra khỏi ; Dinic đúng với mọi sức chứa không âm; còn sức chứa không ảnh hưởng tới việc cung được dùng trong pha nào.
Ví dụChọn Dự Án Trên Đồ Thị
Đề. Cho đồ thị đỉnh, đỉnh thứ có chi phí , và cạnh, mỗi cạnh có trọng số . 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 , mỗi đỉnh là một công cụ với chi phí , và mỗi cạnh cần hai đầu của nó. Đánh số: nguồn là đỉnh , các cạnh là đỉnh tới , các đỉnh của đồ thị là đỉnh tới , đích là đỉnh . Đá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 , lớn hơn hẳn mọi lát cắt hữu hạn (tổng trọng số không quá ), 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à , nên đáp án không bao giờ âm.
#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
3
Độ phức tạp: Dinic trên đỉnh và cung xuôi; cận 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 chở ít nhất và nhiều nhất , 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 , rồi tìm phần còn lại. Chở sẵn trên cung làm thừa đơn vị và thiếu đơn vị; phần còn lại của cung chỉ được chở thêm tối đa . Gọi là tổng phần bắt buộc đi vào trừ tổng phần bắt buộc đi ra khỏi . Đỉnh có đang thừa và phải đẩy phần thừa đi, đỉnh có đang thiếu và phải nhận thêm. Ta thêm một nguồn phụ nối tới mỗi đỉnh thừa bằng cung sức chứa , và mỗi đỉnh thiếu nối tới đích phụ bằng cung sức chứa . Có luồng khả thi khi và chỉ khi luồng cực đại từ tới làm đầy mọi cung ra khỏi ; khi đó luồng trên cung là cộng phần chạy trên cung sức chứa .
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á cung; cùng cỡ với một bài luồng cực đại thường.
Khi mạng có nguồn và đích thật, chỉ và được lệch vào ra. Ta thêm cung 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 là giá trị của một luồng khả thi từ tới . Muốn luồng lớn nhất, ta xóa cung (đặt sức chứa của nó và cung ngược bằng ) rồi chạy tiếp luồng cực đại từ tới 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ừ về 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 () 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 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à . 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 bệ thì đã có gần cặp, cả bảng có thể tới cỡ 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 , còn hai bệ S và T là cạnh vô hướng sức chứa vô hạn. Đồ thị chỉ có đỉnh và không quá 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.
#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.
1
Độ phức tạp: Dinic trên đỉnh và không quá 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ó thẻ (), thẻ thứ mang giá trị (). 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 , nên tổng hai thẻ ít nhất ; số nguyên tố lớn hơn đề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 ; mỗi thẻ chẵn nối tới đích bằng cung sức chứa ; thẻ lẻ nối tới thẻ chẵn bằng cung sức chứa khi 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 ; nếu không thì in Impossible. Kiểm tra số nguyên tố tới 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ở 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 , 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.
#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
1
4 1 2 3 4
Độ phức tạp: sàng với ; mạng có đỉnh và không quá cung xuôi, Dinic chạy rất nhanh; ghép vòng tố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 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 ; Dinic chạy trong .
- Đị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 và đỉnh nhóm B trong .
- Số đường không chung cạnh nhiều nhất bằng luồng cực đại với sức chứa ; 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
- Ứng dụng của luồng trên mạngVNOI Wiki
- Kuhn's Algorithm for Maximum Bipartite Matchingcp-algorithms
- Flows with demandscp-algorithms
- Minimum CutUSACO Guide
- 上下界网络流OI Wiki