网络流学习笔记

网络流学习笔记

网络流,这个曾经只是专业知识的东西现在也逐渐进入寻常百姓家了。其实网络流并不神秘(但的确很神奇),可以将它理解为是一个管道网络,从一个源点出发,最后汇集到一个汇点,中间经过了许多容量大小不一的管道。一般来说,能抽象出网络流的题目在转成网络后求的不外乎是最大流(有时候也会求最小流,不过那是在流量有下界的时候),最小割,以及费用流。下面先对一些名词作简单的介绍:

1.流量限度:一个管道的极限流量,上限是最高流量,下限是最低流量。

2.残量网络:一个管道在已经流过一些流量以后还能流的流量。

3.反向边:设网络中非源非汇的两个顶点u、v,流量限度为c(u,v),先从u流向v f个流量(f<c(u,v)),然后设置一个流量为f的反向边从v流向u。反向边其实就是一条后路,以防你的程序选择了一条错误的道路却无法改正,在程序发现走错后就会从反向边走回来。“反向边的作用就是给程序一个反悔的机会”。

4.增广路:在残量网络中的一条从源点到汇点的路径,可以通过不断地增广来求最大流。

5.割:切断一些管道,使得从源点无法通向汇点,每一条被切断的管道的流量限度之和就是这个割的代价。

接下来是一些定理和性质:

1.流量守恒:除去源点和汇点外,流入其他任意一个节点的流量等于从这个点流出的流量,即每个节点都不会储存流量,开始从源点流出的流量最后一点都不剩的流进了汇点。

2.增广路定理:对于一个流f,在它的残量网络中再也找不到增广路,那么它就是原网络的最大流。

3.最小割最大流定理:一个网络中的最小割等于它的最大流。(这个非常重要,许多题目都是通过转成最小割,再转到求最大流上)

4.一条从源点到汇点的路的最大流量由这条路上的最小流量限制决定。

一、最大流

最大流是最基础的网络流算法,很多题目都是直接或间接地求网络的最大流,让我们从最基本的算法看起:

1.Ford-Fulkerson算法

这是求最大流的最基础的算法,不过因为它的时间复杂度比较高(O(n^4)),所以在实际应用中一般不用它,但是通过学习它,我们可以掌握最大流的基础求法,为后面学习SAP和DINIC打好基础。

FF算法的思想是现在原网络中找到一条从源点流向汇点的可行流,然后在残量网络中不断寻找增广路,直到没有增广路为止,根据增广路定理,可知此时已经找到了最大流。

以下的图可以十分形象的展示FF算法的运转过程:(注:图片摘自leolin_的专栏)

2.Edmonds-Karp算法(EK)

EK是以FF为基础的算法,只是对FF有了一些小的优化,就是用BFS的方法来求增广路,这样相比较起来会优一些。Ek就是通过不断用BFS增广,直到达到最大流,不过它的效率也不是很高(O(V E^2),V是点的数量,E是边的数量),所以一般都是把它当做练习,而实践中也不常用。

3.ISAP算法

重点来了,基本上大家都是使用这种方法解决最大流问题的,而且ISAP具有许多用力的优化,从而使得它具有良好的时间效率,同时编程的复杂度也不高,裸的代码(只加了一个优化)只有60行左右,通俗易懂。

优化:

1.gap优化:开一个gap数组保存距离,开始都是零,然后逐渐更新,当一次更新完后出现断层(gap[i]=0)时,这就代表源点到汇点之间已经不连通了,直接跳出。

2.邻接表优化:如果点的数量过多,邻接矩阵存不下了,就需要用到邻接表(当然也可以用next数组,写成链表也没有问题)。邻接表存储的是边的信息,一般存的是出发点,终点,价值和下一条边的序号。使用邻接表的话可以降低空间复杂度,但是会比较难写,所以邻接矩阵能存下就尽量写邻接矩阵吧。

3.当前弧优化:在使用邻接表增广的时候,保存第一个可行的弧,这样下次在搜到这条边的时候就直接从这个可行的弧开始搜起。

下面是只使用了gap优化的ISAP:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
int n,m,a[101][101]={0},pre[101],dis[101]={0},gap[101]={0};
int ISAP(int S,int T)
{
	memset(pre,-1,sizeof(pre));
	pre[S]=S; gap[0]=T;
	int minn,u=S,v,k,maxn=0;
	while (dis[S]<T)
	{
		for (v=1; v<=T; v++)
			if (dis[u]==dis[v]+1 && a[u][v]>0) break;
		if (v<=T)
		{
			pre[v]=u; u=v;
			if (v==T)
			{
				k=0x7fffffff;
				for (int i=v; i!=S; i=pre[i])
					k=min(k,a[pre[i]][i]);
				maxn+=k;
				for (int i=v; i!=S; i=pre[i])
				{
					a[pre[i]][i]-=k;
					a[i][pre[i]]+=k;
				}
				u=S;
			}
		}
		else
		{
			minn=T;
			for (v=1; v<=T; v++)
				if (a[u][v]>0) minn=min(minn,dis[v]);
			gap[dis[u]]--;
			if (!gap[dis[u]]) break;
			dis[u]=minn+1; gap[dis[u]]++; u=pre[u];
		}
	}
	return maxn;
}
int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1; i<=m; i++)
	{
		int x,y,v;
		scanf("%d%d%d",&x,&y,&v);
		a[x][y]+=v;//邻接矩阵
	}
	printf("%d\n",ISAP(1,n));//此处1为源点,n为汇点 
	return 0;
}

4.预留推进算法(push_relabel)

预留推进算法经过优化就是压入重标记算法,先简述一下预留推进的思想,与其他算法相比,预留推进的一个最大特点是它每次都会给下一条边最大的流量(要么给到满,要么给到自己这里流量空了为止),这样一步一步推进。它会对每个节点维护一个高度,开始时源点高度最高,流量设为无穷大,然后向低的地方流下去,每次把向下已经流满却还有剩余流量的节点(源汇点除外)提升高度,使得能过继续流,如此往复,直到所有的非源非汇节点流量都变成零,此时算法结束,求出最大流。代码如下:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
int c[101][101],h[101],y[101],n;//c表示残量网络,h是高度函数,y是顶点的余流
void init()
{
	scanf("%d",&n);
	for (int i=1; i<=n; i++)
	  for (int j=1; j<=n; j++)
		scanf("%d",&c[i][j]);
	memset(h,0,sizeof(h));
	memset(y,0,sizeof(y));
	h[1]=n+1; y[1]=0x7fffffff;//此处1为源点,开始高度最高,余流为无穷大
}
int work(int s,int t)
{
	int ans=0;
	queue<int> qq;
	qq.push(s);
	while (!qq.empty())
	{
		int u=qq.front();
		qq.pop();
		for (int i=1; i<=t; i++)
		{
			int k;
			if (c[u][i]<y[u]) k=c[u][i];
			else k=y[u];
			if (k>0 && (u==s || h[u]==h[i]+1))
			{
				c[u][i]-=k; c[i][u]+=k;
				if (i==t) ans+=k;
				y[u]-=k; y[i]+=k;
				if (i!=s && i!=t) qq.push(i);
			}
		}
		if (u!=s && u!=t && y[u]>0)//若还有余流,则提高高度,重新入队
		{
			h[u]++;
			qq.push(u);
		}
	}
	return ans;
}
int main()
{
	init();//预处理
	printf("%d\n",work(1,n));
	return 0;
}

二、费用流(最小费用最大流)

费用流的求法与最大流求法相类似,这里不多描述,下面是邻接表实现的最小费用最大流模板(有借鉴神犇模板):

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
struct point
{
	int v,cap,cost,next,reverse;
}edge[200001];
int n,m,ans=0,k,eh[200001],que[200001],pre[200001],dis[200001];
bool vis[200001];
void init()
{
	memset(eh,0,sizeof(eh));
	memset(que,0,sizeof(que));
	memset(pre,-1,sizeof(pre));
	memset(dis,0,sizeof(dis));
} 
void add(int u,int v,int cap,int cost)
{
	edge[k].v=v; edge[k].cap=cap; edge[k].cost=cost;
	edge[k].next=eh[u]; edge[k].reverse=k+1;
	eh[u]=k++;
	edge[k].v=u; edge[k].cap=0; edge[k].cost=-cost;
	edge[k].next=eh[v]; edge[k].reverse=k++;
}
bool spfa()
{
	int head=0,tail=1;
	for (int i=0; i<=n; i++)
	{
		dis[i]=0x7fffffff;
		vis[i]=false;
	}
	dis[0]=que[0]=0; vis[0]=true;
	while (head<tail)
	{
		int u=que[head++];
		for (int i=eh[u]; i!=0; i=edge[i].next)
		{
			int v=edge[i].v;
			if (edge[i].cap && dis[v]>dis[u]+edge[i].cost)
			{
				dis[v]=dis[u]+edge[i].cost;
				pre[v]=i;
				if (!vis[v])
				{
					vis[v]=true;
					que[tail++]=v;
				}
			}
		}
		vis[u]=false;
	}
	if (dis[n]==0x7fffffff) return false;
	return true;
}
void work()
{
	int u,s=0x7fffffff;
	for (u=n; u!=0; u=edge[edge[pre[u]].reverse].v)
		s=min(s,edge[pre[u]].cap);
	for (u=n; u!=0; u=edge[edge[pre[u]].reverse].v)
	{
		edge[pre[u]].cap-=s; edge[edge[pre[u]].reverse].cap+=s;
		ans+=s*edge[pre[u]].cost;
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	init();
	for (int i=1; i<=m; i++)
	{
		int x,y,cap,cost;
		scanf("%d%d%d",&x,&y,&cap,&cost);
		add(x,y,cap,cost); add(y,x,cap,cost);
	}
	while (spfa()) work();
	printf("%d\n",ans);
	return 0;
}

下面是邻接矩阵实现的:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
int a[101][101],b[101][101]; 
int n,ans=0,k,que[101],pre[101],dis[101];
bool vis[101];
void init()
{
	memset(que,0,sizeof(que));
	memset(pre,-1,sizeof(pre));
	memset(dis,0,sizeof(dis));
	memset(a,0,sizeof(a));
	memset(b,0,sizeof(b));
}
bool spfa()
{
	int head=0,tail=1;
	for (int i=0; i<=n; i++)
	{
		dis[i]=0x7fffffff;
		vis[i]=false;
	}
	dis[0]=que[0]=0; vis[0]=true;
	while (head<tail)
	{
		int u=que[head++];
		for (int i=0; i<=n; i++)
			if (a[u][i] && dis[i]>dis[u]+b[u][i])
			{
				dis[i]=dis[u]+b[u][i];
				pre[i]=u;
				if (!vis[i])
				{
					vis[i]=true;
					que[tail++]=i;
					if (tail==n) tail=0;
				}
			}
			vis[u]=false;
			head++;
			if (head==n) head=0;
	}
	if (dis[n]==0x7fffffff) return false;
	return true;
}
void work()
{
	int s=0x7fffffff;
	for (int u=n; u!=0; u=pre[u])
		s=min(s,a[pre[u]][u]);
	for (int u=n; u!=0; u=pre[u])
	{
		a[pre[u]][u]-=s; a[u][pre[u]]+=s;
		ans+=s*b[pre[u]][u];
	}
}
int main()
{
	scanf("%d",&n);
	init();
	for (int i=1; i<=n; i++)
		for (int j=1; j<=n; j++)
			scanf("%d%d",&a[i][j],&b[i][j]);
	while (spfa()) work();
	printf("%d\n",ans);
	return 0;
}

最后,总结一下个人做题经验:在做网络流题目的时候, 建模是最重要的,只要建好了模,直接修改模板即可。还是要多练习才能掌握技巧。



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值