# 二分图与网络流

## 二分图

### 二分图判定

#### 染色法

bool dfs(int x, int color) {
v[x] = color;
for (unsigned int i = 0; i < e[x].size(); i++) {
int y = e[x][i];
if (v[y] == color) return 0;
if (!v[y] && !dfs(y, 3 - color)) return 0;
}
return 1;
}

inline bool pd() {
for (int i = 1; i <= n; i++)
if (!v[i] && !dfs(i, 1)) return 0;
return 1;
}

#### 【例题】P1525 关押罪犯

#include <bits/stdc++.h>
using namespace std;
const int N = 4e4 + 6, M = 1e5 + 6;
int n, m, fa[N];
struct P {
int a, b, c;
inline bool operator < (const P o) const {
return c > o.c;
}
} p[M];

int get(int x) {
if (fa[x] == x) return x;
return fa[x] = get(fa[x]);
}

int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++)
scanf("%d %d %d", &p[i].a, &p[i].b, &p[i].c);
sort(p + 1, p + m + 1);
for (int i = 1; i <= n << 1; i++) fa[i] = i;
for (int i = 1; i <= m; i++) {
int x = get(p[i].a), y = get(p[i].b);
int xx = get(p[i].a + n), yy = get(p[i].b + n);
if (x == y) {
cout << p[i].c << endl;
return 0;
}
fa[x] = yy;
fa[y] = xx;
}
puts("0");
return 0;
}

#include <bits/stdc++.h>
#define pii pair<int, int>
using namespace std;
const int N = 2e4 + 6, M = 2e5 + 6;
struct P {
int x, y, z;
bool operator < (const P w) const {
return z > w.z;
}
} p[M];
int n, m, v[N];
vector<pii> e[N];

bool dfs(int x, int color) {
v[x] = color;
for (unsigned int i = 0; i < e[x].size(); i++) {
int y = e[x][i].first;
if (v[y] == color) return 0;
if (!v[y] && !dfs(y, 3 - color)) return 0;
}
return 1;
}

inline bool pd(int now) {
for (int i = 1; i <= n; i++) e[i].clear();
for (int i = 1; i <= m; i++) {
if (p[i].z <= now) break;
e[p[i].x].push_back(make_pair(p[i].y, p[i].z));
e[p[i].y].push_back(make_pair(p[i].x, p[i].z));
}
memset(v, 0, sizeof(v));
for (int i = 1; i <= n; i++)
if (!v[i] && !dfs(i, 1)) return 0;
return 1;
}

int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++)
scanf("%d %d %d", &p[i].x, &p[i].y, &p[i].z);
sort(p + 1, p + m + 1);
int l = 0, r = p[1].z;
while (l < r) {
int mid = (l + r) >> 1;
if (pd(mid)) r = mid;
else l = mid + 1;
}
cout << l << endl;
return 0;
}

### 二分图最大匹配

#### 增广路的性质

1. 长度为奇数
2. 奇数边是非匹配边，偶数边是匹配边。
3. 如果把路径上所有边的状态（是否为匹配边）取反，那么得到的新的边集 $S'$ 仍然是一组匹配，并且匹配的边数增加了 $1$

### 匈牙利算法（增广路算法）

#### 主要过程

1. $S$ 为空集，即所有边都是非匹配边。
2. 寻找增广路 $path$ ，把 $path$ 上所有边的匹配状态取反，得到一个更大的匹配 $S'$
3. 重复第 $2$ 步，直至图中不存在增广路。

#### 寻找增广路

$y$$x$ 匹配需满足下面两个条件之一：

1. $y$ 是非匹配点。
2. $y$ 已与 $x'$ 匹配，但从 $x'$ 出发能找到另一个 $y'$ 与之匹配。

#### 时间复杂度

$O(nm)$

#### 【模板】P3386 【模板】二分图匹配

#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 6;
int n, m, t, f[N], ans;
vector<int> e[N];
bitset<N> v;

bool dfs(int x) {
for (unsigned int i = 0; i < e[x].size(); i++) {
int y = e[x][i];
if (v[y]) continue;
v[y] = 1;
if (!f[y] || dfs(f[y])) {
f[y] = x;
return 1;
}
}
return 0;
}

int main() {
cin >> n >> m >> t;
while (t--) {
int x, y;
scanf("%d %d", &x, &y);
if (x > n || y > m) continue;
e[x].push_back(y + n);
e[y+n].push_back(x);
}
for (int i = 1; i <= n; i++) {
v.reset();
ans += dfs(i);
}
cout << ans << endl;
return 0;
}

#### 二分图匹配模型的两个要素

1. 节点能分成两个集合，每个集合内部有 $0$ 条边。简称 $0$ 要素。
2. 每个节点只能与 $1$ 条匹配边相连。简称 $1$ 要素。

#### 两个要素在本题中的体现

$1$ 要素：每个格子只能被一张骨牌覆盖，一张骨牌覆盖 $2$ 个相邻的格子。

$0$ 要素：把棋盘黑白相间染色，相同颜色的格子不可能被同一骨牌覆盖。

#include <bits/stdc++.h>
using namespace std;
const int N = 106;
const int dx[4] = {0,0,1,-1};
const int dy[4] = {1,-1,0,0};
int n, m, ans, f[N*N];
bool b[N][N], v[N*N];
vector<int> e[N*N];

bool dfs(int x) {
for (unsigned int i = 0; i < e[x].size(); i++) {
int y = e[x][i];
if (v[y]) continue;
v[y] = 1;
if (f[y] == -1 || dfs(f[y])) {
f[y] = x;
return 1;
}
}
return 0;
}

int main() {
cin >> n >> m;
while (m--) {
int x, y;
scanf("%d %d", &x, &y);
b[x-1][y-1] = 1;
}
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (!b[i][j])
for (int k = 0; k < 4; k++) {
int x = i + dx[k], y = j + dy[k];
if (x >= 0 && x < n && y >= 0 && y < n && !b[x][y]) {
e[i*n+j].push_back(x * n + y);
e[x*n+y].push_back(i * n + j);
}
}
memset(f, -1, sizeof(f));
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) {
if ((i ^ j) & 1) continue;
memset(v, 0, sizeof(v));
ans += dfs(i * n + j);
}
cout << ans << endl;
return 0;
}

#### 【例题】U64965 車的放置

$1$ 要素：每行、每列只能放 $1$ 个車。

$0$ 要素：行节点之间没有边，列节点之间也没有边。

### 多重匹配

#### 方法

1. 拆点求解二分图最大匹配。
2. 网络流。

### 二分图带权匹配

1. KM算法。
2. 费用流。

#### KM算法的优劣

• 优：程序实现简单，稠密图效率一般高于费用流，时间复杂度 $O(n^3)$
• 劣：只能求解带权最大匹配一定是完备匹配的问题。

### 二分图最小点覆盖

#### $2$ 要素在本题中的体现

$A,B$$m$ 种模式分别作为 $m$ 个左部点和右部点，每个任务作为边连接左部 $a_i$ 节点和右部 $b_i$ 节点。

#include <bits/stdc++.h>
using namespace std;
const int N = 106;
int n, m, k, f[N], ans;
bool v[N];
vector<int> e[N];

bool dfs(int x) {
for (unsigned int i = 0; i < e[x].size(); i++) {
int y = e[x][i];
if (v[y]) continue;
v[y] = 1;
if (!f[y] || dfs(f[y])) {
f[y] = x;
return 1;
}
}
return 0;
}

inline void Machine_Schedule() {
cin >> m >> k;
for (int i = 1; i <= n; i++) e[i].clear();
for (int i = 0; i < k; i++) {
int x, y;
scanf("%d %d %d", &i, &x, &y);
e[x].push_back(y);
}
memset(f, 0, sizeof(f));
ans = 0;
for (int i = 1; i <= n; i++) {
memset(v, 0, sizeof(v));
ans += dfs(i);
}
cout << ans << endl;
}

int main() {
while (cin >> n && n) Machine_Schedule();
return 0;
}

#### 【例题】POJ2226 Muddy Fields

$2$ 要素：每块泥地要么被横着的木板盖住，要么被竖着的木板盖住。

### 二分图最大独立集

#### 定理

1. 无向图 $G$ 的最大团 $=$ 补图 $G'$ 的最大独立集。
2. 对于一般无向图，最大团、最大独立集是 NPC 问题。
3. $G$ 是有 $n$ 个节点的二分图， $G$ 的最大独立集大小 $=$ $n-$ 最小点覆盖数 $=$ $n-$ 最大匹配数。

#### 【例题】P3355 骑士共存问题

#include <bits/stdc++.h>
using namespace std;
const int N = 206;
const int dx[8] = {-2,-2,-1,-1,1,1,2,2};
const int dy[8] = {-1,1,-2,2,-2,2,-1,1};
int n, m, ans, f[N][N][2];
bool a[N][N], v[N][N];

inline bool dfs(int x, int y) {
for (int i = 0; i < 8; i++) {
int nx = x + dx[i], ny = y + dy[i];
if (nx < 1 || ny < 1 || nx > n || ny > n) continue;
if (a[nx][ny] || v[nx][ny]) continue;
v[nx][ny] = 1;
if (!f[nx][ny][0] || dfs(f[nx][ny][0], f[nx][ny][1])) {
f[nx][ny][0] = x;
f[nx][ny][1] = y;
return 1;
}
}
return 0;
}

int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d %d", &x, &y);
a[x][y] = 1;
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
if (((i ^ j) & 1) || a[i][j]) continue;
memset(v, 0, sizeof(v));
ans += dfs(i, j);
}
cout << n * n - m - ans << endl;
return 0;
}

#### 【例题】P2423 [HEOI2012]朋友圈

A国最多取两个点，可以枚举这两个点 $x,y$ ；数据给定了A和B国之间有边的情况，删除与 $x$$y$ 之间无边的B国点；然后在B国剩余的点上求二分图的最大独立集。

## 网络流

$f(x,y)$ 是定义在节点二元组 $(x \in V,y \in V)$ 上的实数函数，且满足：

1. 容量限制$f(x,y) \leq c(x,y)$
2. 斜对称$f(x,y)=-f(y,x)$
3. 流量守恒$\forall x \neq S,x \neq T$$\sum_{(u,x)\ \in E} f(u,x) = \sum_{(x,v)\ \in E} f(x,v)$

$f$ 称为网络的函数。对于 $(x,y) \in E$$f(x,y)$ 称为边的流量$c(x,y) - f(x,y)$ 称为边的剩余流量

$\sum_{(S,v)\ \in E} f(S,v)$ 称为整个网络的流量（ $S$ 为源点）。

### EK（Edmond—Karp）增广路算法

#### 时间复杂度

$O(nm^2)$ ，实际运用中远远达不到，能够处理 $10^3$ ~ $10^4$ 规模的网络。

#### 【模板】P3376 【模板】网络最大流

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 6, M = 2e5 + 6, inf = 1e9;
int n, m, s, t, ans, now[N], pre[N];
int Head[N], Edge[M], Leng[M], Next[M], tot = 1;
bitset<N> v;

inline void add(int x, int y, int z) {
Edge[++tot] = y;
Leng[tot] = z;
}

inline bool bfs() {
v.reset();
queue<int> q;
q.push(s);
v[s] = 1;
now[s] = inf;
while (q.size()) {
int x = q.front();
q.pop();
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (v[y] || !z) continue;
now[y] = min(now[x], z);
pre[y] = i;
if (y == t) return 1;
q.push(y);
v[y] = 1;
}
}
return 0;
}

inline void upd() {
ans += now[t];
int x = t;
while (x != s) {
int i = pre[x];
Leng[i] -= now[t];
Leng[i^1] += now[t];
x = Edge[i^1];
}
}

int main() {
cin >> n >> m >> s >> t;
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
}
while (bfs()) upd();
cout << ans << endl;
return 0;
}

### Dinic算法

#### 算法步骤

1. 在残量网络上 BFS 求出节点层次，构造分层图。
2. 在分层图上 DFS 寻找增广路，在回溯时实时更新剩余容量。另外，每个点可以流向多条出边，同时还加入若干剪枝。

#### 时间复杂度

$O(n^2m)$ ，实际运用中远远达不到，能够处理 $10^4$ ~ $10^5$ 规模的网络。

#### 【模板】P3376 【模板】网络最大流

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 6, M = 2e5 + 6, inf = 1e9;
int n, m, s, t, ans, d[N];
int Head[N], Edge[M], Leng[M], Next[M], tot = 1;

inline void add(int x, int y, int z) {
Edge[++tot] = y;
Leng[tot] = z;
}

inline bool bfs() {
memset(d, 0, sizeof(d));
queue<int> q;
q.push(s);
d[s] = 1;
while (q.size()) {
int x = q.front();
q.pop();
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (d[y] || !z) continue;
q.push(y);
d[y] = d[x] + 1;
if (y == t) return 1;
}
}
return 0;
}

int dinic(int x, int flow) {
if (x == t) return flow;
int rest = flow;
for (int i = Head[x]; i && rest; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (d[y] != d[x] + 1 || !z) continue;
int k = dinic(y, min(rest, z));
if (!k) d[y] = 0;
else {
Leng[i] -= k;
Leng[i^1] += k;
rest -= k;
}
}
return flow - rest;
}

int main() {
cin >> n >> m >> s >> t;
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
}
int now = 0;
while (bfs())
while ((now = dinic(s, inf)))
ans += now;
cout << ans << endl;
return 0;
}

#### 最大流解决二分图多重匹配

Dinic 算法求解二分图最大匹配的时间复杂度为 $O(m \sqrt n)$ ，比匈牙利算法快，实际表现则更快。

### 二分图最大匹配的必须边与可行边

#### 最大匹配是完备匹配

• 必须边： $(x,y)$ 是当前二分图的匹配边 && $x,y$ 在新的有向图中属于不同的强连通分量。
• 可行边： $(x,y)$ 是当前二分图的匹配边 || $x,y$ 在新的有向图中属于同一个强连通分量。

#### 最大匹配不一定是完备匹配

• 必须边： $(x,y)$ 的流量为 $1$ && 在残量网络上属于不同的强连通分量。
• 可行边： $(x,y)$ 的流量为 $1$ || 在残量网络上属于同一个强连通分量。

### 最小割

#### 点边转化

#include <bits/stdc++.h>
using namespace std;
const int N = 56, M = 2e4 + 6, inf = 0x3f3f3f3f;
int n, m, s, t;
int a[N*N], b[N*N], d[N<<1];
int Head[N<<1], Edge[M], Leng[M], Next[M], tot;

inline void add(int x, int y, int z) {
Edge[++tot] = y;
Leng[tot] = z;
Edge[++tot] = x;
Leng[tot] = 0;
}

inline bool bfs() {
memset(d, 0, sizeof(d));
queue<int> q;
q.push(s);
d[s] = 1;
while (q.size()) {
int x = q.front();
q.pop();
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (z && !d[y]) {
q.push(y);
d[y] = d[x] + 1;
if (y == t) return 1;
}
}
}
return 0;
}

inline int dinic(int x, int f) {
if (x == t) return f;
int rest = f;
for (int i = Head[x]; i && rest; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (z && d[y] == d[x] + 1) {
int k = dinic(y, min(rest, z));
if (!k) d[y] = 0;
Leng[i] -= k;
Leng[i^1] += k;
rest -= k;
}
}
return f - rest;
}

inline void Cable_TV_Network() {
for (int i = 0; i < m; i++) {
char str[20];
scanf("%s", str);
a[i] = b[i] = 0;
int j;
for (j = 1; str[j] != ','; j++) a[i] = a[i] * 10 + str[j] - '0';
for (j++; str[j] != ')'; j++) b[i] = b[i] * 10 + str[j] - '0';
}
int ans = inf;
for (s = 0; s < n; s++)
for (t = 0; t < n; t++)
if (s != t) {
tot = 1;
int maxf = 0;
for (int i = 0; i < n; i++)
if (i == s || i == t) add(i, i + n, inf);
else add(i, i + n, 1);
for (int i = 0; i < m; i++) {
}
while (bfs()) {
int num;
while ((num = dinic(s, inf))) maxf += num;
}
ans = min(ans, maxf);
}
if (n <= 1 || ans == inf) ans = n;
cout << ans << endl;
}

int main() {
while (cin >> n >> m) Cable_TV_Network();
return 0;
}

#### 【例题】P4126 [AHOI2009]最小割

• 可行边：在残量网络中不存在 $x$$y$ 的路径（强连通分量）；
• 必须边：在残量网络中 $S$ 能到 $x$ && $y$ 能到 $T$
#include <bits/stdc++.h>
using namespace std;
const int N = 4e3 + 6, M = 6e4 + 6, inf = 1e9;
int n, m, s, t, d[N], f[N];
int Head[N], Edge[M<<1], Leng[M<<1], Next[M<<1], tot = 1;
struct E {
int x, y, z;
} e[M<<1];
int dfn[N], low[N], num, st[N], top, ins[N], c[N], cnt;

inline void add(int x, int y, int z) {
Edge[++tot] = y;
Leng[tot] = z;
}

inline bool bfs() {
memset(d, 0, sizeof(d));
queue<int> q;
d[s] = 1;
q.push(s);
while (q.size()) {
int x = q.front();
q.pop();
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (d[y] || !z) continue;
d[y] = d[x] + 1;
q.push(y);
if (y == t) return 1;
}
}
return 0;
}

int dinic(int x, int flow) {
if (x == t) return flow;
int rest = flow;
for (int i = Head[x]; i && rest; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (d[y] != d[x] + 1 || !z) continue;
int k = dinic(y, min(z, rest));
if (!k) d[y] = 0;
else {
Leng[i] -= k;
Leng[i^1] += k;
rest -= k;
}
}
return flow - rest;
}

void dfs(int x, int k) {
f[x] = k;
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i^(k-1)];
if (f[y] || !z) continue;
dfs(y, k);
}
}

void tarjan(int x) {
dfn[x] = low[x] = ++num;
st[++top] = x;
ins[x] = 1;
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (!z) continue;
if (!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
} else if (ins[y])
low[x] = min(low[x], dfn[y]);
}
if (dfn[x] == low[x]) {
++cnt;
int y;
do {
y = st[top--];
ins[y] = 0;
c[y] = cnt;
} while (x != y);
}
}

int main() {
cin >> n >> m >> s >> t;
for (int i = 1; i <= m; i++) {
scanf("%d %d %d", &e[i].x, &e[i].y, &e[i].z);
}
while (bfs())
while (dinic(s, inf));
dfs(s, 1);
dfs(t, 2);
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i);
for (int i = 1; i <= m; i++) {
int k = i << 1;
if (Leng[k]) puts("0 0");
else printf("%d %d\n", c[e[i].x] != c[e[i].y], f[e[i].x] == 1 && f[e[i].y] == 2);
}
return 0;
}

#### 对偶图

• 平面图被划分出的每一个区域当作对偶图的一个点；
• 平面图中的每一条边两边的区域对应的点用边相连，特别地，若两边为同一区域则加一条回边（自环）。

#### 定理

#include <bits/stdc++.h>
#define pii pair<int, int>
#define X first
#define Y second
#define mp make_pair
#define ui unsigned int
using namespace std;
const int N = 2e6 + 6;
int n, m, s, t, d[N];
vector<pii> e[N];
priority_queue<pii> q;
bitset<N> v;

inline void add(int x, int y, int z) {
e[x].push_back(mp(y, z));
}

inline int get(int i, int j, int k) {
return 2 * (m - 1) * (i - 1) + 2 * (j - 1) + k;
}

inline void ins(int x, int y) {
int z;
scanf("%d", &z);
}

inline void dijkstra() {
memset(d, 0x3f, sizeof(d));
d[s] = 0;
q.push(mp(0, s));
while (q.size()) {
int x = q.top().Y;
if (x == t) return;
q.pop();
if (v[x]) continue;
v[x] = 1;
for (ui i = 0; i < e[x].size(); i++) {
int y = e[x][i].X, z = e[x][i].Y;
if (d[y] > d[x] + z) {
d[y] = d[x] + z;
q.push(mp(-d[y], y));
}
}
}
}

int main() {
cin >> n >> m;
t = 2 * (n - 1) * (m - 1) + 1;
for (int j = 1; j < m; j++) ins(get(1, j, 2), t);
for (int i = 2; i < n; i++)
for (int j = 1; j < m; j++)
ins(get(i - 1, j, 1), get(i, j, 2));
for (int j = 1; j < m; j++) ins(get(n - 1, j, 1), s);
for (int i = 1; i < n; i++) {
ins(get(i, 1, 1), s);
for (int j = 2; j < m; j++)
ins(get(i, j - 1, 2), get(i, j, 1));
ins(get(i, m - 1, 2), t);
}
for (int i = 1; i < n; i++)
for (int j = 1; j < m; j++)
ins(get(i, j, 1), get(i, j, 2));
dijkstra();
cout << d[t] << endl;
return 0;
}

### 费用流

#### 【模板】P3381 【模板】最小费用最大流

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 6, M = 2e5 + 6, inf = 0x3f3f3f3f;
int n, m, s, t, maxflow, ans, d[N], now[N], pre[N];
int Head[N], Edge[M], Leng[M], Cost[M], Next[M], tot = 1;
bitset<N> v;

inline void add(int x, int y, int z, int w) {
Edge[++tot] = y;
Leng[tot] = z;
Cost[tot] = w;
}

inline bool spfa() {
v.reset();
memset(d, 0x3f, sizeof(d));
queue<int> q;
q.push(s);
v[s] = 1;
d[s] = 0;
now[s] = inf;
while (q.size()) {
int x = q.front();
q.pop();
v[x] = 0;
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i], w = Cost[i];
if (!z || d[y] <= d[x] + w) continue;
d[y] = d[x] + w;
now[y] = min(now[x], z);
pre[y] = i;
if (!v[y]) {
q.push(y);
v[y] = 1;
}
}
}
return d[t] != inf;
}

inline void upd() {
maxflow += now[t];
ans += d[t] * now[t];
int x = t;
while (x != s) {
int i = pre[x];
Leng[i] -= now[t];
Leng[i^1] += now[t];
x = Edge[i^1];
}
}

int main() {
cin >> n >> m >> s >> t;
for (int i = 1; i <= m; i++) {
int x, y, z, w;
scanf("%d %d %d %d", &x, &y, &z, &w);
}
while (spfa()) upd();
cout << maxflow << " " << ans << endl;
return 0;
}

#### 【例题】P2045 方格取数加强版

1. 点边转化：把每个格子 $(i,j)$ 拆成一个入点一个出点。
2. 从每个入点向对应的出点连两条有向边：一条容量为 $1$ ，费用为格子 $(i,j)$ 中的数；另一条容量为 $k-1$ ，费用为 $0$
3. $(i,j)$ 的出点到 $(i,j+1)$$(i+1,j)$ 的入点连有向边，容量为 $k$ ，费用为 $0$
4. $(1,1)$ 的入点为源点， $(n,n)$ 的出点为汇点，求最大费用最大流。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 6, M = 2e5 + 6;
const int inf = 0x3f3f3f3f, _inf = 0xcfcfcfcf;
int Head[N], Edge[M], Leng[M], Cost[M], Next[M], tot = 1;
int d[N], f[N], p[N];
bool v[N];
int n, k, s = 1, t, ans;

inline void add(int x, int y, int z, int c) {
Edge[++tot] = y;
Leng[tot] = z;
Cost[tot] = c;
Edge[++tot] = x;
Leng[tot] = 0;
Cost[tot] = -c;
}

inline int num(int i, int j, int k) {
return (i - 1) * n + j + k * n * n;
}

inline bool spfa() {
queue<int> q;
memset(d, 0xcf, sizeof(d));
memset(v, 0, sizeof(v));
q.push(s);
d[s] = 0;
v[s] = 1;
f[s] = inf;
while (q.size()) {
int x = q.front();
v[x] = 0;
q.pop();
for (int i = Head[x]; i; i = Next[i]) {
if (!Leng[i]) continue;
int y = Edge[i];
if (d[y] < d[x] + Cost[i]) {
d[y] = d[x] + Cost[i];
f[y] = min(f[x], Leng[i]);
p[y] = i;
if (!v[y]) {
q.push(y);
v[y] = 1;
}
}
}
}
return d[t] != _inf;
}

void upd() {
int x = t;
while (x != s) {
int i = p[x];
Leng[i] -= f[t];
Leng[i^1] += f[t];
x = Edge[i^1];
}
ans += d[t] * f[t];
}

int main() {
cin >> n >> k;
t = 2 * n * n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
int c;
scanf("%d", &c);
add(num(i, j, 0), num(i, j, 1), 1, c);
add(num(i, j, 0), num(i, j, 1), k - 1, 0);
if (j < n) add(num(i, j, 1), num(i, j + 1, 0), k, 0);
if (i < n) add(num(i, j, 1), num(i + 1, j, 0), k, 0);
}
while (spfa()) upd();
cout << ans << endl;
return 0;
}

#### 费用提前计算

$i$ 位车主向第 $j$ 位维修人员拆成的第 $k$ 个点连边，容量为 $1$ ，费用为 $k \times t_{i,j}$

#### 【例题】P2050 [NOI2012]美食节

P2053 [SCOI2007]修车 的建图方式，但是硬求最小费用最大流只能拿到 $60$ 分。

#### 动态开点

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 6, M = 4e7 + 6, inf = 0x3f3f3f3f;
int n, m, s, t, ans, d[N], pre[N], now[N], num, p[N];
int Head[N], Edge[M], Leng[M], Cost[M], Next[M], tot = 1;
bitset<N> v;
int a[46][106];

inline void add(int x, int y, int z, int w) {
Edge[++tot] = y;
Leng[tot] = z;
Cost[tot] = w;
}

inline bool spfa() {
v.reset();
memset(d, 0x3f, sizeof(d));
queue<int> q;
q.push(s);
v[s] = 1;
d[s] = 0;
now[s] = inf;
while (q.size()) {
int x = q.front();
q.pop();
v[x] = 0;
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i], w = Cost[i];
if (!z || d[y] <= d[x] + w) continue;
d[y] = d[x] + w;
now[y] = min(now[x], z);
pre[y] = i;
if (!v[y]) {
q.push(y);
v[y] = 1;
}
}
}
return d[t] != inf;
}

inline void upd() {
ans += d[t] * now[t];
int x = t;
while (x != s) {
int i = pre[x];
Leng[i] -= now[t];
Leng[i^1] += now[t];
x = Edge[i^1];
}
x = Edge[pre[t]^1];
p[++num] = p[x];
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], w = Cost[i^1];
if (y == t) continue;
w += a[y][p[x]];
}
}

int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
}
num = t = n + m + 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
int x;
scanf("%d", &x);
a[i][j] = x;
add(i, n + j, 1, x);
add(n + j, i, 0, -x);
}
for (int i = 1; i <= m; i++) {
add(n + i, t, 1, 0);
add(t, n + i, 0, 0);
p[n+i] = i;
}
while (spfa()) upd();
cout << ans << endl;
return 0;
}

### 上下界网络流

#### 无源汇上下界可行流

$inB[u]=\sum B(i,u)$$outB[u]=\sum B(u,i)$$d[u]=inB[u]-outB[u]$

#### 有源汇上下界可行流

$T$$S$ 连一条下界为 $0$ ，上界为 $+inf$ 的边，把汇流入的流量转移给源流出的流量，转化为无源汇的网络，然后求解无源汇上下界可行流

#### 有源汇上下界最大流

1. 二分答案 $ans$ ，从 $T$$S$ 连一条下界为 $ans$ ，上界为 $+inf$ 的边，转化为无源汇网络。找到最大的 $ans$ ，使得该无源汇上下界网络有可行流。
2. $T$$S$ 连一条下界为 $0$ ，上界为 $+inf$ 的边，转化为无源汇网络。按照无源汇上下界可行流的做法求一次无源汇上下界超级源到超级汇的最大流。回到原网络，在上一步的残量网络基础上，求一次 $S$$T$ 的最大流。

#### 有源汇上下界最小流

1. 二分答案 $ans$ ，从 $T$$S$ 连一条下界为 $0$ ，上界为 $ans$ 的边，转化为无源汇网络。找到最小的 $ans$ ，使得该无源汇上下界网络有可行流。
2. 类似有源汇上下界可行流的构图方法，但先不添加 $T$$S$ 的边，求一次超级源到超级汇的最大流。然后再添加一条从 $T$$S$ 下界为 $0$ ，上界为 $+inf$ 的边，在残量网络上再求一次超级源到超级汇的最大流。流经 $T$$S$ 的边的流量就是最小流的值。该算法的思想是在第一步中尽可能填充循环流，以减小最小流的代价。

#### 【例题】P4843 清理雪道

1. $(s,i,0,+inf)$
2. $(i,t,0,+inf)$
3. 对每条雪道，连边 $(i,j,1,+inf)$

#include <bits/stdc++.h>
using namespace std;
const int N = 106, M = 2e4 + 6, inf = 1e9;
int n, s, t, S, T, d[N], ans;
int Head[N], Edge[M], Leng[M], Next[M], tot = 1;

inline void add(int x, int y, int z) {
Edge[++tot] = y;
Leng[tot] = z;
Edge[++tot] = x;
Leng[tot] = 0;
}

inline void ins(int x, int y, int l, int r) {
d[x] -= l;
d[y] += l;
}

inline bool bfs() {
memset(d, 0, sizeof(d));
queue<int> q;
q.push(S);
d[S] = 1;
while (q.size()) {
int x = q.front();
q.pop();
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (d[y] || !z) continue;
q.push(y);
d[y] = d[x] + 1;
if (y == T) return 1;
}
}
return 0;
}

int dinic(int x, int flow) {
if (x == T) return flow;
int rest = flow;
for (int i = Head[x]; i && rest; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (d[y] != d[x] + 1 || !z) continue;
int k = dinic(y, min(rest, z));
if (!k) d[y] = 0;
else {
Leng[i] -= k;
Leng[i^1] += k;
rest -= k;
}
}
return flow - rest;
}

int main() {
cin >> n;
s = n + 1, t = n + 2, S = n + 3, T = n + 4;
for (int i = 1; i <= n; i++) {
ins(s, i, 0, inf);
ins(i, t, 0, inf);
int k;
scanf("%d", &k);
while (k--) {
int x;
scanf("%d", &x);
ins(i, x, 1, inf);
}
}
for (int i = 1; i <= t; i++) {
if (d[i] > 0) add(S, i, d[i]);
else if (d[i] < 0) add(i, T, -d[i]);
}
while (bfs()) while (dinic(S, inf));
ins(t, s, 0, inf);
while (bfs()) while (dinic(S, inf));
cout << Leng[tot] << endl;
return 0;
}

#### 【例题】P4553 80人环游世界

1. $(s,s\_,m,m,0)$
2. $(s\_,i,0,m,0)$
3. $(i+n,t,0,m,0)$
4. $(i,i+n,V[i],V[i],0)$
5. $i,j$ 两个国家通航，连边 $(i+n,j,0,m,Cost_{i,j})$

#include <bits/stdc++.h>
using namespace std;
const int N = 206, M = 1e5 + 6, inf = 0x3f3f3f3f;
int n, m, S, T, s, s_, t, d[N], now[N], pre[N], ans;
int Head[N], Edge[M], Leng[M], Cost[M], Next[M], tot = 1;
bitset<N> v;

inline void add(int x, int y, int z, int w) {
Edge[++tot] = y;
Leng[tot] = z;
Cost[tot] = w;
Edge[++tot] = x;
Leng[tot] = 0;
Cost[tot] = -w;
}

inline void ins(int x, int y, int l, int r, int w) {
add(x, y, r - l, w);
d[x] -= l;
d[y] += l;
}

inline bool spfa() {
v.reset();
memset(d, 0x3f, sizeof(d));
queue<int> q;
q.push(S);
v[S] = 1;
d[S] = 0;
now[S] = m;
while (q.size()) {
int x = q.front();
q.pop();
v[x] = 0;
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i], w = Cost[i];
if (!z || d[y] <= d[x] + w) continue;
d[y] = d[x] + w;
now[y] = min(now[x], z);
pre[y] = i;
if (!v[y]) {
q.push(y);
v[y] = 1;
}
}
}
return d[T] != inf;
}

inline void upd() {
ans += d[T] * now[T];
int x = T;
while (x != S) {
int i = pre[x];
Leng[i] -= now[T];
Leng[i^1] += now[T];
x = Edge[i^1];
}
}

int main() {
cin >> n >> m;
s = n * 2 + 1, s_ = s + 1, t = s_ + 1;
S = t + 1, T = S + 1;
ins(s, s_, m, m, 0);
for (int i = 1; i <= n; i++) {
ins(s_, i, 0, m, 0);
ins(i + n, t, 0, m, 0);
int x;
scanf("%d", &x);
ins(i, i + n, x, x, 0);
}
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++) {
int x;
scanf("%d", &x);
if (~x) ins(i + n, j, 0, m, x);
}
//  ins(t, s, 0, m, 0);
for (int i = 1; i <= t; i++) {
if (d[i] > 0) add(S, i, d[i], 0);
else if (d[i] < 0) add(i, T, -d[i], 0);
}
while (spfa()) upd();
cout << ans << endl;
return 0;
}

#### 【例题】P3980 [NOI2008]志愿者招募

1. $(i,i+1,a_i,+inf,0)$

2. $(t_i +1, s_i, 0, +inf, Cost_i)$

### 最大权闭合子图

$G$ 中的点有点权，则点权和最大的闭合子图称为有向图 $G$最大权闭合子图

#### 定理

• 最大权闭合图的点权和 $=$ 所有正权点权值和 – 最小割。
• 上述图的最小割包含 $S$不在最大权闭合图内的正权节点的边和在最大权闭合图内的负权节点$T$ 的边。

#### 【例题】P2805 [NOI2009]植物大战僵尸

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 6, M = 1e6 + 6, inf = 1e9;
int n, m, s, t, a[N], ans, d[N], deg[N], v[N];
int Head[N], Edge[M], Leng[M], Next[M], tot = 1;
queue<int> q;
vector<int> e[N];

inline void add(int x, int y, int z) {
Edge[++tot] = y;
Leng[tot] = z;
}

inline bool bfs() {
memset(d, 0, sizeof(d));
queue<int> q;
q.push(s);
d[s] = 1;
while (q.size()) {
int x = q.front();
q.pop();
for (int i = Head[x]; i; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (deg[y] || d[y] || !z) continue;
q.push(y);
d[y] = d[x] + 1;
if (y == t) return 1;
}
}
return 0;
}

int dinic(int x, int flow) {
if (x == t) return flow;
int rest = flow;
for (int i = Head[x]; i && rest; i = Next[i]) {
int y = Edge[i], z = Leng[i];
if (d[y] != d[x] + 1 || !z) continue;
int k = dinic(y, min(rest, z));
if (!k) d[y] = 0;
else {
Leng[i] -= k;
Leng[i^1] += k;
rest -= k;
}
}
return flow - rest;
}

int main() {
cin >> n >> m;
s = n * m, t = s + 1;
for (int i = 0; i < s; i++) {
scanf("%d", &a[i]);
int k;
scanf("%d", &k);
while (k--) {
int x, y;
scanf("%d %d", &x, &y);
e[i].push_back(x * m + y);
++deg[x*m+y];
}
}
for (int i = 0; i < n; i++)
for (int j = 1; j < m; j++) {
e[i*m+j].push_back(i * m + j - 1);
++deg[i*m+j-1];
}
for (int i = 0; i < s; i++)
if (!deg[i]) q.push(i), v[i] = 1;
while (q.size()) {
int x = q.front();
q.pop();
for (unsigned int i = 0; i < e[x].size(); i++) {
int y = e[x][i];
if (!v[y] && !--deg[y]) q.push(y), v[y] = 1;
}
}
for (int x = 0; x < s; x++) {
if (!v[x]) continue;
for (unsigned int i = 0; i < e[x].size(); i++) {
int y = e[x][i];
if (!v[y]) continue;
}
if (a[x] > 0) add(s, x, a[x]), add(x, s, 0), ans += a[x];
}
int now = 0;
while (bfs())
while ((now = dinic(s, inf)))
ans -= now;
cout << ans << endl;
return 0;
}

# 致谢 & 参考资料

posted @ 2019-03-01 16:16 xht37 阅读(...) 评论(...) 编辑 收藏