Loading

【笔记】二分图/网络流

来自\(\texttt{SharpnessV}\)省选复习计划中的二分图/网络流


【模板】二分图最大匹配

给定一个二分图,需要找出最多的不相交的边。

比较简单的方法是匈牙利算法,每次找增广路然后直接增广即可。时间复杂度是\(\rm O(NM)\)

#include<cstdio>
#include<cstring>
using namespace std;
struct edge{
	int next;
	int to;
}e[1000000];
int n,m,k,h[5005],to[5005],pop=0;
int visit[5005];
void add(int x,int y){
	pop++;
	e[pop].next=h[x];
	e[pop].to=y;
	h[x]=pop;
}
bool find(int p){
	for(int i=h[p];i;i=e[i].next){
		if(visit[e[i].to])continue;
		visit[e[i].to]=1;
		if(!to[e[i].to]||find(to[e[i].to])){
		  to[e[i].to]=p;
		  return true;
		}
	}
	return false;
}
int main()
{
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=k;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		if(x<=n&&y<=m)add(x,y);
	}
	int ans=0;
	for(int i=n;i>=1;i--){
		memset(visit,0,sizeof(visit));
		if(find(i))ans++;
	}
	printf("%d\n",ans);
	return 0;
}

复杂一点的方法是直接建图跑网络流,\(\texttt{Dinic}\) 跑二分图的时间复杂度是 \(\rm O(N\sqrt{M})\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 1005
#define M 100005
using namespace std;
int n,m,k,h[N],tot=1;
struct edge{
	int to,nxt,cap;
}e[M<<1];
void add(int x,int y,int z){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].cap=z;e[tot].to=y;
}
int s,t,d[N],cur[N];
queue<int>q;
bool bfs(){
	memset(d,0,sizeof(d));
	d[s]=1;q.push(s);
	while(!q.empty()){
		int x=q.front();q.pop();
		cur[x]=h[x];
		for(int i=h[x];i;i=e[i].nxt)if(e[i].cap&&!d[e[i].to])
			d[e[i].to]=d[x]+1,q.push(e[i].to);
	}
	return d[t];
}
int dfs(int x,int flow){
	if(x==t)return flow;
	int res=flow;
	for(int &i=cur[x];i;i=e[i].nxt)
		if(res&&e[i].cap&&d[x]+1==d[e[i].to]){
			int now=dfs(e[i].to,min(res,e[i].cap));
			if(!now)d[e[i].to]=0;
			e[i].cap-=now;
			e[i^1].cap+=now;
			res-=now;
		}
	return flow-res;
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	s=n+m+1;t=n+m+2;
	rep(i,1,n)add(s,i,1),add(i,s,0);
	rep(i,1,m)add(n+i,t,1),add(t,n+i,0);
	rep(i,1,k){
		int x,y;scanf("%d%d",&x,&y);
		add(x,n+y,1);add(n+y,x,0);
	}
	int ans=0;
	while(bfs())ans+=dfs(s,0x7fffffff);
	printf("%d\n",ans);
	return 0;
} 

【模板】网络最大流

仍然是每次找增广路然后增广。每次先 \(\texttt{BFS}\) 出每个节点的层数,然后在分层图上增广。因为分了层,所以可以在找到路径的同时增广。

时间复杂度是\(\rm O(N^2M)\),一般卡不满。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 205
#define M 10005
#define int long long 
using namespace std;
int n,m,s,t,h[N],tot=1;
struct edge{
	int to,nxt,cap;
}e[M];
void add(int x,int y,int z){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].cap=z;
	e[++tot].nxt=h[y];h[y]=tot;e[tot].to=x;e[tot].cap=0;
}
int d[N],cur[N];queue<int>q;
bool bfs(){
	memset(d,0,sizeof(d));
	d[s]=1;q.push(s);
	while(!q.empty()){
		int x=q.front();q.pop();
		cur[x]=h[x];
		for(int i=h[x];i;i=e[i].nxt)if(e[i].cap&&!d[e[i].to])
			d[e[i].to]=d[x]+1,q.push(e[i].to);
	}
	return d[t]>0;
}
int dfs(int x,int flow){
	if(x==t)return flow;
	int res=flow;
	for(int &i=cur[x];i;i=e[i].nxt){
		if(d[x]+1==d[e[i].to]&&e[i].cap){
			int now=dfs(e[i].to,min(res,e[i].cap));
			if(!now){d[e[i].to]=0;}
			e[i].cap-=now;e[i^1].cap+=now;res-=now;
		}
		if(!res)return flow;
	}
	return flow-res;
}
signed main(){
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	rep(i,1,m){
		int x,y,z;scanf("%lld%lld%lld",&x,&y,&z);
		add(x,y,z);
	}
	long long ans=0;
	while(bfs())ans+=dfs(s,0x7fffffffffffffffLL);
	printf("%lld\n",ans);
	return 0;
}

【模板】最小费用最大流

由于要使得费用最小,所以我们每次找费用最小的增广路,这样就不能再使用\(\texttt{Dinic}\)算法。

退一步,我们用 \(\texttt{EK}\) 算法,用 \(\texttt{SPFA}\) 找费用最小的增广路,然后增广。

时间复杂度能过。一般最大流不卡 \(\texttt{Dinic}\) ,费用流不卡 \(\texttt{EK}\),如果卡了喷出题人就完事了。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 5005
#define M 100005
using namespace std;
int n,m,s,t,h[N],tot=1;
struct edge{
	int to,nxt,cap,val;
}e[M];
void add(int x,int y,int z,int val){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].cap=z;e[tot].val=val;
	e[++tot].nxt=h[y];h[y]=tot;e[tot].to=x;e[tot].cap=0;e[tot].val=-val;
}
queue<int>q;
int d[N],pre[N],ff[N];bool v[N];
bool spfa(){
	memset(d,0x3f,sizeof(d));
	memset(v,0,sizeof(v));
	memset(ff,0,sizeof(ff));
	q.push(s);d[s]=0;ff[s]=0x7fffffff;
	while(!q.empty()){
		int x=q.front();q.pop();v[x]=0;
		for(int i=h[x];i;i=e[i].nxt)if(e[i].cap&&e[i].val+d[x]<d[e[i].to]){
			d[e[i].to]=d[x]+e[i].val,pre[e[i].to]=i^1,ff[e[i].to]=min(ff[x],e[i].cap);
			if(!v[e[i].to])v[e[i].to]=1,q.push(e[i].to);
		}
	}
	if(d[t]<0x3f3f3f3f)return true;return false;
}
int flow,ans;
void updata(){
	flow+=ff[t];ans+=ff[t]*d[t];
	int now=t;while(now!=s){e[pre[now]].cap+=ff[t],e[pre[now]^1].cap-=ff[t];now=e[pre[now]].to;}
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	rep(i,1,m){
		int x,y,z,op;scanf("%d%d%d%d",&x,&y,&z,&op);
		add(x,y,z,op);
	}
	while(spfa())updata();
	printf("%d %d\n",flow,ans);
	return 0;
}

以下是正文。

Part 1: 网络流24

Link

P2756 飞行员配对方案问题

二分图模板,两种飞行员相互连边即可。

代码

P4016 负载平衡问题

费用流模板,相邻点连费用为 \(1\) 的边。

代码

P1251 餐巾计划问题

拆点,将每天拆成两个点,分别表示这天的新餐巾和这天的旧餐巾。

源点向旧餐巾连容量 \(r_i\),费用为 \(0\) 表示每天产生的旧餐巾。

源点向新餐巾连容量 \(r_i\),费用为 \(p\) 表示购买的新餐巾。

旧餐巾向 \(m/n\) 天后的新餐巾连容量无限,费用为\(f/s\)表示洗餐巾。

新餐巾向汇点连容量 \(r_i\),费用为 \(0\) 表示每天需要的餐巾。

\(i\) 天的旧餐巾向第 \(i+1\) 天的旧餐巾连容量无限,费用 \(0\) 的边表示今天的餐巾可以拖到明天。

最后跑费用流即可。

代码

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

按时间建立分层图,然后跑最大流。

代码

P2762 太空飞行计划问题

最小割即最大流,证明略。

对于本题,源点向实验连容量为利润的边,器材向汇点连容量为费用的边,相关的实验和器材之间连 \(\inf\) 的边。

当我们割掉一条边,意味着放弃实验/购买器材。如果存在一条由源点到汇点的路径,意味着有一个实验没有放弃,但是器材仍没有购买。所以我们要花费最小的代价使得图不连通,直接跑最小费用最大流。

代码

P2763 试题库问题

简单网络流建模,但是要输出方案。

由于最大流等于最小割,所以被流满的边就是割集中的边,就是我们选的试题。

代码

P2764 最小路径覆盖问题

拆点,原 DAG 上一个点拆为入点和出点,原图的边 \(u\to v\),转换为 \(u_{out}\to v_{in}\),源点向入点连边,出点向汇点连边,割掉的一条边表示合并原来的两条路径。

不是很难理解,最后输出方案需要用到并查集。

代码

P2766 最长不下降子序列问题

拆点,对于在点上的限制,例如限制一个点的选取次数,我们可以将点拆为两个点,然后在点之间连边。

本题拆点,然后对于\(f[i]+1=f[j]\)的转移,在\(i\)的出点和\(j\)的入点间连边。

代码

P3355 骑士共存问题

建模不难,这是个二分图最大独立集。

二分图中:最大独立集 \(=\) 点数 \(-\) 最小点覆盖,最小点覆盖 \(=\) 最大匹配 \(=\) 最小割 \(=\) 最大流 。

代码

Part 2:省选

P2423 [HEOI2012]朋友圈

显然补图的最大独立集等于原图的最大团,补图的最大团等于原图的最大独立集。

二分图的最大独立集等于点数减去最小覆盖,最小覆盖等于最大匹配。

代码

P2825 [HEOI2016/TJOI2016]游戏

如果没有墙,就是经典的行列模型,直接上二分图。

既然有墙,我们仍然可以看作行列模型。只不过如果有墙阻挡,就把原来的行/列拆成多段,然后二分图匹配即可。

代码

P3731 [HAOI2017]新型城市化

求二分图最大匹配必经边。

我们可以先跑网络流,得到残余网络。

残余网络包括很多信息,比如退流的信息。

那么必经边的两段在原图上必定不强连通,因为如果强连通,则必然包含一个环,我们可以将环上的一条边退流,从环的其余部分增广。

所以我们再在残余网络上跑一边\(\texttt{Tarjan}\)算法即可。

代码

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

经典模型:最大权闭合子图。

给定若干个物品,每个物品有一个价值,以及一些限制条件\(u\to v\)表示选了物品\(u\)就必须选物品\(v\),求最大价值和。

这个简单:我全部选

价值可以为负。

我们可以将模型转换为最小割模型,对于每个物品,如果点权大于\(0\),与\(S\)连边,否则与\(T\)连边,容量为价值的绝对值。

对于一个条件\(u\to v\),从 \(u\)\(v\) 连容量为 \(\inf\) 的边。

最后跑最大流最小割即可。

分析一下,对于每个物品,如果割掉连 \(S\) 的边,表示不选它,割掉和\(T\)的边,表示选它。

那么如果存在一条通路\(S\to a\to b\to T\),表示没有选择了\(a\),而没有选择\(b\)。最小割可以使得网络中不存在通路。

代码

P2805 [NOI2009] 植物大战僵尸

同样是最大权闭合子图,难度低于上面的题,留给思考。

代码

P2053 [SCOI2007]修车

很好的思维题。

拆点,对于每个师傅,我们拆乘\(N\)个点,第\(i\)个点表示是倒数第\(i\)个来修车的。

对于一辆车,如果它是倒数第\(i\)个修的,那么它的修车时间要算\(i\)次。所以费用为修车时间\(\times i\) ,容量为\(1\),拆出的每个点连向汇点的边容量为\(1\),表示一个师傅一个时间只能修一个车。

代码

P4043 [AHOI2014/JSOI2014]支线剧情

上下界最小费用可行流。

对于每条必须边,先把它流满。为使整张网络的流量守恒,我们再建立超级源点和超级汇点进行补流操作。

我们对每个点计算 \(d[i]\) 表示将必经边流满后第\(i\)个节点入流和出流的差。

如果\(d[i]>0\)说明供大于求,我们从超级源点向\(i\)连一条容量为\(d[i]\)的边,表示还需要吐出这么多流。

如果\(d[i]<0\)说明供不应求,我们从\(i\)向超级汇点连一条容量为\(-d[i]\)的边,表示还需要吞掉这么多流。

我们还要连\(t\to s\)的容量为\(\inf\)的边,表示整张图的入流和出流平衡。

代码

上下界网络流还有一系列,但本质上相同。

posted @ 2021-12-16 22:35  7KByte  阅读(109)  评论(0编辑  收藏  举报