网络流

引入

网络

是有向图。
每条边有权值 \(c(u,v)\),称为容量(Capacity)。
有两个重要的点,源点 \(s\),汇点 \(t\)

流函数 \(f\) 满足:
容量限制: \(f(u,v)\le c(u,v)\)
斜对称 : \(f(u,v)=-f(u,v)\)
流量守恒 :\(\forall x\not=s,t , \sum_{(u,x)\in E} f(u,x)=\sum_{(x,v)\in E} f(x,v)\).

最大流

概述

运用 Ford-Fulkerson 增广算法。
Ford-Fulkerson 增广是计算最大流的一类算法的总称。该方法运用贪心的思想,通过寻找增广路来更新并求解最大流。

剩余容量:\(c_f(u,v)=c(u,v)-f(u,v)\)
残量网格 \(G_f\)\(E\) 中剩余容量所有大于 0 的边。
增广路:将 \(G_f\) 上一条从源点到汇点的路径称为增广路。
增广:对于一条增广路,给每一条边 \((u,v)\) 都加上等量的流量,以令整个网络的流量增加。

由此,最大流的求解可以被视为若干次增广分别得到的流的叠加。
值得注意的是,根据流的斜对称性,我们在增广时需要记得退流,
\(f(u, v)\) 增加时 \(f(v, u)\) 应当减少同等的量。

下图可能帮你理解退流的过程。

容易发现,只要 \(G_f\) 上存在增广路,那么对其增广就可以令总流量增加;
否则说明总流量已经达到最大可能值,求解过程完成。

最大流最小割定理

割:把图分为两部分,使得源点和汇点在不同的部分,称为割 \((S,T)\)
割的容量为 \(c(S,T)\) 为所有 \(S\)\(T\) 边的容量之和。

最大流=最小割。
粗略的证明:
1.最大流不可能大于最小割, 因为最大流所有的水流都一定经过最小割那些割边, 流过的水流怎么可能比水管容量还大呢?
2.最大流不可能小于最小割, 如果小, 那么说明水管容量没有物尽其用, 可以继续加大水流.

Edmonds-Karp 算法

每次 BFS 寻找增广路,直到没有增广路。
时间复杂度为 \(O(nm^2)\).

Dinic 算法

考虑增广前 BFS 对 \(G_f\) 分层,即按照到 \(s\) 距离分层,
即删除 \(d_v\not = d_u +1\) 的出边,形成层次图 \(G_L\)
如果我们在层次图 \(G_L\) 上找到一个最大的增广流 \(f_b\)
使得仅在 \(G_L\) 上是不可能找出更大的增广流的,则我们称 \(f_b\)\(G_L\) 的阻塞流。
DFS 求解出阻塞流。
将阻塞流合并到原来的流中。
重复以上过程直到不存在 \(s\)\(t\) 路径。

不过,求解阻塞流时,我们需要引入当前弧优化。
如果某一时刻我们已经知道边 \((u, v)\) 已经增广到极限,
\(u\) 的流量没有必要再尝试流向出边 \((u, v)\)
我们维护邻接表中第一条还有必要尝试的出边。
即当前弧。
这是保证 Dinic 时间复杂度正确性的一部分。

常数优化:多路增广。
我们不必要一次只求解一条增广路,而是我们可以再找一条岔路继续 DFS.

时间复杂度 \(O(n^2m)\),求解二分图是 \(O(m\sqrt n )\)

参考代码:(当前弧优化,多路增广)

code
#include<bits/stdc++.h>
using namespace std;
using LL=long long;
const int N=200+5,M=5e3+5;
struct flow {
	int cnt=1,head[N],nxt[M<<1],ver[M<<1],limit[M<<1];
	void add(int u,int v,int w) {
		nxt[++cnt]=head[u],head[u]=cnt,ver[cnt]=v,limit[cnt]=w;
		nxt[++cnt]=head[v],head[v]=cnt,ver[cnt]=u,limit[cnt]=0;
	}
	int T,dis[N],cur[N];
	LL dfs(int id,LL res) {
		if(id==T) return res;
		LL flow=0;
		for(int i=cur[id]; i&&res; i=nxt[i]) {
			cur[id]=i;
			int c=min(res,(LL)limit[i]),it=ver[i];
			if(dis[id]+1==dis[it]&&c) {
				int k=dfs(it,c);
				flow+=k,res-=k,limit[i]-=k,limit[i^1]+=k;
			}
		}
		if(!flow) dis[id]=-1;
		return flow;
	}
	LL maxflow(int s,int t) {
		T=t;
		LL flow=0;
		while(1) {
			queue<int> q;
			memcpy(cur,head,sizeof(head));
			memset(dis,-1,sizeof(dis));
			q.push(s),dis[s]=0;
			while(!q.empty()) {
				int t=q.front();
				q.pop();
				for(int i=head[t]; i; i=nxt[i])
					if(dis[ver[i]]==-1&&limit[i])
						dis[ver[i]]=dis[t]+1,q.push(ver[i]);
			}
			if(dis[t]==-1) return flow;
			flow+=dfs(s,1e18);
		}
	}
} g;
int n,m,s,t;
int main() {
	cin>>n>>m>>s>>t;
	for(int i=1; i<=m; i++) {
		int u,v,w;
		cin>>u>>v>>w;
		g.add(u,v,w);
	}
	cout<<g.maxflow(s,t)<<endl;
	return 0;
}

费用流

概述

相比最大流,每条边多了一个权值,意味着单位流量的花费。
我们要求在最大流情况下费用最小。

SSP 算法是一个贪心的算法。它的思路是每次寻找单位费用最小的增广路进行增广,直到图上不存在增广路为止。

实现

只需要在 Dinic 或 EK 算法找增广路时,把 BFS 换成 SPFA 或其他算法即可。

code(Dinic)

为了防止 MLE,在 dfs 的时候要添加 vis 数组确保不会重复经过一个点。

#include<bits/stdc++.h>
using namespace std;
const int N=5e3+5,M=5e4+5;
const int inf=0x3f3f3f3f;
struct flow {
	int tot=1,head[N],nxt[M<<1],ver[M<<1],limit[M<<1],cst[M<<1];
	void add(int u,int v,int w,int z) {
		nxt[++tot]=head[u],head[u]=tot,ver[tot]=v,limit[tot]=w,cst[tot]=z;
		nxt[++tot]=head[v],head[v]=tot,ver[tot]=u,limit[tot]=0,cst[tot]=-z;
	}
	int T,vis[N],dis[N],cur[N];
	int dfs(int u,int res) {
		vis[u]=1;
		if(u==T) return res;
		int flow=0;
		for(int i=cur[u]; i&&res; i=nxt[i]) {
			cur[u]=i;
			int c=min(res,limit[i]),v=ver[i];
			if(!vis[v]&&dis[v]==dis[u]+cst[i]&&c) {
				int k=dfs(v,c);
				flow+=k,res-=k,limit[i]-=k,limit[i^1]+=k;
			}
		}
		vis[u]=0;
		return flow;
	}
	pair<int,int> mincost(int s,int t) {
		int flow=0,cost=0;
		T=t;
		while(1) {
			queue<int> Q;
			memset(dis,0x3f,sizeof dis);
			memset(vis,0,sizeof vis);
			memcpy(cur,head,sizeof head);
			dis[s]=0,vis[s]=1,Q.push(s);
			for(; !Q.empty(); ) {
				int x=Q.front(); Q.pop();
				vis[x]=0;
				for(int i=head[x]; i; i=nxt[i]) {
					int y=ver[i],c=cst[i];
					if(limit[i]&&dis[x]+c<dis[y]) {
						dis[y]=dis[x]+c;
						if(!vis[y]) vis[y]=1,Q.push(y);
					}
				}
			}
			if(dis[t]>=inf) return make_pair(flow,cost);
			int fl=dfs(s,inf);
			flow+=fl,cost+=fl*dis[t];
		}
	}
} g;
int n,m,s,t;
int main() {
	cin>>n>>m>>s>>t;
	for(int i=1; i<=m; i++) {
		int u,v,w,z;
		cin>>u>>v>>w>>z;
		g.add(u,v,w,z);
	}
	pair<int,int> ans=g.mincost(s,t);
	cout<<ans.first<<" "<<ans.second<<endl;
	return 0;
}
code(EK)
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+5,M=5e4+5;
const int inf=0x3f3f3f3f;
struct flow {
	int tot=1,head[N],nxt[M<<1],ver[M<<1],limit[M<<1],cst[M<<1];
	void add(int u,int v,int w,int z) {
		nxt[++tot]=head[u],head[u]=tot,ver[tot]=v,limit[tot]=w,cst[tot]=z;
		nxt[++tot]=head[v],head[v]=tot,ver[tot]=u,limit[tot]=0,cst[tot]=-z;
	}
	int fr[N],fl[N],vis[N],dis[N];
	pair<int,int> mincost(int s,int t) {
		int flow=0,cost=0;
		while(1) {
			queue<int> Q;
			memset(dis,0x3f,sizeof dis);
			memset(vis,0,sizeof vis);
			fl[s]=inf,dis[s]=0,vis[s]=1,Q.push(s);
			for(; !Q.empty(); ) {
				int x=Q.front(); Q.pop();
				vis[x]=0;
				for(int i=head[x]; i; i=nxt[i]) {
					int y=ver[i],c=cst[i];
					if(limit[i]&&dis[x]+c<dis[y]) {
						fl[y]=min(limit[i],fl[x]),fr[y]=i;
						dis[y]=dis[x]+c;
						if(!vis[y]) vis[y]=1,Q.push(y);
					}
				}
			}
			if(dis[t]>=inf) return make_pair(flow,cost);
			flow+=fl[t],cost+=dis[t]*fl[t];
			for(int u=t; u!=s; u=ver[fr[u]^1]) limit[fr[u]]-=fl[t],limit[fr[u]^1]+=fl[t];
		}
	}
} g;
int n,m,s,t;
int main() {
	cin>>n>>m>>s>>t;
	for(int i=1; i<=m; i++) {
		int u,v,w,z;
		cin>>u>>v>>w>>z;
		g.add(u,v,w,z);
	}
	pair<int,int> ans=g.mincost(s,t);
	cout<<ans.first<<" "<<ans.second<<endl;
	return 0;
}

应用

P2756 飞行员配对方案问题

用 Dinic 求解二分图最大匹配,方法是建立一个源点,向外籍飞行员建容量 \(1\) 的边,
建立汇点,使英国飞行员向其建容量 \(1\) 的边。
求解最大流。输出方案只需把剩余流量为 \(0\) 的边输出。

P4001 [ICPC-Beijing 2006] 狼抓兔子

是最大流最小割定理。

P2598 [ZJOI2009]狼和羊的故事

还是最小割。一个方格向他四周连边即可。

P2472 [SCOI2007] 蜥蜴

这题运用到拆点。
对于一个点,拆成两个点,分别用于入边和出边,容量为正无限。
这两个点之间连一条容量为其石柱高度的边。
求解最大流。

P1231 教辅的组成

把答案向书建容量 \(1\) 边,把书向练习册建容量 \(1\) 边。
注意这里有 Bug,要把书拆点,建容量为 \(1\) 的边。

P2774 方格取数问题

这道题是二分图带权最大独立集。
只要总权值减去最小割即可。
把图黑白染色分成两部分。
源点连黑色点,白色点连汇点,容量都为权值。
再将联通的点建边,权值 inf。

P1646 [国家集训队]happiness

老套路,先将所有权值加上,在减去最小割。
源点对应文科,汇点对应理科,容量为喜悦值。
最小割可以使一个点只能选一个科。
对于好朋友,只需新建节点 \(x\)\(x\) 向好朋友都连容量 inf 的边。
再把 \(x\) 向源点或汇点连起来即可,容量喜悦值。

P5934 [清华集训2012]最小生成树

我们模拟 kruskal 的过程,先排序。
先考虑最小生成树。
\(Lab\) 的边加入时候,需要加入这条边,那么需要 \(u\)\(v\) 两段不连通。
那么我们就可以最小割。源点为 \(u\),汇点为 \(v\)
最大树同理。
最后答案是两个答案加起来。

P2057 [SHOI2007] 善意的投票

源点对应 \(1\),汇点对应 \(0\)
每个小朋友先向他的主见建边,再向他的好朋友建边。
跑最小割。
如果割掉自己的边,表示改变意见。
如果割掉和朋友的边,表示保留意见。
容量都为 \(1\)

P2172 [国家集训队]部落战争

有向无环图,最小路径覆盖。
拆点,拆成 \(u,u'\),源点连 \(u\)\(u'\) 连汇点。
对于每一条边,对 \(u\)\(v'\) 建边。
最小路径覆盖是总点数减去最大流。
可理解为一开始都是自己是一个路径,每连一条边就合并两个路径。

P2764 最小路径覆盖问题

和上题一样,输出方案是类似这样 \(u\rightarrow v',v\rightarrow w'...\)

P2766 最长不下降子序列问题

Dp 先跑出第一个问题答案 \(fi=\max(f_j+1)(a_j<=a_i)\),设最长为 \(s\)
第二个问题是先把 \(f_i=f_j+1\) 的所有边建上。然后拆点限制流量为 \(1\)
源点连所有 \(f_i=1\)\(f_i=n\) 连汇点。
然后跑最大流。
第三个问题只需把 \(1,n\) 的流量改成无限即可。

P4016 负载平衡问题

费用流。
源点连把大于平均值的,小于平均值的连汇点。
相邻点建边,价格为 \(1\)

P4014 分配问题

带权二分图匹配。
费用流可以解决。
只要在工人连工作的边上加上价格即可。

P4013 数字梯形问题

费用流。
第一问把边的容量设为 \(1\),费用 \(0\),点拆点然后容量为 \(1\),附带上费用。
第二问把点容量改为 inf。
第三问把边容量改为 inf。

P1251 餐巾计划问题

源点向每天晚上连容量为脏餐巾的边。表示有多少餐巾要洗。
每天早上向汇点连容量为使用餐巾的边。表示够用。
每天晚上向明天晚上连容量为 inf 的边。表示留到以后再洗。
每天晚上向快洗/慢洗完成的早上连对应价格,容量 inf 的边。表示洗了。
源点向每天早上连价格为购买价,容量 inf 的边。表示要购买。
费用流。

P2053 [SCOI2007] 修车

我们发现工人修倒数第一辆车时,贡献了 \(T\) 的时间。
修倒数第二辆车,贡献 \(2T\) 的时间。
于是我们把工人拆成修倒数第几辆车的工人,向汇点连容量 \(1\) 的边,费用 \(0\)
源点连车,再把车连工人,带上费用。
费用流。
事实上是二分图带权最大匹配。

P2754 [CTSC1999]家园 / 星际转移问题

这题先按时间分层,枚举时间。
然后跑最大流。
这题不需要每个时间都重新建图,只需要在残量网格上加边即可。

P5029 T'ill It's Over

DS 优化建图,然后跑 \(1\sim t\) 最大流。

P4012 深海机器人问题

若路径上有标本,则建一条容量为 \(1\),带费用的边。
那么这样一个路只能经过一次了。
再建一条容量为 inf,费用 \(0\) 的边。
费用流。

P3410 拍照

最大闭合子图。
源点连正权点,容量是权值。
负权点连汇点,容量为权值的相反数。
然后把题目的边连上,容量 inf,费用 0.
最小割=(不选的正权之和+要选的负权绝对值之和)
最大权闭合子图=(正权之和-不选的正权之和-要选的负权绝对值之和)=正权值和-最小割
若正权点与源点连接,那么代表选了这个点进子图。
否则,没有选。

P2762 太空飞行计划问题

同上。
方案的输出只需输出所有联通源点的点。

P1674 [USACO05FEB] Secret Milking Machine G

二分+最大流。

P6061 [加油武汉]疫情调查

路径覆盖问题,只是有环。
这是与 P2764 最小路径覆盖问题 不同的地方。
那么我们还是拆点。
\(u'\)\(u\) 建边。
对于图中的边,我们对 \(u\)\(v'\) 建边,带费用。
我们再 \(u\)\(u'\)\(a_u\) 的边。
求一个二分图最大带权完美匹配。

P4251 [SCOI2015]小凸玩矩阵

二分加二分图匹配。

P2469 [SDOI2010]星际竞速

观察这是一个有向无环图。
是点覆盖问题。
先拆点,跑费用流即可。

P3227 [HNOI2013]切糕

“切糕”模型。
对于每个纵轴建 \(s\sim 1\sim 2\sim ...\sim r \sim t\) 的链,第 \(i\) 条边边权为 \(v_i\).
若割掉某一条边就表示纵轴的切割点是对应的点。
对于相邻纵轴的限制,我们这设两条链为 \(p,q\)
我们把所有 \(i-j=d\)\(p_i\rightarrow q_j\) 建有向边,\(q_i\rightarrow p_j\) 建有向边,权值无限。
那么这两条链若割了 \(i-j > d\) 的边,仍会有 \(s-t\) 联通。
最小割。

P3749 [六省联考 2017] 寿司餐厅

最大权闭合子图。
注意这里有个建图优化,不需要把 \(d_{i,j}\) 向所有点连边,
只需向 \(d_{i,j-1},d_{i+1,j}\) 建边即可。

P4553 80人环游世界

先拆点。
源点向每个点的入点连容量为 \(a_i\) 的边,费用 \(0\).
每个点出点向汇点容量连 \(a_i\) 的边,费用 \(0\).
图中的边按原有的费用,由出点连向入点。
再新建一个点,从源点连过来,容量为 \(m\),再把这个点连向每个点的入点。
费用流。

posted @ 2023-05-07 21:20  s1monG  阅读(22)  评论(1)    收藏  举报