网络流

一、网络流

1.1 概念

网络流的定义是对于一个带权的有向图集合G=(V,E),满足以下条件

  • 仅有一个入度为0的源点S
  • 仅有一个初度为0的汇点T
  • 每条边的权值(容量)均为非负数

网络流主要有两种类型——最大流和最小费用最大流

1.2 性质

对于任意一个时刻,设f(u,v)实际流量,则整个图G的流网络满足3个性质:

  • 容量限制:对任意u,v∈V,f(u,v)≤c(u,v)。
  • 反对称性:对任意u,v∈V,f(u,v) = -f(v,u)。从u到v的流量一定是从v到u的流量的相反值。
  • 流守恒性:对任意u,若u不为S或T,一定有∑f(u,v)=0,(u,v)∈E。即u到相邻节点的流量之和为0,因为流入u的流量和u点流出的流量相等,u点本身不会"制造"和"消耗"流量。

1.3 最大流最小割定理

对于网络流中的一些边组成的集合,如果这些边被删去后让整个网络不连通,那么这些边的集合就是一个割集
最小割:容量和最小的割集
最大流最小割定理:对于一个网络,其最大流等于最小割
证明方法就不在这里赘述了,证明思路就是证明
m a x f l o w ≤ m i n c u t 且 m a x f l o w ≥ m i n c u t maxflow\leq mincut 且maxflow\geq mincut maxflowmincutmaxflowmincut

二、最大流

2.1 思想

最大流算法的核心思想都是:建立反向边
为什么呢?
因为在我们发现这条路已经行不通的时候,我们可以通过反向边来把流量退回去
即使走到了本来不存在的反向边,我们也可以理解成把之前溜流过去的给退回来
所以,建立反向边是给了算法一个反悔的机会

最大流的算法通常有两种类型的解法:增广路算法和预流推进算法
但是因为本人特别的菜,这里只介绍比较简单的增广路算法

增广路算法主要有三种:FF, E d m o n d s Edmonds Edmonds_ K a r p Karp Karp d i n i c dinic dinic

2.2 FF算法

FF算法主要是通过dfs来找增广路,每次dfs找,然后把正向边流量减去每次找到的,反向边加上,但是复杂度很高,所以不建议大家去写

2.3 Edmonds_Karp算法

我们考虑在FF的基础上进行优化,如果我们用bfs去找增广路经的话,我们可以在某种程度上降低复杂度,所以EK算法就是bfs找增广

可以证明EK的复杂度是 O ( n m 2 ) O(nm^2) O(nm2)

奇丑无比的代码:

int n,m,s,t,ans;
int head[N],cnt=-1,pre[N],incf[N];
bool vis[N];
struct Edge{
	int to,next,w;
}e[N<<1];
inline void add(int x,int y,int c){
	e[++cnt]=(Edge){y,head[x],c},head[x]=cnt;
	e[++cnt]=(Edge){x,head[y],0},head[y]=cnt;
}

inline bool bfs(){
	mct(vis,0);
	queue<int> q;
	q.push(s),vis[s]=true;
	incf[s]=inf;
	while(!q.empty()){
		int u=q.front();
		q.pop();
		for(int i=head[u];~i;i=e[i].next){
			if(e[i].w){
				int v=e[i].to;
				if(vis[v])continue;
				incf[v]=min(incf[u],e[i].w);
				pre[v]=i;
				q.push(v);
				vis[v]=true;
				if(v==t) return true;
			}
		}
	}
	return false;
}

inline void EK(){
	
	int x=t;
	while (x!=s){
		int k=pre[x];
		e[k].w-=incf[t];
		e[k^1].w+=incf[t];
//		printf("%d %d %d\n",x,k,e[k^1].to);
		x=e[k^1].to;
//		printf("%d %d %d\n",x,k,e[k^1].to);
	}
	ans+=incf[t];
}

int main()
{
	mct(head,-1);
	n=read(),m=read(),s=read(),t=read();
	Rep(i,1,m){
		int u,v,w;
		u=read(),v=read(),w=read();
		add(u,v,w);
	}
	while(bfs())EK();
	printf("%d\n",ans);
	return 0;
}

大家要注意网络流存图的时候要从2开始存,因为我们调用他的反向边时运用的是 x o r xor xor 1的操作

2.4 dinic算法

2.4.1 dinic

我们发现,EK每次bfs只能够寻找一条增广路,这也拖慢了时间效率,所以我们考虑使用bfs分层,然后dfs增广,这样每次分层可以找出多条增广路,于是, d i n i c dinic dinic算法诞生了

可以证明 d i n i c dinic dinic在求解一般网络时复杂度为 O ( n 2 m ) O(n^2m) O(n2m)——大部分时候比EK快

int n,m,s,t,ans;
int head[N],cnt=1;
int depth[N];

struct Edge{
	int to,next,w;	
}e[N];

inline void add(int x,int y,int c){
	e[++cnt]=(Edge){y,head[x],c},head[x]=cnt;
	e[++cnt]=(Edge){x,head[y],0},head[y]=cnt;	
}

inline bool bfs(){
	memset(depth,0,sizeof(depth));
	queue<int> q;
	depth[s]=1;
	q.push(s);
	while(!q.empty()){
		int u=q.front();q.pop();
		for(int i=head[u];~i;i=e[i].next){
			int v=e[i].to;
			if(e[i].w&&!depth[v]){
				depth[v]=depth[u]+1;
				q.push(v);
				if(v==t)return true;
			}
		}
	}
	return false;
}

int dfs(int u,int flow){
	if(u==t)return flow;
	int rest=flow,k;
	for(int i=head[u];~i&&rest;i=e[i].next){
		int v=e[i].to;
		if(e[i].w&&depth[v]==depth[u]+1){
			k=dfs(v,min(e[i].w,rest));
			if(!k)depth[v]=0;
			e[i].w-=k;
			e[i^1].w+=k;
			rest-=k;	
		}
	}
	return flow-rest;
}

inline void dinic(){
	int tmp;
	while(bfs())
		while(tmp=dfs(s,1e9))ans+=tmp;	
}

int main()
{
	memset(head,-1,sizeof(head));
	read(n),read(m),read(s),read(t);
	Rep(i,1,m){
		int x,y,c;
		read(x),read(y),read(c);
		add(x,y,c);
	}
	dinic();
	printf("%d\n",ans);
	return 0;
}

2.4.2 当前弧优化

对于每一个点,都记录上一次检查到哪一条边。因为我们每次增广一定是彻底增广(即这条已经被增广过的边已经发挥出了它全部的潜力,不可能再被增广了),下一次就不必再检查它,而直接看第一个未被检查的边。

优化之后渐进时间复杂度没有改变,但是实际上能快不少。 实际写代码的时候要注意,head数组初始值为-1,存储时从0开始存储,这样在后面写反向弧的时候比较方便,直接异或即可。 关于复制head的数组cur;目的是为了当前弧优化。已经增广的边就不需要再走了.

2.4.3 dinic与二分图

2.4.3.1 二分图

对于无向图G=(V,E),如果顶点V可分割为两个互不相交的子集 ( A , B ) (A,B) (A,B),并且图中的每条边 ( i , j ) (i,j) (i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集 ( i ∈ A , j ∈ B ) (i \in A,j \in B) (iA,jB),则称图G为一个二分图。

2.4.3.2 二分图的判定

二分图可以通过染色来判定,我们将一个图进行黑白染色,根据二分图的定义可知如果相邻两点同色,则该图不是二分图

在NOIP2008中出了一道题叫双栈排序,就可以用判定二分图的方法来做

2.4.3.3 二分图匹配

二分图匹配是指对于被分开的两个点集A,B,对于A中的每一个点,匹配B中的一个点并且没有矛盾,问A中最多有几个点可以匹配上

有一种非常暴力的做法叫做匈牙利算法,每次dfs寻找匹配,复杂度为 O ( n m ) O(nm) O(nm),在这里就不过多的讲了

2.4.3.4 dinic求二分图匹配

我们发现,如果我们从源点往左半边点都连一个容量为1的边,所有原图的点也都连一个容量为1的边,把右半边点都和汇点连一条边,那么整个图就变成了了一个网络,跑最大流就是答案

可以证明 d i n i c dinic dinic算法求二分图匹配的复杂度是 O ( n m ) O(n\sqrt m) O(nm )

2.5 最大流经典题型

2.5.1 拆点转化成n分图匹配

飞行员配对方案问题 裸二分图匹配+输出路径
方格取数问题 奇偶性分组转化为二分图匹配
试题库问题 稍微转化的二分图匹配+输出路径
最小路径覆盖问题 告诉你怎么做的二分图匹配+输出路径
教辅的组成 三分图匹配
[SHOI2001]小狗散步 二分图匹配+输出路径

n分图输出路径方法

在n分图中,判断一条边是否有流量即为判断其反向边流量是否不为0

2.5.2 分层图最大流问题

[CTSC1999]家园分层图最大流据说是非正解

2.5.3 利用残量网络的最大流问题

最长不下降子序列问题利用残量网络继续加边跑最大流

** 其实你会发现,通常最大流的考题都是二分图**

三、最小费用最大流

3.1 最小费用最大流

最小费用最大流,又称费用流,就是在最大流的基础上,每条边多了一个单位费用,流过该边的每单位流量都要支付相应的代价,让你求在最大流的情况下花费的最小代价

3.2 MCMF算法

MCMF(min cost max flow)算法,是在EK的基础上,每次使用spfa去增广,寻找一条最便宜的增广路,其他的和EK基本一样

个人认为费用流比最大流好理解

int n,m,s,t,maxflow,mincost;
int head[N],cnt=1;
int dis[N],flow[N],pre[N];
bool inq[N];

struct Edge{
	int to,next,w,c;	
}e[N<<1];

inline void add(int x,int y,int w,int c){
	e[++cnt]=(Edge){y,head[x],w,c},head[x]=cnt;
	e[++cnt]=(Edge){x,head[y],0,-c},head[y]=cnt;	
}

inline bool spfa(){
	memset(dis,0x3f,sizeof(dis));
	memset(flow,0x3f,sizeof(flow));
	queue<int> q;
	q.push(s);
	dis[s]=0;
	flow[s]=1e9;
	inq[s]=true;
	while(!q.empty()){
		int u=q.front();q.pop();
		inq[u]=false;
		for(int i=head[u];~i;i=e[i].next){
			int v=e[i].to;
			if(e[i].w&&dis[v]>dis[u]+e[i].c){
				dis[v]=dis[u]+e[i].c;
				pre[v]=i;
				flow[v]=min(flow[u],e[i].w);
				if(!inq[v])inq[v]=true,q.push(v);
			}
		}
	}
	return dis[t]<1e9;
}

inline void mcmf(){
	while(spfa()){
		int u=t;
		while(u!=s){
			e[pre[u]].w-=flow[t];
			e[pre[u]^1].w+=flow[t];
			u=e[pre[u]^1].to;
		}
		maxflow+=flow[t];
		mincost+=flow[t]*dis[t];
	}
}

int main()
{
	memset(head,-1,sizeof(head));
	read(n),read(m),read(s),read(t);
	Rep(i,1,m){
		int x,y,w,c;
		read(x),read(y),read(w),read(c);
		add(x,y,w,c);
	}
	mcmf();
	printf("%d %d\n",maxflow,mincost);
	return 0;
}

3.3 费用流经典题型

通常我们把费用流的问题转化为
源->点 f l o w = 1 , c o s t = 0 flow=1,cost=0 flow=1,cost=0
点->点 f l o w = x , c o s t = c ( u , v ) flow=x,cost=c(u,v) flow=x,cost=c(u,v)
点->汇 f l o w = 1 , c o s t = 0 flow=1,cost=0 flow=1,cost=0
也是转化为二分图模型,只是多了一位费用
[SDOI2009]晨跑拆点转化为二分图
[SCOI2007]修车暴力连边转化为满二分图
[ZJOI2010]网络扩容利用跑完最大流的残量网络继续跑费用流
[NOI2008]志愿者招聘玄学连边

四、写在最后

4.1 关于时间复杂度

网络流的时间复杂度虽然看着很高,但是往往跑不到,比如按 d i n i c dinic dinic O ( n 2 m ) O(n^2m) O(n2m) l u o g u luogu luogu板子题都过不去

所以大家不要被复杂度吓到了——虽然可能真的过不去

4.2 关于题目来源

这里引用的是线性规划与网络流24题上的一些题目以及 l u o g u luogu luogu省选试炼场最大流,网络流,二分图三关里的一些题目,推荐大家去写一写网络流24题,质量都很高

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值