【学习笔记:网络流入门算法】

先挂友链:
传送门一
传送门二
传送门三

好的,最近开始学习网络流了,所以博主还是决定写写博客来理一下思路整理一下知识点。

在一张有向图里面,首先由一个源点和一个汇点,然后你可以通过建模或者题目直接给出而得到图中每一条边的信息:起点,终点,容量。容量指的是该边的最大流量(容量为非负数)。而图中的每一个点是没有流量限制的。
表示:图G=(v,e,c)
所以我们可以得到网络流图中的一些简单性质

  1. 每一条边的流量一定小于等于该边的容量。
  2. 每一个点的流入量一定等于流出量。
  3. 源点的流入量一定等于汇点的流出量,源点的入度和汇点的出度都是0。

最大流问题:
对于一张网络流图,显然它的最大流量是受到限制的,限制它的就是每一条边的容量。
在讲最大流的具体算法之前,我们先讲一讲几个概念。
一些概念常见的表示方法:
1.边(u,v)的容量:c(u,v);
2.边(u,v) 的流量:f(u,v);
增广路:学过二分图的同学们一定对这个不陌生,但是,增广路的定义并不仅限于二分图里的匈牙利算法,应该说,只要在原图的基础上能找到一条新的路使得答案更优,那么这就是一条增广路。
残量网络:一些边已经消耗掉了一部分容量了,而当前情况下所有的残边与它们的剩余流量就构成了一张流量的残图,即残量网络。
可行流:在网络流的图中,往往是残量网络中找到了一条路径,其中每一条边的流量都还没有达到容量,这条路径就是一条可行流,通俗一点儿就是这条路还没有堵车,还能有车可以走。
饱和弧:网络中f(u,v)=c(u,v)的弧;
非饱和弧:显然啊;
零流弧:网络中f(i,j)等于0的弧;
非零弧:更显然啊;
下一个概念非常重要,请大家务必记住,因为这很可能决定了您在下面的博文中是否会一脸懵逼~~(听不懂的话会万脸懵逼)~~
后向弧:在建网络流图时,对于每一条边,我们都要建立一条与原边反向的、初始容量为0的边,称为反向弧,而且在之后,反向弧的容量会一直随对应的正向弧的流量改变而改变,二者是恒等于的关系。至于为什么,我们之后会讲,请大家先记住这一点。
最大流的算法常见的有EK,dinic,都是基于增广路的概念的(貌似SAP也有时会用)。
不过由于 dinic最好理解也最好写 博主只会dinic,我们就只介绍dinic算法吧。
我们已经清楚地了解到,增广路算法由两部分组成,首先是在残量网络中计算可增加的流量,第二步是更新流量,其实如果直接在原图上这样跑是错的,因为你之前的方案并不是最优解的一部分,然而它却会占用最优解的容量,也就是说可能会阻塞最优解。
然而,在这个算法加上一个反向弧就成了完美的了。上面再介绍反向弧的定义时已经说了反向弧的容量需要维护,使得它的容量恒等于它对应的正向弧的流量。相当于是我们一开始让正向弧e[u,v]流量为F,由于这样可能不是最优解的一部分,然而如果最优解的f[u,v]更大 那么在继续增广的时候一定会考虑到;如果最优解的f[u,v]更小,那么往海里算撑死也就是流量为0,相当于就是最多有F倒流回去了,也就是说在u,v之间可能存在倒流的情况,且倒流量的范围为[0,F],等效于是从v向u连了一条容量为F的边。
而且仔细分析的话就不难理解到,反向弧的性质与正向弧没有任何区别,他们的地位也是平等的,也就是说,当反向弧的流量改变时,正向弧也得跟着变。这就有点像是哈希,反正就是正向弧就是一对CP一个变了另一个也得跟着变。那么聪明的你就要问了,既然建图是正向弧与反向弧是没有区别的,那么我修改一条边的剩余容量时如何维护与它相反的另一条边呢???这里就要用到神奇的位运算了!
来一道幼儿园数学找规律!
2 2 2^ 1 = = 3 1==3 1==3
3 3 3^ 1 = = 2 1==2 1==2
4 4 4^ 1 = = 5 1==5 1==5
5 5 5^ 1 = = 4 1==4 1==4
6 6 6^ 1 = = 7 1==7 1==7
7 7 7^ 1 = = 6 1==6 1==6


聪明的你知道了吧!如果我们像这样把正向弧和反向弧存起来,就像把它们捆在了一起,一荣俱荣,一损俱损,可视为双向的hash(又是一个胞间连丝????)所以正是由于^运算的这个神奇性质,网络流在读优写优后面定义全局变量时,用来做前向星链表建图的cnt一定是初始化为1的,而且很多大佬们都选择把建正向弧和反向弧的操作同时写进一个函数里面,要么在add时直接改,要么另外开一个insert函数。
小结1
最大流dinic算法:

  1. 建图,记得cnt初始为1,加上反向弧(其实真的这是最难的)。
  2. 通过广搜找到一条最短的增广路,这里的最短指的是该路径经过的节点最少而与边容量毫无关系,然后深搜流量可变化多少将ans加上该值,并修改各边的剩余容量来维护残量网络。
  3. 一直重复第二步,直到残量网络中再也找不到任何一条增广路为止。
    复杂度分析:由于最坏的情况下,每一次bfs广搜找增广路最多执行n次,每一次dfs就是跑一遍全图的mn所以复杂度是n^2*m的,然而zxyoi神仙在吊打博主的时候曾偶然提到说,至今没有找到一张图能把广搜的那个n卡满。。。
    代码:
#include<bits/stdc++.h>
using namespace std;
inline int read(){
	char ch;int flag=1;
	while((ch=getchar())<'0'||ch>'9') if(ch=='-') flag=-1;
	int ans=ch-48;
	while((ch=getchar())>='0'&&ch<='9') ans=ans*10+ch-48;
	return ans;
}
inline void write(int x){
	if(x<0) x=-x,putchar('-');
	if(x>9) write(x/10);
	putchar(x%10+'0');return;
}
const int M=4000001;
const int INF=0x7fffffff;

int n,m,s,t,ans=0;
int cnt=1,head[1000001],dis[1000001],cur[1000001];
//cnt初始值为1
struct node{
	int u,v,c,nt;
}e[M<<1];//注意,由于会有反向弧的存在,所以边集要开大一倍
inline void add(int u,int v,int c){
	cnt++;
	e[cnt]=(node){u,v,c,head[u]};
	head[u]=cnt;
}
inline bool bfs(){
	memset(dis+1,-1,n<<2);
	queue<int>q;
	dis[s]=0;q.push(s);
	while(!q.empty()){
		int u=q.front();q.pop();
		for(int i=head[u];i;i=e[i].nt){
			int v=e[i].v;
			if(dis[v]==-1&&e[i].c>0){
				dis[v]=dis[u]+1;
				q.push(v);
				if(v==t) return 1;
			}
		}
	}
	return 0;
}
inline int dfs(int x,int f){
	if(x==t||f==0) return f;
	int used=0;
	for(int &i=cur[x];i;i=e[i].nt){
		int v=e[i].v;
		if(e[i].c&&dis[v]==dis[x]+1){
			int w=dfs(v,min(f,e[i].c));
			if(!w) continue;
			used+=w;f-=w;
			e[i].c-=w,e[i^1].c+=w;
			if(f==0) break;
		}
	}
	if(!used) dis[x]=-1;
	return used;
}
int main(){
	n=read(),m=read(),s=read(),t=read();
	int u,v,c;
	for(int i=1;i<=m;i++){
		u=read(),v=read(),c=read();
		add(u,v,c);add(v,u,0);
	}
	while(bfs()){
		memcpy(cur+1,head+1,n<<2);
		ans+=dfs(s,INF);
	}
	write(ans);
	return 0;
} 

小细节:
1.博主的cur数组是用来做当前边优化的。dfs时。如果现在的f并没有填满u到v的边,那么下一次搜到u时直接从这条边开始搜就可以了,因为u出去的排在边(u,v)前面的边已经满流了,不需要看了,注意dfs里for循环的括号内要带引用符号。
2.很多题里面建图时源点编号往往是0,这时要把memset和memcpy改成最常见的形式。

最小费用最大流
菜鸡博主今天终于开始写费用流的博文了反正也什么没人看
最小费用最大流就是对于每一条边都有一个容量和一个单位流量需要消耗的费用,现在我们需要在保证流量为最大流的基础之上,寻找一个最小的花费。
与最大流问题类似的,我们每一次找到一条最小的增广路来扩大流量,但与之前不同的是,最大流问题对于这个“最小”的定义是经过的边最少,而费用流的定义是单位流量消耗的总费用最少,也就是这条路径上每条边的费用值之和最少。大家看出来了吧,就是一个最短路问题,由于可能存在有负边权,所以我们只能用SPFA来实现。
值得一提的是,我们每一次找到一条最小增广路时,接下来的操作是更新费用,但是,这条增广路上每一条边的费用是不一样的,所以我们在做最短路要记录每一个点的前驱边和前驱点,这一点大家看见代码就明白了。
而且,这个前驱边还有个作用就是判断是否还有增广路存在。
模板题代码:

#include<bits/stdc++.h>
using namespace  std;
#define N 501
#define M 15005
#define inf 0x7f
inline int read(){
	int flag=1;char ch;
	while((ch=getchar())<'0'||ch>'9') if(ch=='-') flag=-1;
	int ans=ch-48;
	while((ch=getchar())>='0'&&ch<='9') ans=ans*10+ch-48;
	return ans*flag;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>9) write(x/10);
	putchar('0'+x%10);
	return;
}
int head[N],cnt=1,n,m,dis[N];
int pre[N],flow[N],vis[N],pos[N];
int ans1=0,ans2=0;
struct node{
	int v,c,w,nt;
}e[M<<1];
inline void add(int u,int v,int c,int w){
	cnt++;
	e[cnt]=(node){v,c,w,head[u]};
	head[u]=cnt;
}
inline bool spfa(){
	queue<int>q;
	q.push(1);
	memset(dis,inf,sizeof(dis));
	memset(flow,inf,sizeof(flow));
	memset(vis,0,sizeof(vis));
	vis[1]=1,pre[n]=-1,dis[1]=0;
	while(q.size()){
		int u=q.front();q.pop();
		vis[u]=0;
		for(int i=head[u];i;i=e[i].nt){
			int v=e[i].v;
			if(e[i].c>0&&dis[v]>dis[u]+e[i].w){
				dis[v]=dis[u]+e[i].w;
				pre[v]=u;
				pos[v]=i;
				flow[v]=min(flow[u],e[i].c);
				if(!vis[v]){
					vis[v]=1,q.push(v);
				}
			}
		}
	} 
	if(pre[n]==-1) return 0;
	return 1;
}
int main(){
	n=read(),m=read();
	int u,v,c,w;
	for(int i=1;i<=m;i++){
		cin>>u>>v>>c>>w;
		add(u,v,c,w);
		add(v,u,0,-w);
	}
	while(spfa()){
		ans1+=flow[n];
		ans2+=flow[n]*dis[n];
		int pot=n;
		while(pot!=1){
			e[pos[pot]].c-=flow[n];
			e[pos[pot]^1].c+=flow[n];
			pot=pre[pot];
		}
	}
	write(ans1),putchar(' '),write(ans2);
	return 0;
} 

我们来做一道看起来很nice的省选题练练手
传送门在此
题目描述
给定一张有向图,每条边都有一个容量C和一个扩容费用W。这里扩容费用是指将容量扩大1所需的费用。求: 1、 在不扩容的情况下,1到N的最大流; 2、 将1到N的最大流增加K所需的最小扩容费用。

输入输出格式
输入格式:
输入文件的第一行包含三个整数N,M,K,表示有向图的点数、边数以及所需要增加的流量。 接下来的M行每行包含四个整数u,v,C,W,表示一条从u到v,容量为C,扩容费用为W的边。

输出格式:
输出文件一行包含两个整数,分别表示问题1和问题2的答案。

输入输出样例
输入样例#1:
5 8 2
1 2 5 8
2 5 9 9
5 1 6 2
5 1 1 8
1 2 8 7
2 5 4 9
1 2 1 1
1 4 2 1
输出样例#1:
13 19
说明
30%的数据中,N<=100

100%的数据中,N<=1000,M<=5000,K<=10

与很多这道题的题解一样的,博主的开头第一句是:第一问是傻逼题,就是裸的不能再裸的最大流,我们只讨论讨论第二问的做法。
在跑完了第一问的最大流后我们得到一个残量网络,现在我们的目的是将流量增加k。我们在一开始输入边目录时把边目录先存一份不慌放入图中,在跑完了最大流后再将它们一条一条加入到残量网络中,此时应该讲它们的容量定为INF,因为如果不看费用你是想怎么扩就怎么扩的。
此时问题就转化为如何将流量限制为k。现在就easy了,只需要开一个编号为0的汇点,在它与节点1中连一条容量为k,费用为0的边就OK了,此时套上最小费用最大流的模板,由于只在0与1之间有流量限制,所以跑满时流量一定为k。

值得一提的是,在一开始跑最大流时需要将每条边的费用定为0,因为要避免对于后面费用流得影响,不然会炸的啊!!!别问我为什么如此激动。
代码:

#include<bits/stdc++.h>
using namespace std;
inline int read(){
	int flag=1;char ch;
	while((ch=getchar())<'0'||ch>'9') if(ch=='-') flag=-1;
	int ans=ch-48;
	while((ch=getchar())>='0'&&ch<='9') ans=ans*10+ch-48;
	return ans*flag;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>9) write(x/10);
	putchar('0'+x%10);
	return;
}
const int N=1005;
const int M=5005;
const int INF=0x7fffffff;
int n,m,k;
int flow[N],pre[N],pos[N];
int cnt=1,head[N],dis[N],dep[N],cur[N],vis[N];
struct node{
	int v,c,w,nt;
}e[M<<2];
struct node1{
	int u,v,c,w;
}lst[M<<2];
inline void add(int u,int v,int c,int w){
	cnt++;e[cnt]=(node){v,c,w,head[u]};head[u]=cnt;
}
inline bool bfs(){
	memset(dep,-1,sizeof(dep));
	queue<int>q;
	dep[1]=0,q.push(1);
	while(!q.empty()){
		int u=q.front();q.pop();
		for(int i=head[u];i;i=e[i].nt){
			int v=e[i].v;
			if(dep[v]==-1&&e[i].c>0){
				dep[v]=dep[u]+1;
				q.push(v);
				if(v==n) return 1;
			}
		}
	}
	return 0;
} 
inline int dfs(int x,int f){
	if(f==0||x==n) return f;
	int used=0;
	for(int &i=cur[x];i;i=e[i].nt){
		int v=e[i].v;
		if(e[i].c&&dep[v]==dep[x]+1){
			int w=dfs(v,min(f,e[i].c));
			if(!w) continue;
			used+=w;f-=w;
			e[i].c-=w;e[i^1].c+=w;
			if(f==0) break;
		}
	}
	if(!used) dep[x]=-1;
	return used;
}
inline bool spfa(){
	queue<int>q;
	q.push(0);
	memset(dis,127,sizeof(dis));
	memset(flow,127,sizeof(flow));
	memset(vis,0,sizeof(vis));
	vis[0]=1,pre[n]=-1,dis[0]=0;
	while(q.size()){
		int u=q.front();q.pop();
		vis[u]=0;
		for(int i=head[u];i;i=e[i].nt){
			int v=e[i].v;
			if(e[i].c>0&&dis[v]>dis[u]+e[i].w){
				dis[v]=dis[u]+e[i].w;
				pre[v]=u;
				pos[v]=i;
				flow[v]=min(flow[u],e[i].c);
				if(!vis[v]){
					vis[v]=1,q.push(v);
				}
			}
		}
	} 
	if(pre[n]==-1) return 0;
	return 1;
}
int main(){
	n=read(),m=read(),k=read();
	int u,v,c,w;
	for(int i=1;i<=m;i++){
		u=read(),v=read(),c=read(),w=read();
		lst[i].u=u,lst[i].v=v,lst[i].c=c,lst[i].w=w; 
		add(u,v,c,0);
		add(v,u,0,0);
	}
	int ans1=0;
	while(bfs()){
		memcpy(cur,head,sizeof(head));
		ans1+=dfs(1,INF);
	}
	add(0,1,k,0);add(1,0,0,0);
	for(int i=1;i<=m;i++){
		add(lst[i].u,lst[i].v,INF,lst[i].w);
		add(lst[i].v,lst[u].u,0,-lst[i].w);
	}
	int ans2=0;
	while(spfa()){
		ans2+=flow[n]*dis[n];
		int pot=n;
		while(pot!=0){
			e[pos[pot]].c-=flow[n];
			e[pos[pot]^1].c+=flow[n];
			pot=pre[pot];
		}
	}
	write(ans1),putchar(' '),write(ans2);
	return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值