最大网络流基本概念--EK,dinic

1. 基本概念

1.1 流网络,不考虑反向边


如果存在反向边也没事,不如有u->v和v->u两条边,那么就可以新加入一个点 p,u->v,v->p,p->u,转化为这三条边

1.2 可行流,不考虑反向边

1.2.1 两个条件:容量限制、流量守恒

容量限制:每条边流的不能超过这条边的权值
流量守恒:每个点流入的流量等于流出去的流量()

1.2.2 可行流的流量指从源点流出的流量 - 流入源点的流量

1.2.3 最大流是指最大可行流

1.2.4 一个可行流的实例图:(红色表示这条边上的实际流,蓝色表示这条边的容量)

1.3 残留网络,考虑反向边,残留网络的可行流f' + 原图的可行流f = 原题的另一个可行流

(1) |f' + f| = |f'| + |f|

(2) |f'| 可能是负数

残留网络实例,下图为上图的残留网络,原图为f,残留网络记为G[f]

正向边为容量-原图流量,反向边为原图流量

1.4 增广路径

在残留网络从,从源点开始沿着大于0的边走,如果能到达终点,那么就是一条增光路径,如下图所示的红色路径

重要: 如果一个可行流的残留网络没有增广路径,那么这个可行流就是一个最大流

1.5 割

1.5.1 割的定义

一个图G=(V,E),然后把点划分成两个集合S和T,并且保证这两个集合是不重不漏的,并且源点s∈S,汇点t∈T

1.5.2 割的容量,不考虑反向边,“最小割”是指容量最小的割。

所有S连向T的边的容量的和,称为割的容量,记为c(S,T)

1.5.3 割的流量,考虑反向边,f(S, T) <= c(S, T)

割的流量=S流到T的流量-T流到S的容量

1.5.4 对于任意可行流f,任意割[S, T],|f| = f(S, T),即割的流量等于可行流的流量

1.5.5 对于任意可行流f,任意割[S, T],|f| <= c(S, T)

1.5.6 最大流最小割定理,以下三个条件都是等价的

(1) 可行流f是最大流

(2) 可行流f的残留网络中不存在增广路

(3) 存在某个割[S, T],|f| = c(S, T)

1.6. 算法

1.6.1 EK O(nm^2) -- 最大流所有算法中最简单的实现,经验是可以处理点数+边数处于[1000,10000]的网络范围

原理就是不断维护残留网络,即不断的寻找增广路径,然后把这条增广路径给并到可行流内,不断重复这个过程,直到不存在残留网络

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1010,M=20010,INF=1e15;

int n,m,S,T;
int h[N],e[M],f[M],ne[M],idx;//f数组表示容量
int q[N],d[N],pre[N];
//q表示宽搜的队友,d[i]表示从起点走到i,当前所有边的容量最小值,pre[i]用来记录路径
bool st[N];

/*
这边正向边的编号与反向边的编号做了一个巧妙的变换
比如正向边编号为t,反向边编号为s
那么t^1=s;
0 1 0是正向边,1是反向边
2 3 
4 5
依此类推赋值边的编号
*/
void add(int a,int b,int c){
	e[idx]=b,f[idx]=c,ne[idx]=h[a],h[a]=idx++;//正向边
	e[idx]=a,f[idx]=0,ne[idx]=h[b],h[b]=idx++;//反向边,一开始残留网络的反向边的权值为0
}

bool bfs(){
	int hh=0,tt=0;
	memset(st,false,sizeof st);
	q[0]=S,st[S]=true,d[S]=INF;
	
	while(hh<=tt){
		int t=q[hh++];
		for(int i=h[t];~i;i=ne[i]){
			int ver=e[i];
			if(!st[ver]&&f[i]){//如果当前的点没被遍历过,并且连向的边不为0的话
				st[ver]=true;
				d[ver]=min(d[t],f[i]);
				pre[ver]=i;//记录前驱边的编号,然后就可以获得完整的路径信息,方便对残留网络进行更新
				if(ver==T)return true;
				q[++tt]=ver;
			}
		}
	}
	return false;
}

int EK(){
	int ans=0;//答案
	while(bfs()){
		ans+=d[T];
		for(int i=T;i!=S;i=e[pre[i]^1]){
			f[pre[i]]-=d[T],f[pre[i]^1]+=d[T];
			//pre[i]的反向边编号是pre[i]^1,所以e[pre[i]^1]的点就是该点在记录路径上的上一个点
		}
	}
	return ans;
}

void slove(){
	cin>>n>>m>>S>>T;
	memset(h,-1,sizeof h);
	
	for(int i=0;i<m;i++){
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c); 
	}
	
	cout<<EK()<<endl;
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
	while(T--) slove();
}

1.6.2 Dinic O(n^2m) -- 经验是可以处理点数+边数处于[10000,100000]的网络范围

一个图可能不止一条增广路,dinic的思想就是用dfs爆搜出多条增广路,一次增广多条路径,大大提高增广的效率
然后为了防止出现环而影响判断,引入一个分层图的概念,每个点的层数是到起点的最短距离,然后路径设置是只能从层数低跳到层数高的点,这样就能抑制环的出现,提高效率
然后就是把所有这个分层图能搜索到的增广路径,全部统一增广一遍
步骤:
1.bfs一遍-->建立分层图
2.dfs 找出所有能够增广的路径

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=10010,M=200010,INF=1e15;//根据边的大小,来调整N,M,INF

int n,m,S,T;
int h[N],e[M],f[M],ne[M],idx;//f数组表示容量
int q[N],d[N],cur[N];
//q表示宽搜的队列,d[i]表示i这个点的层数,cur是当前弧优化
/*
当前弧优化的意思:
每次遍历一个点的邻点的时候,都会按照固定的顺序去遍历每条边
然后dfs的过程就是去枚举每条路径,然后把这条路径的容量减去当前路径流量的最小值
然后每次dfs完之后,每条边的流量都会加上一个值,而一旦这条边的容量满了,那么就没有
继续遍历这条边的必要,所以后面遍历的时候,就可以直接跳过这条边
所以一开始每个点的cur[i]=h[i],当f[h[i]]这条边满的时候,也就没继续遍历的必要,cur[i]=ne[h[i]]
*/
void add(int a,int b,int c){
	e[idx]=b,f[idx]=c,ne[idx]=h[a],h[a]=idx++;//正向边
	e[idx]=a,f[idx]=0,ne[idx]=h[b],h[b]=idx++;//反向边,一开始残留网络的反向边的权值为0
}

bool bfs(){//规划分层图,然后判断是否存在增广路
	int hh=0,tt=0;
	memset(d,-1,sizeof d);
	q[0]=S,d[S]=0,cur[S]=h[S];//起点的层数为0
	while(hh<=tt){
		int t=q[hh++];
		for(int i=h[t];~i;i=ne[i]){
			int ver=e[i];
			if(d[ver]==-1&&f[i]){//只有这条边有流量的时候,才能继续走下去
				d[ver]=d[t]+1;
				cur[ver]=h[ver];
				if(ver==T)return true;//如果能搜到T的话,那么就说明可以找到一条增广路
				q[++tt]=ver;
			}
		}
	}
	return false;
}

int find(int u,int limit){//limit表示从源点走到u这个点,能流的最大的流量
	if(u==T) return limit;
	int flow=0;//flow表示从u这个点开始,往后流的最多的流量是多少
	//flow一定是要小于等于limit的,这个毋庸置疑的,当flow=limit的时候,说明u这个点往外已经把流量全部流完了
	for(int i=cur[u];~i&&flow<limit;i=ne[i]){//当前弧优化
		 cur[u]=i;//i前面的边都流完了
		 int ver=e[i];
		 if(d[ver]==d[u]+1&&f[i]){
		 	int t=find(ver,min(f[i],limit-flow));
		 	if(!t) d[ver]=-1;//这个点出发到终点是没有流量大于0的路径的,那么就可以删掉了
		 	f[i]-=t,f[i^1]+=t,flow+=t;
		 }
	}
	return flow;
}


int dinic(){
	int ans=0,flow;
	while(bfs()) while(flow=find(S,INF)) ans+=flow;
	return ans;
}

void slove(){
	cin>>n>>m>>S>>T;
	memset(h,-1,sizeof h);
	
	for(int i=0;i<m;i++){
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c); 
	}
	
	cout<<dinic()<<endl;
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
	while(T--) slove();
}

1.7 应用

1.7.1 二分图

(1) 二分图匹配

(2) 二分图多重匹配

1.7.2 上下界网络流

(1) 无源汇上下界可行流

(2) 有源汇上下界最大流

(3) 有源汇上下界最小流

1.7.3 多源汇最大流

posted @ 2024-12-16 22:18  MENDAXZ  阅读(22)  评论(0编辑  收藏  举报