网络流之最大流 学习笔记

网络流基本概念

相关概念

  1. 网络:特殊的 有向图 \(G = (V,E)\),与一般的有向图的不同点在于有 源汇点容量
  2. 容量:网络中的每条边 \((u, v)\) 都有的一个权值,记作 \(c(u, v)\),表示单位时间内允许流过的 最大流量。当 \((u, v) \notin E\) 时,可以假定 \(c(u, v) = 0\)
  3. 源汇点:可类比水流的源点和汇点。源点可以流出 无穷 的流量,一般用 \(S\) 表示。汇点可以可以流入 无穷 的流量,一般用 \(T\) 表示。
  4. 流量:类比水流理解即可。
  5. 可行流:对于满足下面两个条件的流,称为可行流。一般用字母 \(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\)

  1. 可行流的流量:从源点流出的流量之和叫做可行流的流量。
  2. 最大流:流网络中流量值最大的一个 可行流,称为最大可行流,简称最大流。
  3. 残量网络:对于流网络 \(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)\)
  4. 残量网络的流与原图的流的关系: 对于一个可行流 \(f\),设 \(f'\) 为该可行流的残量网络的一个流,那么 \(f + f'\) 也是原图的一个可行流,并且流量为 \(|f + f'| = |f| + |f'|\)
  5. 增广路径:在残量网络中一条从源点出发沿着容量大于\(0\) 的边走到汇点的路径叫做 增广路径
  6. :对于流网络 \(G = (V,E)\),将 \(V\) 划分为互不相交的两个子集(子集中点不必有边相连) \(S, T\),满足 \(S \cup T = V,S \in S,T \in T\)。这样的划分叫做
  7. 割的容量:所有从 \(S\) 指向 \(T\) 的边的容量之和称为 割的容量。即:
    \(c(S, T) = \sum_{(u, v) \in E,u \in S,v \in T}c(u, v)\)
  8. 最小割:流网络所有割中,容量最小的割的容量简称为最小割。

最大流最小割定理

对于一个流网络 \(G = (V,E)\),下列三个结论等价:

  1. 可行流 \(f\)\(G\) 的最大流。
  2. \(f\) 的残量网络不存在增广路。
  3. \(\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\) 的最大流,将这个最大流加上原来的流量即可。
一些注意点:

  1. 原来的流量等于新图上 \(T \to S\) 的反向边的容量。
  2. \(S\)\(T\) 的最大流时需要将 \(T \to S\) 这条加入的边删掉。
  3. 其余边的状态都 不能 改变。

代码:

// 有源汇上下界最大流 
// 首先跑出一个可行流
// 然后在这个可行流的基础上跑最大流 
#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\)

  1. 计算其最长不下降子序列的长度 \(s\)
  2. 如果每个元素只允许使用一次,计算从给定的序列中最多可取出多少个长度为 \(s\) 的不下降子序列。
  3. 如果允许在取出的序列中多次使用 \(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\) 向它能到的所有点连一条有向边。
在新图上求 无交最小路径覆盖 就是答案。

posted @ 2026-09-10 20:27  Czl2007  阅读(14)  评论(0)    收藏  举报