网络流之最大流 学习笔记
网络流基本概念
相关概念
- 网络:特殊的 有向图 \(G = (V,E)\),与一般的有向图的不同点在于有 源汇点 和 容量。
- 容量:网络中的每条边 \((u, v)\) 都有的一个权值,记作 \(c(u, v)\),表示单位时间内允许流过的 最大流量。当 \((u, v) \notin E\) 时,可以假定 \(c(u, v) = 0\)。
- 源汇点:可类比水流的源点和汇点。源点可以流出 无穷 的流量,一般用 \(S\) 表示。汇点可以可以流入 无穷 的流量,一般用 \(T\) 表示。
- 流量:类比水流理解即可。
- 可行流:对于满足下面两个条件的流,称为可行流。一般用字母 \(f\) 表示, \(|f|\) 表示可行流的 流量。
1. 容量限制。每条边的 流量 不能超过该边的 容量,即:
\(0 \leq f(u, v) \leq c(u, v) \left \{ u,v \in V ,(u, v) \in E\right \}\)
2.流量守恒。在流网络中,除了源点和汇点,每一个点流入的流量和流出的流量相等。即:
\(\forall x \in V / \left \{ u, v \right \}, \sum_{u \in V}f(u, x) - \sum_{v \in V}f(x, v) = 0\)
- 可行流的流量:从源点流出的流量之和叫做可行流的流量。
- 最大流:流网络中流量值最大的一个 可行流,称为最大可行流,简称最大流。
- 残量网络:对于流网络 \(G\) 中任意一个可行流 \(f\),都有一个残留网络 \(G_f\)。 \(G_f\) 的构造: \(V_f = V\),\(E_f = E \cup \left \{ (v, u),(u, v) \in E\right \}\)。对于原图上的边 \((u, v)\), \(c_f(u, v) = c(u,v) - f(u, v)\)。对于新加的边 \((v, u)\),\(c_f(v, u) = f(u, v)\)。
- 残量网络的流与原图的流的关系: 对于一个可行流 \(f\),设 \(f'\) 为该可行流的残量网络的一个流,那么 \(f + f'\) 也是原图的一个可行流,并且流量为 \(|f + f'| = |f| + |f'|\)。
- 增广路径:在残量网络中一条从源点出发沿着容量大于\(0\) 的边走到汇点的路径叫做 增广路径。
- 割:对于流网络 \(G = (V,E)\),将 \(V\) 划分为互不相交的两个子集(子集中点不必有边相连) \(S, T\),满足 \(S \cup T = V,S \in S,T \in T\)。这样的划分叫做 割。
- 割的容量:所有从 \(S\) 指向 \(T\) 的边的容量之和称为 割的容量。即:
\(c(S, T) = \sum_{(u, v) \in E,u \in S,v \in T}c(u, v)\) - 最小割:流网络所有割中,容量最小的割的容量简称为最小割。
最大流最小割定理
对于一个流网络 \(G = (V,E)\),下列三个结论等价:
- 可行流 \(f\) 为 \(G\) 的最大流。
- \(f\) 的残量网络不存在增广路。
- \(\exists [S,T],|f| = c(S, T)\)。
最大流
模板
\(dinic\) 算法求最大流
题目描述:给出一张网络 \(G\),网络中有 \(n\) 个点, \(m\) 条边,每条边有一个容量 \(c_i\),源点是 \(S\),汇点是 \(T\)。求该流网络的最大流。
分析:
根据 最大流最小割定理,我们可以通过不断地在 残量网络 上找 增广路径,并把增广路径能够提供的最大流量累加到答案中,同时动态修改残量网络,直到残量网络中 没有增广路径 为止。具体实现方式如下:
1.每次跑一遍 \(bfs\) ,判断是否存在从源点到汇点的增广路。同时给图中的点进行分层。
2.每次 \(bfs\) 后,从源点出发榨干当前所有增广路。这个过程可以通过 \(dfs\) 实现:我们对于当前搜索状态记录一个 \(limit\) 表示从该节点 能流出的最大流量,然后扫描出边进行 多条路增广。下一个节点的 \(limit\) 等于当前 \(limit\) 与出边的容量(指残留网络上的边)的较小值。
3.几个不能忽略的优化:
1.当前弧 优化:对于一个点,我们不必每次都从头扫描它的出边,只需从 还能够增广 的出边开始扫描。我们可以用一个 \(cur\) 数组记录这个信息。
2. 如果当前已经增加的流量 等于 \(limit\),那么直接停止枚举。
3. 如果发现 从一个点往后不能被增广,那么标记这个点,下一次枚举到它直接跳过。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int N = 210;
const int M = 5100;
typedef long long LL;
const LL INF = 3e9;
int n, m, s, t, d[N], cur[N]; // d[i] 表示每个点所在的层 cur[i] 当前弧
struct edge {
int v, last; LL cap;
}E[M * 2];
int head[N], idx, u, v;
LL w;
void add(int u, int v, LL c) {
E[idx].v = v, E[idx].last = head[u], E[idx].cap = c, head[u] = idx ++;
E[idx].v = u, E[idx].last = head[v], E[idx].cap = 0, head[v] = idx ++;
}
bool bfs() { // bfs 判断有无增广路
memset(d, -1, sizeof d);
queue< int > q;
q.push(s); d[s] = 0; cur[s] = head[s];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v;
if(d[ver] == -1 && E[i].cap) {
d[ver] = d[u] + 1;
cur[ver] = head[ver];
if(ver == t) return true;
q.push(ver);
}
}
}
return false;
}
LL Find(int u, LL limit) {
if(u == t) return limit; // 汇点无限制
LL flow = 0; // 记录当前流量
for(int i = cur[u]; ~i && flow < limit; i = E[i].last) {
cur[u] = i; // 一旦循环过就会将一条增广路榨干, 更新 cur 数组
int ver = E[i].v; LL cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
LL t = Find(ver, min(limit - flow, cap));
if(!t) d[ver] = -1; // 被榨干了,不能再用了
flow += t; E[i].cap -= t; E[i ^ 1].cap += t;
}
}
return flow;
}
LL dinic() { // dinic
LL res = 0, flow;
while(bfs()) while(flow = Find(s, INF)) res += flow; // 多次Find,是因为可能回流后更优
return res;
}
int main() {
scanf("%d%d%d%d", &n, &m, &s, &t);
memset(head, -1, sizeof head); // 初值不要忘记
for(int i = 1; i <= m; i ++ ) {
scanf("%d%d%lld", &u, &v, &w);
add(u, v, w); // 建边
}
printf("%lld\n", dinic());
return 0;
}
无源汇上下界可行流
题目描述:给你一张 无源汇 网络,每条边的容量有上下界,求出一组可行流,满足每个点流量守恒。
分析:
普通的最大流只能求下界为 \(0\) 的情况。
首先将所有边的流量设置为下界,先满足下界的限制。
那么一条边 \((u, v, a, b)\) 变为 \((u, v, 0, b - a)\) 加入图中,表示当前这条边的容量还剩下 \(b -a\)。
由于流量可能不守恒,记每个点的入流量为 \(in_i\),出流量为 \(ou_i\)。 \(w_i = in_i - ou_i\)。
如果 \(w_i > 0\),表示入流量太多了。我们连一条 \((S, i, w_i)\) 的有向边,表示假装源点提供了这多余的入流量,希望能经过原图的边将这些流量流走。
如果 \(w_i < 0\),表示出流量太多了。我们连一条 \((i, T, -w_i)\) 的有向边,表示假装需要往汇点流出这些流量,希望能通过原图的边将这些流量流入。
其中 \(S, T\) 都是新建的虚拟点。
对这张图跑 最大流,有解的条件是从 \(S\) 连出去的边都满流(此时连向 \(T\) 的边也一定都满流)。
将当前每条边的流量加下界流量就是一组解。
代码:
// 无源汇上下界可行流
#include<bits/stdc++.h>
using namespace std;
const int N = 210;
const int M = 21000;
const int INF = 1e9;
int n, m, s, t;
int w[N];
int head[N], ide;
int d[N], cur[N], h[M * 2];
struct edge {
int v, last, cap;
}E[M * 2];
void add(int u, int v, int c) {
E[ide] = (edge) {v, head[u], c};
head[u] = ide ++;
}
bool bfs() {
memset(d, -1, sizeof d);
queue< int > q; q.push(s);
d[s] = 0; cur[s] = head[s];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, c = E[i].cap;
if(d[ver] == -1 && c) {
d[ver] = d[u] + 1;
if(ver == t) return true;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return false;
}
int Find(int u, int limit) {
if(u == t) return limit;
int flow = 0;
for(int i = cur[u]; ~i && flow < limit; i = E[i].last) {
cur[u] = i;
int ver = E[i].v, c = E[i].cap;
if(d[ver] == d[u] + 1 && c) {
int t = Find(ver, min(limit - flow, c));
if(!t) d[ver] = -1;
flow += t; E[i].cap -= t; E[i ^ 1].cap += t;
}
}
return flow;
}
int dinic() {
int res = 0, flow;
while(bfs()) while(flow = Find(s, INF)) res += flow;
return res;
}
int main() {
scanf("%d%d", &n, &m);
memset(head, -1, sizeof head);
s = 0, t = n + 1;
for(int i = 1; i <= m; i ++ ) {
int u, v, a, b;
scanf("%d%d%d%d", &u, &v, &a, &b);
h[(i - 1) * 2] = a;
w[v] += a; w[u] -= a;
add(u, v, b - a); add(v, u, 0);
}
int all = 0;
for(int i = 1; i <= n; i ++ ) {
if(w[i] > 0) {
add(s, i, w[i]); add(i, s, 0);
all += w[i];
}
else if(w[i] < 0) {
add(i, t, -w[i]); add(t, i, 0);
}
}
if(dinic() == all) {
puts("YES");
for(int i = 0; i < m * 2; i += 2) {
printf("%d\n", E[i ^ 1].cap + h[i]);
}
}
else puts("NO");
return 0;
}
有源汇上下界可行流
题目描述:给你一张 有源汇 的网络,每条边的容量有上下界,求一组可行流。
分析:
发现源点 \(S\) 的流出量等于汇点 \(T\) 的流入量,除了源汇点外所有点流量守恒。
连一条汇点向源点,容量为 \(INF\) 的有向边。即 \((T, S, INF)\)
那么问题转化成了无源汇上下界可行流。
有源汇上下界最大流
题目描述: 给你一张 有源汇 的网络,每条边的容量有上下界,求一组最大流。
分析:
首先按照 有源汇上下界可行流 的方法找到一组合法解,如果没有就结束了。
否则在当前的残留网络上跑从 \(S\) 到 \(T\) 的最大流,将这个最大流加上原来的流量即可。
一些注意点:
- 原来的流量等于新图上 \(T \to S\) 的反向边的容量。
- 跑 \(S\) 到 \(T\) 的最大流时需要将 \(T \to S\) 这条加入的边删掉。
- 其余边的状态都 不能 改变。
代码:
// 有源汇上下界最大流
// 首先跑出一个可行流
// 然后在这个可行流的基础上跑最大流
#include<bits/stdc++.h>
using namespace std;
const int N = 210;
const int M = 2e4;
const int INF = 1e9;
struct edge {
int v, last, cap;
}E[M * 2];
int cur[N], d[N];
int head[N], ide;
int w[N];
int n, m, S, T, sx, tx;
void add(int u, int v, int cap) {
E[ide] = (edge) {v, head[u], cap};
head[u] = ide ++;
}
bool bfs(int s, int t) {
memset(d, -1, sizeof d);
queue< int > q; q.push(s);
d[s] = 0; cur[s] = head[s];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, c = E[i].cap;
if(d[ver] == -1 && c) {
d[ver] = d[u] + 1;
if(ver == t) return true;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return false;
}
int Find(int u, int t, int limit) {
if(u == t) return limit;
int flow = 0;
for(int i = cur[u]; ~i && flow < limit; i = E[i].last) {
cur[u] = i;
int ver = E[i].v, c = E[i].cap;
if(d[ver] == d[u] + 1 && c) {
int v = Find(ver, t, min(c, limit - flow));
if(!v) d[ver] = -1;
flow += v; E[i].cap -= v; E[i ^ 1].cap += v;
}
}
return flow;
}
int dinic(int s, int t) {
int res = 0, flow;
while(bfs(s, t)) {
while(flow = Find(s, t, INF)) {
res += flow;
}
}
return res;
}
int main() {
scanf("%d%d%d%d", &n, &m, &S, &T);
memset(head, -1, sizeof head);
sx = 0, tx = n + 1;
for(int i = 1; i <= m; i ++ ) {
int u, v, a, b;
scanf("%d%d%d%d", &u, &v, &a, &b);
w[u] -= a; w[v] += a;
add(u, v, b - a); add(v, u, 0);
}
add(T, S, INF); add(S, T, 0);
int all = 0;
for(int i = 1; i <= n; i ++ ) {
if(w[i] > 0) {
all += w[i];
add(sx, i, w[i]);
add(i, sx, 0);
}
else if(w[i] < 0) {
add(i, tx, -w[i]);
add(tx, i, 0);
}
}
if(dinic(sx, tx) != all) puts("No Solution");
else {
int res = E[2 * m + 1].cap;
E[2 * m].cap = E[2 * m + 1].cap = 0;
res += dinic(S, T);
printf("%d\n", res);
}
return 0;
}
有源汇上下界最小流
题目描述:给你一张 有源汇 的网络,每条边的容量有上下界,求一组最小流。
分析:
首先按照 有源汇上下界可行流 的方法找到一组合法解,如果没有就结束。
否则在当前的残留网络上跑从 \(T\) 到 \(S\) 的最大流,将原来的流量减去这个最大流即可。
代码:
// 有源汇上下界最小流
#include<bits/stdc++.h>
using namespace std;
const int N = 7e4 + 10;
const int M = 330000;
typedef long long LL;
const LL INF = 1e18;
int n, m, S, T, s, t;
struct edge {
int v, last; LL cap;
}E[M * 2];
LL w[N];
int head[N], ide;
int cur[N], d[N];
void add(int u, int v, LL cap) {
E[ide] = (edge) {v, head[u], cap};
head[u] = ide ++;
}
bool bfs(int sx, int tx) {
memset(d, -1, sizeof d);
queue< int > q; d[sx] = 0;
q.push(sx); cur[sx] = head[sx];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v; LL cap = E[i].cap;
if(d[ver] == -1 && cap) {
d[ver] = d[u] + 1;
if(ver == tx) return true;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return false;
}
LL Find(int u, int goal, LL limit) {
if(u == goal) return limit;
LL flow = 0;
for(int i = cur[u]; ~i && flow < limit; i = E[i].last) {
cur[u] = i;
int ver = E[i].v; LL c = E[i].cap;
if(d[ver] == d[u] + 1 && c) {
LL v = Find(ver, goal, min(limit - flow, c));
if(!v) d[ver] = -1;
flow += v; E[i].cap -= v, E[i ^ 1].cap += v;
}
}
return flow;
}
LL dinic(int s, int t) {
LL res = 0, flow;
while(bfs(s, t)) {
while(flow = Find(s, t, INF)) res += flow;
}
return res;
}
int main() {
scanf("%d%d%d%d", &n, &m, &S, &T);
s = 0; t = n + 1;
memset(head, -1, sizeof head);
for(int i = 1; i <= m; i ++ ) {
int a, b; LL c, d;
scanf("%d%d%lld%lld", &a, &b, &c, &d);
w[b] += c; w[a] -= c;
add(a, b, d - c); add(b, a, 0);
}
add(T, S, INF); add(S, T, 0);
LL all = 0;
for(int i = 1; i <= n; i ++ ) {
if(w[i] > 0) {
all += w[i];
add(s, i, w[i]);
add(i, s, 0);
}
else if(w[i] < 0) {
add(i, t, -w[i]);
add(t, i, 0);
}
}
if(dinic(s, t) == all) { // 跑出了一个可行流
LL res = 0;
res = E[m * 2 + 1].cap;
E[m * 2].cap = 0; E[m * 2 + 1].cap = 0;
res -= dinic(T, S);
printf("%lld\n", res);
}
else puts("No Solution");
return 0;
}
多源汇最大流
题目描述:给你一张 多源汇 的网络,求最大流。
分析:
建立超级源点 \(S\) 和超级汇点 \(T\)。连 \((S, s_i, INF)\), \((t_i, T, INF)\) 的有向边。跑最大流即可。
最大流练习题
二分图最大匹配
题意:给你一张二分图,求它的最大匹配。
分析:
网络流可以求 二分图 的最大匹配,但是不能求一般图的最大匹配。
建图方法是:源点向每个左部点连一条容量为 \(1\) 的有向边,对于一条边 \((u, v)\),连一条\(u\) 向 \(v\) 容量为 \(1\) 的有向边。每个右部点向汇点连一条容量为 \(1\) 的有向边。
然后跑最大流即可。
P2754 [CTSC1999] 家园 / 星际转移问题(拆点,流动模型)
二分答案,假设二分的答案是 \(tim\),将一个点 \(x\) 的状态拆成 \((t, x)\)(\(0 \leq t \leq tim\))
不难证明 \(tim\) 的上界是 \(n^2\) 量级的,因此最多有 \(n^3\) 个点。
然后枚举每个飞船进行连边。比如 \(t\) 时刻这个飞船在 \(x\),下个时刻飞船到 \(y\),飞船的容量为 \(c\),那么就把 \((t, x)\) 向 \((t + 1, y)\) 连一条容量为 \(c\) 的有向边。
由于可以等待,因此我们对每个 \((t, x)\) 往 \((t + 1, x)\) 连一条容量为 \(INF\) 的有向边。
总边数为 \(m \times tim + n^2\)。
然后跑一遍最大流看是否大于 \(K\) 即可。
CODE:
#include<bits/stdc++.h>
using namespace std;
const int N = 25;
const int M = 120000;
const int INF = 1e9;
int n, m, K;
int h[N], r[N], S[N][N];
int head[M], d[M], ide;
int cur[M];
struct edge {
int v, last, cap;
}E[M * 4];
void add(int u, int v, int cap) {
E[ide] = (edge) {v, head[u], cap};
head[u] = ide ++;
}
bool bfs(int s, int t, int c) {
for(int i = 0; i <= c; i ++ ) d[i] = -1;
d[s] = 0; queue< int > q; q.push(s);
cur[s] = head[s];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == -1 && cap) {
d[ver] = d[u] + 1;
if(ver == t) return true;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return false;
}
int Find(int u, int t, int limit) {
if(u == t) return limit;
int flow = 0;
for(int i = cur[u]; ~i && flow < limit; i = E[i].last) {
cur[u] = i;
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
int v = Find(ver, t, min(limit - flow, cap));
if(!v) d[ver] = -1;
flow += v; E[i].cap -= v; E[i ^ 1].cap += v;
}
}
return flow;
}
int dinic(int s, int t, int c) {
int res = 0, flow;
while(bfs(s, t, c)) while(flow = Find(s, t, INF)) res += flow;
return res;
}
inline int ID(int T, int t, int x) {
return x * (T + 1) + t; // 算上 0 时刻
}
bool check(int tim) {
// 建图, 将每个点拆成(t, i) 总过是 tim * N <= 2 * N ^ 4
memset(head, -1, sizeof head);
ide = 0;
for(int i = 1; i <= m; i ++ ) { // 每一种边
for(int t = 0; t < tim; t ++ ) {
add(ID(tim, t, S[i][t % r[i]]), ID(tim, t + 1, S[i][(t + 1) % r[i]]), h[i]);
add(ID(tim, t + 1, S[i][(t + 1) % r[i]]), ID(tim, t, S[i][t % r[i]]), 0);
}
}
for(int i = 0; i <= n + 1; i ++ ) {
for(int t = 0; t < tim; t ++ ) {
add(ID(tim, t, i), ID(tim, t + 1, i), INF);
add(ID(tim, t + 1, i), ID(tim, t, i), 0);
}
}
return dinic(ID(tim, 0, 0), ID(tim, tim, n + 1), (tim + 1) * (n + 2) - 1) >= K;
}
int main() {
scanf("%d%d%d", &n, &m, &K);
for(int i = 1; i <= m; i ++ ) {
scanf("%d%d", &h[i], &r[i]);
h[i] = min(h[i], K);
for(int j = 0; j < r[i]; j ++ ) {
scanf("%d", &S[i][j]);
if(S[i][j] == -1) S[i][j] = n + 1;
}
}
int l = 1, r = 4400, mid, res = 0;
while(l <= r) {
mid = (l + r >> 1);
if(check(mid)) res = mid, r = mid - 1;
else l = mid + 1;
}
printf("%d\n", res);
return 0;
}
餐饮(拆点,限制使用次数,匹配模型)
题意:
有 \(n\) 个人,\(F\) 种食物, \(D\) 种饮料。每个人都有自己喜欢的食物和饮料(可能有多个),当一个人吃到了一种自己喜欢的食物 并且 喝到了一种 自己喜欢的饮料时这个人是满意的。
每种食物和饮料只能被吃一次。问最多能使多少人满意。
数据规模:
\(n,F,D \leq 100\)
分析:
从源点连向每种食物容量为 \(1\) 的有向边,表示只能被用一次。
每种食物向喜欢它的人连一条容量为 \(1\) 的有向边。
如果这时每个人向他喜欢的饮料连一条容量为 \(1\) 的有向边,每种饮料向汇点连一条容量为 \(1\) 的有向边 跑最大流。发现每种食物和饮料都只会被用一次,但是一个人的贡献可能被多次算。
怎么办?
考虑将一个人的节点拆成 \(x\) 和 \(x'\)。 \(x\) 负责流入, \(x'\) 负责流出, \(x\) 向 \(x'\) 连一条容量为 \(1\) 的有向边即可。
CODE:
// 分三层即可
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
const int INF = 1e8;
struct edge {
int v, last, cap;
}E[6 * N * N];
int ide, head[N * 4];
int n, F, D, S, T;
int cur[N * 4], d[N * 4];
void add(int u, int v, int cap) {
E[ide] = (edge) {v, head[u], cap};
head[u] = ide ++;
}
bool bfs(int s, int t) {
queue< int > q; q.push(s);
memset(d, -1, sizeof d); d[s] = 0;
cur[s] = head[s];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == -1 && cap) {
d[ver] = d[u] + 1;
if(ver == t) return true;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return false;
}
int Find(int u, int t, int limit) {
if(u == t) return limit;
int flow = 0;
for(int i = cur[u]; ~i && flow < limit; i = E[i].last) {
cur[u] = i;
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
int v = Find(ver, t, min(limit - flow, cap));
if(!v) d[ver] = -1;
flow += v; E[i].cap -= v; E[i ^ 1].cap += v;
}
}
return flow;
}
int dinic(int s, int t) {
int res = 0, flow;
while(bfs(s, t)) while(flow = Find(s, t, INF)) res += flow;
return res;
}
int main() {
freopen("dining.in", "r", stdin);
freopen("dining.out", "w", stdout);
scanf("%d%d%d", &n, &F, &D);
memset(head, -1, sizeof head);
S = 0, T = n * 2 + F + D + 1;
for(int i = 1; i <= n; i ++ ) {
int ff, dd; scanf("%d%d", &ff, &dd);
for(int j = 1; j <= ff; j ++ ) {
int x; scanf("%d", &x);
add(2 * n + x, i, 1);
add(i, 2 * n + x, 0);
}
for(int j = 1; j <= dd; j ++ ) {
int x; scanf("%d", &x);
add(i + n, n * 2 + F + x, 1);
add(n * 2 + F + x, i + n, 0);
}
}
for(int i = 1; i <= n; i ++ ) {
add(i, i + n, 1);
add(i + n, i, 0);
}
for(int i = 1; i <= F; i ++ ) {
add(S, n * 2 + i, 1);
add(n * 2 + i, S, 0);
}
for(int i = 1; i <= D; i ++ ) {
add(n * 2 + F + i, T, 1);
add(T, n * 2 + F + i, 0);
}
printf("%d\n", dinic(S, T));
return 0;
}
P2766 最长不下降子序列问题(匹配模型)
题意:
给定正整数序列 \(x_1 \ldots, x_n\)。
- 计算其最长不下降子序列的长度 \(s\)。
- 如果每个元素只允许使用一次,计算从给定的序列中最多可取出多少个长度为 \(s\) 的不下降子序列。
- 如果允许在取出的序列中多次使用 \(x_1\) 和 \(x_n\)(其他元素仍然只允许使用一次),则从给定序列中最多可取出多少个不同的长度为 \(s\) 的不下降子序列。
令 \(a_1, a_2, \ldots, a_s\) 为构造 \(S\) 时所使用的下标,\(b_1, b_2, \ldots, b_s\) 为构造 \(T\) 时所使用的下标。且 \(\forall i \in [1,s-1]\),都有 \(a_i \lt a_{i+1}\),\(b_i \lt b_{i+1}\)。则 \(S\) 和 \(T\) 不同,当且仅当 \(\exists i \in [1,s]\),使得 \(a_i \neq b_i\)。
数据规模:
\(1 \le n\le 500\)
分析:
典题。这个题教会了我们怎样求 每个元素只能使用一次,一个序列最多能划分成多少个 \(LIS\)。
对于第一问,可以用 \(O(n^2)\) 的 \(dp\) 暴力求。
第二第三问需要网络流:
设我们求出来 \(f_i\) 表示以 \(i\) 结尾的最长不降子序列长度。
考虑取出子序列的过程中, 一个 \(i\) 后面接什么样的 \(j\) 是有可能的。
发现必须满足 \(f_j = f_i + 1,a_j \geq a_i\)。
不满足第二个条件显然没法接。不满足第一个条件,那么 \(f_j\) 肯定大于 \(f_i + 1\)。如果接上了 \(j\),那么这个子序列不管后面接什么都不可能达到 \(LIS\) 的长度。因为如果能达到,那么就可以在 \(f_j\) 的基础上接一模一样的, \(LIS\) 会变大。
因此我们连一条 \((i, j, 1)\) 的边,有流量的意义为 \(j\) 接在了 \(i\) 后面。
每个元素只能用一次,根据上一道题的套路,将一个点拆成 \((x', x)\),其中 \(x'\) 连接入边, \(x\) 连接出边, \(x‘\) 向 \(x\) 连一条容量为 \(1\) 的边。
如果 \(f_i = 1\),就从源点连一条容量为 \(1\) 的边。
如果 \(f_i = lis\),就向汇点连一条容量为 \(1\) 的边。
跑一遍最大流就是第二问的答案。
对于第三问,只需要将源点汇点与 \(1, n\) 之间边的容量以及 \(1',1\) 和 \(n',n\) 之间边的容量修改为 \(INF\)。
注意特判 \(lis = 1\) 的情况。
CODE:
// 典题:每个元素最多用一次,求最多将序列划分成多少LIS
#include<bits/stdc++.h>
using namespace std;
const int N = 505;
const int INF = 1e8;
int n, f[N], a[N];
int S, T, l;
struct edge {
int v, last, cap;
}E[N * N * 4];
int head[N * 2], ide;
int cur[N * 2], d[N * 2];
void add(int u, int v, int cap) {
E[ide] = (edge) {v, head[u], cap};
head[u] = ide ++;
}
void Link(int i, int p) {
if(!p) add(i, i + n, 1), add(i + n, i, 0);
else add(i, i + n, INF), add(i + n, i, 0);
if(f[i] == 1) {
if(!p) add(S, i, 1), add(i, S, 0);
else add(S, i, INF), add(i, S, 0);
}
if(f[i] == l) {
if(!p) add(i + n, T, 1), add(T, i + n, 0);
else add(i + n, T, INF), add(T, i + n, 0);
}
for(int j = i + 1; j <= n; j ++ ) {
if(a[j] >= a[i] && f[j] == f[i] + 1) {
add(i + n, j, 1);
add(j, i + n, 0);
}
}
}
void build(int o) {
memset(head, -1, sizeof head);
ide = 0;
for(int i = 2; i < n; i ++ ) Link(i, 0);
Link(1, o); Link(n, o);
}
bool bfs(int s, int t) {
memset(d, -1, sizeof d); d[s] = 0;
queue< int > q; q.push(s);
cur[s] = head[s];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, c = E[i].cap;
if(d[ver] == -1 && c) {
d[ver] = d[u] + 1;
if(ver == t) return true;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return false;
}
int Find(int u, int t, int limit) {
if(u == t) return limit;
int flow = 0;
for(int i = cur[u]; ~i && flow < limit; i = E[i].last) {
cur[u] = i;
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
int v = Find(ver, t, min(limit - flow, cap));
if(!v) d[ver] = -1;
flow += v; E[i].cap -= v; E[i ^ 1].cap += v;
}
}
return flow;
}
int dinic(int s, int t) {
int res = 0, flow;
while(bfs(s, t)) while(flow = Find(s, t, INF)) res += flow;
return res > n ? n : res;
}
int main() {
scanf("%d", &n);
S = 0, T = n * 2 + 1;
for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
f[0] = 0;
for(int i = 1; i <= n; i ++ ) {
for(int j = 0; j < i; j ++ ) {
if(a[j] <= a[i]) f[i] = max(f[i], f[j] + 1);
}
}
l = 0;
for(int i = 1; i <= n; i ++ ) l = max(l, f[i]);
printf("%d\n", l);
int ret;
build(0);
ret = dinic(S, T);
printf("%d\n", ret);
build(1);
ret = dinic(S, T);
printf("%d\n", ret);
return 0;
}
P2764 无交最小路径覆盖问题(拆点,匹配模型)
问题描述:
给定有向图 \(G=(V,E)\) 。设 \(P\) 是 \(G\) 的一个简单路(顶点不相交)的集合。如果 \(V\) 中每个定点恰好在 \(P\) 的一条路上,则称 \(P\) 是 \(G\) 的一个路径覆盖。\(P\) 中路径可以从 \(V\) 的任何一个定点开始,长度也是任意的,特别地,可以为 \(0\)。\(G\) 的最小路径覆盖是 \(G\) 所含路径条数最少的路径覆盖。设计一个有效算法求一个 DAG(有向无环图)\(G\) 的最小路径覆盖。
\(1\leq n\leq 150\),\(1\leq m\leq 6000\)。
分析:
问最少能将一张 \(DAG\) 划分成多少条不交路径。
成两个点之间有一条有向边为一个 匹配。那么有一个很重要的定理是:当路径不交时,路径数 = 点数 - 匹配数
这个不难证明:一条路径的匹配数 = 路径上点数 - 1,因此总点数减总匹配数就是路径条数。
我们想让路径数最少,因此需要最大化匹配数。
一个点能匹配两次,发现它既可以作为入点匹配,又可以作为出点匹配。
那么我们把一个点拆成入点和出点两个状态,设 \(x'\) 为 \(x\) 的入点, \(x\) 为 \(x'\) 的出点。
与之前 拆点是为了控制使用次数不同,这里拆点是为了表示两种匹配状态。因此 \(x'\) 和 \(x\) 之间没有边相连。
连边方式:
- \(S\) 向所有 \(x'\) 连一条容量为 \(1\) 的边,表示这个点作为出点匹配最多一次。
- 所有 \(x\) 向 \(T\) 连一条容量为 \(1\) 的边,表示这个点作为入点匹配最多一次。
- 如果存在一条有向边 \((u, v)\),那么连一条 \(u\) 指向 \(v'\) 容量为 \(1\) 的边,表示一种可行的匹配。
跑最大流即可。输出方案只需要找到每个点的出点状态流向谁就可以知道它的后继。
CODE:
// 最大流模型 -- DAG无交最小路径覆盖
// 最小路径覆盖数 = 点数 - 最大匹配
// 一个点可以匹配两次,就拆成作为出点和作为入点两种点
// 跑最大流即可
#include<bits/stdc++.h>
using namespace std;
const int N = 155;
const int M = 6010;
const int INF = 1e8;
int n, m;
int S, T, node[N][2], idx;
struct edge {
int v, last, cap;
}E[(M + N * 3) * 2];
int head[N * 2], ide;
int cur[N * 2], d[N * 2];
int ID[N * 2], nxt[N], lst[N];
void add(int u, int v, int cap) {
E[ide] = (edge) {v, head[u], cap};
head[u] = ide ++;
}
bool bfs(int s, int t) {
memset(d, -1, sizeof d); d[s] = 0;
queue< int > q; q.push(s); cur[s] = head[s];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == -1 && cap) {
d[ver] = d[u] + 1;
if(ver == t) return true;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return false;
}
int Find(int u, int t, int limit) {
if(u == t) return limit;
int flow = 0;
for(int i = cur[u]; ~i && flow < limit; i = E[i].last) {
cur[u] = i;
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
int v = Find(ver, t, min(limit - flow, cap));
if(!v) d[ver] = -1;
flow += v; E[i].cap -= v; E[i ^ 1].cap += v;
}
}
return flow;
}
int dinic(int s, int t) {
int res = 0, flow;
while(bfs(s, t)) while(flow = Find(s, t, INF)) res += flow;
return res;
}
int main() {
memset(head, -1, sizeof head);
scanf("%d%d", &n, &m);
S = ++ idx; T = ++ idx;
for(int i = 1; i <= n; i ++ ) {
node[i][0] = ++ idx; node[i][1] = ++ idx; // 分别作为入点和出点,两者不连边,一般用于可以匹配两次的情况
ID[node[i][1]] = i;
}
for(int i = 1; i <= n; i ++ ) {
add(S, node[i][0], 1); add(node[i][0], S, 0);
add(node[i][1], T, 1); add(T, node[i][1], 0);
}
for(int i = 1; i <= m; i ++ ) {
int u, v; scanf("%d%d", &u, &v);
add(node[u][0], node[v][1], 1);
add(node[v][1], node[u][0], 0);
}
int ret = dinic(S, T);
for(int i = 1; i <= n; i ++ ) {
for(int j = head[node[i][0]]; ~j; j = E[j].last) {
int v = E[j].v;
if(v == S) continue;
if(E[j ^ 1].cap == 1) {
nxt[i] = ID[v];
lst[ID[v]] = i;
}
}
}
for(int i = 1; i <= n; i ++ ) {
if(lst[i] == 0) {
int now = i;
while(now) {
printf("%d ", now);
now = nxt[now];
}
puts("");
}
}
printf("%d\n", n - ret);
return 0;
}
可交最小路径覆盖问题
题目描述:
给一张 \(DAG\),问最少将图划分成多少条路径使得所有点都被覆盖。路径可交。
分析:
和上题非常类似。
我们首先 \(floyd\) 跑一遍 传递闭包,求出 \(u\) 能到达的所有点 \(v\)。
建一张新图,每个点 \(u\) 向它能到的所有点连一条有向边。
在新图上求 无交最小路径覆盖 就是答案。

浙公网安备 33010602011771号