网络流与费用流(上)网络流

网络流


小学生都知道的一些性质:

  • 最大流等于最小割
  • 平面图的最大流等于其对偶图的最短路
  • 二分图中最大流等于最大匹配数
  • 还有一些乱七八糟的忘了

求最大流的算法

  • FF方法(最大流算法的基本思想): 
建图,每条边要建反向边,容量为0(无向图容量跟正向边一样),一直找从S到T的增广路,每次找到都把最小的容量加到最大流,然后路上所有边都减去这个值,对应的反向边加上这个值,知道找不到增广路。
  • EK算法:

FF方法的最简单实现,每次寻找增广路都是朴素的BFS。时间复杂度O(N*M^2)。

  • SAP算法及其优化
SAP算法使用距离标号的最短增广路,距离标号的思想来源于push_relable类算法(不知道是啥)。

算法思路:
所谓距离标号就是最少经过多少条有容量的边能到达汇点,假如距离标号数组为lv[],找增广路找到u点的时候都按照lv[u]==lv[v]+1找出v点扩展,满足这个条件的边称为可行弧,证明什么的就不说了。
距离标号可以在算法开始之前进行一次BFS把初始标号预处理出来,这样有利于常数上的优化,也可以不预处理先直接清0后面再维护。关于距离标号的维护,由于增广时都走容量大于0的可行弧,如果没有容量大于0的边了那这个点差不多就废了,距离标号为n用于断层优化;如果还有容量大于0的边但没有可行弧那u的lv就取可扩展点的最小lv+1。
SAP可以加很多常数上的优化,优化全加上后效果似乎很好,比如上面的BFS预处理。除了这个以外还有三个很重要的优化就是当前弧优化,断层优化,多路增广优化。
  • 当前弧优化就是记录每个节点邻接表中从哪条边开始是当前正在增广或者可能可以增广的,作用就是处理回退边的时候可以很方便,找增广路的时候直接从当前弧开始找就少循环几遍,随便yy一下都能明白当前弧以前的边都是不会再走的了,所以当前弧优化的正确性是显然的。
  • 断层优化就跟上面提到的一个点再也没有边可以增广有关,如果由于多次废点的产生而导致某一个标号的数量减少到0,那就意味着出现了断层,源点再也不可能到达汇点,可以直接退出循环,而如果源点也成为废点那也可以结束了。
  • 多路增广优化就是每次找到增广路之后都不回退到源点重新开始找增广路,而是回退到最前面的限流边,然后继续增广,这个应该很好理解没啥好说的。
模板代码:
const int maxn=100010;
const int maxm=maxn<<1;
const int INF=0x3f3f3f3f;

int gap[maxn],lv[maxn],pre[maxn],cur[maxn];

struct edge
{
	int v,w,next;
}e[maxm];
int h[maxn],ecnt;

inline void init()
{
	memset(h,-1,sizeof(h));
	ecnt=0;
}

inline void addedge(int u,int v,int w)
{
	e[ecnt].v=v;
	e[ecnt].w=w;
	e[ecnt].next=h[u];
	h[u]=ecnt++;
}

inline void Addedge(int u,int v,int w)
{
	addedge(u,v,w);
	// addedge(v,u,0);//directed
	addedge(v,u,w);//undirected
}

int sap(int n,int s,int t)
{
	memcpy(cur,h,sizeof(h));
	memset(gap,0,sizeof(gap));
	int u=pre[s]=s;
	int ans=0,flow,neck;
	
	// memset(lv,0,sizeof(lv)); //nopre
	// gap[0]=n;
	
	for(int i=1;i<=n;i++)lv[i]=n;//bfspre
	gap[lv[t]=0]++;
	queue<int> q;
	q.push(t);
	while(!q.empty())
	{
		int p=q.front();q.pop();
		for(int i=h[p];~i;i=e[i].next)
		{
			if(lv[e[i].v]!=n)continue;
			// if(e[i].w)continue;//directed
			gap[lv[e[i].v]=lv[p]+1]++;
			q.push(e[i].v);
		}
	}
	
	while(lv[s]<n)
	{
		if(u==t)
		{
			flow=INF;
			for(int i=s;i!=t;i=e[cur[i]].v)
			{
				if(e[cur[i]].w<flow)
				{
					flow=e[cur[i]].w;
					neck=i;
				}
			}
			ans+=flow;
			for(int i=s;i!=t;i=e[cur[i]].v)
			{
				e[cur[i]].w-=flow;
				e[cur[i]^1].w+=flow;
			}
			u=neck;
		}
		for(int &i=cur[u];~i;i=e[i].next)
			if(e[i].w && lv[u]==lv[e[i].v]+1)break;
		if(~cur[u])
		{
			pre[e[cur[u]].v]=u;
			u=e[cur[u]].v;
		}
		else
		{
			if(!--gap[lv[u]])break;
			cur[u]=h[u];
			int mlv=n-1;
			for(int i=h[u];~i;i=e[i].next)
			{
				if(e[i].w && lv[e[i].v]<mlv)
				{
					mlv=lv[e[i].v];
					cur[u]=i;
				}
			}
			gap[lv[u]=mlv+1]++;
			u=pre[u];
		}
	}
	
	return ans;
}

hdu4280测速结果,这个是我自己进行终极优化考虑各种情况的版本了= =,搞了一天都吐血了,这个时间还是比较满意的

  • dinic算法
其实网络流算法万变不离其宗,dinic的思路还是跟sap差不多的,只不过实现上的差异让dinic也有优秀的时间表现。

算法思路:
每次增广之前都从源点开始BFS一次,这次是搞相对于源点的距离标号,一旦访问到汇点就说明可以增广,结束BFS并开始增广,如果BFS结束后都没有访问到汇点,那就不能再增广了,算法结束。增广则是使用DFS进行多路增广,实现起来十分方便给力。

模板代码:
const int maxn=210;
const int maxm=maxn<<1;
const int INF=0x3f3f3f3f;

int lv[maxn];

struct edge
{
	int v,w,next;
}e[maxm];
int h[maxn],ecnt;

inline void init()
{
	memset(h,-1,sizeof(h));
	ecnt=0;
}

inline void addedge(int u,int v,int w)
{
	e[ecnt].v=v;
	e[ecnt].w=w;
	e[ecnt].next=h[u];
	h[u]=ecnt++;
}

inline void Addedge(int u,int v,int w)
{
	addedge(u,v,w);
	addedge(v,u,0);//directed
	// addedge(v,u,w);//undirected
}

bool bfs(int s,int t)
{
	queue<int> q;
	q.push(s);
	memset(lv,-1,sizeof(lv));
	lv[s]=0;
	while(!q.empty())
	{
		int u=q.front();q.pop();
		if(u==t)return 1;
		for(int i=h[u];~i;i=e[i].next)
		{
			int v=e[i].v;
			if(~lv[v] || !e[i].w)continue;
			lv[v]=lv[u]+1;
			q.push(v);
		}
	}
	return 0;
}

int dfs(int u,int flow,int t)
{
	if(u==t)return flow;
	int ret=0,f;
	for(int i=h[u];~i;i=e[i].next)
	{
		int v=e[i].v;
		if(e[i].w && lv[u]+1==lv[v])
		{
			f=dfs(v,min(flow-ret,e[i].w),t);
			e[i].w-=f;
			e[i^1].w+=f;
			ret+=f;
			if(ret==flow)return ret;
		}
	}
	return ret;
}

int dinic(int s,int t)
{
	int ret=0;
	while(bfs(s,t))ret+=dfs(s,INF,t);
	return ret;
}

网络流算法结语

以后遇到时间要求不高的题就写dinic,因为实在是很好写,有什么意外调起来也好些;时间限制比较紧张的就写极度优化的sap,虽然我没自己测试过这个跟dinic到底哪个比较快,但既然dinic都TLE了那就试试这个呗= =感觉要被压仓底了


有下界的网络流

这个意思就是边不仅有容量上限而且还有下限,这时候就要先确定能不能满足下限的要求,也就是所谓的先求可行流。

无源汇点求可行流

首先对于原来的每条边(u,v),都把它的容量建成上限减下限。然后加上超级源汇点,对于每个点u都记一下所有(i,u)边的下限的和记为in[u],所有(u,i)边的下限和记为out[u],算一下tmp=in[u]-out[u],如果tmp为0就不用管它了,如果tmp大于0那么就从超级源点向u点连一条容量为tmp的边,否则就从u向超级汇点连一条容量为-tmp(因为tmp此时为负值)。然后跑一遍最大流,跑完检查一下超级源点的所有出边的容量是否都用完,没全部用完说明没有可行流;如果存在可行流,那么这个可行流方案中原来的每条边的流量就是它的下限加上刚刚跑完最大流之后对应那条边所用的流量,这个可以在回退边找到。
至于正确性我在这里就不详细说了,随便一搜都一大把,主要说的就是根据每个点流量平衡的原则对网络进行变形,各种边的关系加加减减啊什么的。

有源汇点求最大流/最小流

大家都知道对于有源汇点的网络来说源点的流出量是等于汇点的流入量的,那我们只要连一条从T到S的容量无穷大的边就把网络变成上面的无源汇情况了,于是还是用上面的方法先确定可行流的存在。根据流量平衡可知S到T的可行流被保存在边T->S上记为f,然后我们只要把这个边去掉,在残余网络从S到T求一遍最大流得到f‘,那么f+f'就是满足下限的最大流了。
然而这种问题的出得比较多的还是最小流。但最小流其实也思想也查不多,具体做法是:在加T->S边之前先从跑一遍超级S到超级T的最大流,这一步把除了最小流的部分流走,然后再加上容量为INF的T->S边,再从超级S到超级T跑一边最大流,此时T->S的流量就是答案。
另外这两种问题还有另外一种比较好理解的做法就是,拿最大流来说,把T->S的边建成上限为INF,下限为x的边,然后二分这个x,直到找到最大的x满足存在可行流的条件,而最小流显然就是二分上限了。这种做法虽然比较好想,但是在时间上多个log,实现起来也挺麻烦的(因为每次求可行流都要恢复原网络),一般还是采用上面的方法比较好(虽然上面的实现起来也简单不到哪里去就是了= =)。

结语

到这里已经大概掌握了网络流算法和在得到网络流模型的情况下如何求解网络流问题了,现在我做网络流已经不需要看着模板了= =,继续努力,赶紧搞定下一篇费用流。

你可能感兴趣的:(网络流与费用流(上)网络流)