网络流之dinic算法

背景

刚刚学习了网络流,觉得很神奇难懂,但又觉得有道理

序言

照例放上概念~~

网络流


没了。。

一般来说,网络流分为两大类:最大流和费用流

费用流又分为最大(小)费用流和最大(小)费用最大流,这个我们一会再详细讲解

下面是一些定义

源点(S):入度为0

汇点(T):出度为0

在有向图中,每条边(弧)都有一个边权,我们叫它容量

满足有唯一的源点和汇点的图称为网络流图

每条弧(u,v)上给定一个数f(u,v),满足0<=f(u,v)<=c( u, v )(其中c为弧的容量)

我们称f(u,v)为弧(u,v)上的流量。

满足下列条件的一组流量称为可行流:

除源点S和汇点T之外,其余每个点满足:流入量=流出量(流量平衡)

一个网络流图,去掉一组可行流后还能流多少流量称为残量网络

增广路:在残量网络中的可行流称为增广路

易证:一个图中没有可行流当且仅当残量网络中没有增广路

我们要做的,就是找出网络流图中的最大流。

求最大流有几种算法

最基本的:Ford-Fulkerson算法

Dinic算法(优化)

(I)SAP算法(另一种优化,不过我不会)

预流推进(大名鼎鼎,我也不会)

其实后三种会一种就足够了

咱们先从最基本的Ford-Fulkerson算法开始

我们先来看一张网络图

(自己做的有点丑勿喷)

S=1,T=7



费我汇

我们先bfs找出一条可行流

(为了讲解,我们选取1--2--4--6--7)

然后残量网络就是



此时我们再找一条增广路 1--2--5--7 (1--2--4--5--7也行)

然后残量网络为



此时找不到增广路,停止。

按照上面的方案,我们找到的“最大流”是4+2=6

事实上,我们能看出此图中的最大流是9

为什么会出现这个错误呢?

我们发现,当我们在寻找最短路时是盲目寻找的,这就会导致我们“堵塞”了真正的最大流。

其实,寻找最大流是有顺序的,但由于实现原因,在oi中我们一般不这么做。

怎么办?

给它一个反悔的机会。

具体实现是这样的:我们对每个边都建立一个反边,容量为0(如图)


然后我们进行增广的同时,也把它的反边容量加上流量,那么下次如果从它的反边经过,就相当于不走原边,即退流

来看一下如何实现的

一次增广(1--2--4--6--7)


二次增广(1--2--5--7)


三次增广(1--3--6--4--5--7)


此时最大流为4+2+3=9 是正确的

我们发现,通过退流,我们把这三次增广拆成了1--2--5--7,1--2--4--5--7,1--3--6--7,完美地解决掉顺序问题

我们来看这个图



大家可以试着推一推,如果按照1--3--2--4增广会怎样?

我们发现,Ford-Fulkerson算法和容量有直接关系,面对int范围的数我们必须要找一个优化方法,使一条边不被重复增广,dinic就是一个很好的算法

dinic算法

dinic算法基于分层图的思想,即将网络流图分层,不同层之间转移,不允许向回转移

每次寻找增广路前进行一次bfs,如果存在增广路,再进行增广,代码如下

int dinic()
{
    int tmp=0;
    while(bfs())//查找是否存在增广路 
	{
	tmp+=dfs(S,inf);//搜索查找残量网络最大流	
	}
    return tmp;//返回最大流 
}

查找是否存在增广路代码

queue <int> q;
bool bfs()
{
    while(!q.empty())q.pop();
    memset(d,-1,sizeof(d));//初始化 
    d[S]=1;q.push(S);//从源点广搜 
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(int i=fst[x];i;i=nxt[i])//邻接表 
        {
        if(k[i]&&d[v[i]]==-1)  //存在通向v[i]点的可行流 
        {
        d[v[i]]=d[x]+1; //分层 
        q.push(v[i]);//加入队列搜索 
        }   
        }
    }
    return ~d[T];//如果没访问到T点返回0,否则返回1 
}

增广代码

int dfs(int x,int val)
{
    if(x==T||!val)return val;//如果没有可行流或者到达汇点 
    int tmp=0;
    for(int i=fst[x];i;i=nxt[i])
    {
        if(k[i]&&d[v[i]]==d[x]+1)//必须有可行流,必须向下一层转移 
        {
            int flow=dfs(v[i],min(val,k[i]));//通过最大流 
            val-=flow;//当前流 
			k[i^1]+=flow;//反边 
			k[i]-=flow; //正边 
			tmp+=flow;//答案 
            if(!val)break;//如果没流继续搜索没有意义,返回 
        }
    }
    if(!tmp)d[x]=-1;//如果此点无法通过流,阻塞此点 
    return tmp;//返回答案 
}

到此,最大流算法已经讲完了,下面讲一下它的变种

最小割


BZOJ[1001]

现在小朋友们最喜欢的"喜羊羊与灰太狼",话说灰太狼抓羊不到,但抓兔子还是比较在行的,
而且现在的兔子还比较笨,它们只有两个窝,现在你做为狼王,面对下面这样一个网格的地形:

 

左上角点为(1,1),右下角点为(N,M)(上图中N=4,M=5).有以下三种类型的道路 
1:(x,y)<==>(x+1,y) 
2:(x,y)<==>(x,y+1) 
3:(x,y)<==>(x+1,y+1) 
道路上的权值表示这条路上最多能够通过的兔子数,道路是无向的. 左上角和右下角为兔子的两个窝,
开始时所有的兔子都聚集在左上角(1,1)的窝里,现在它们要跑到右下解(N,M)的窝中去,狼王开始伏击
这些兔子.当然为了保险起见,如果一条道路上最多通过的兔子数为K,狼王需要安排同样数量的K只狼,
才能完全封锁这条道路,你需要帮助狼王安排一个伏击方案,使得在将兔子一网打尽的前提下,参与的
狼的数量要最小。因为狼还要去找喜羊羊麻烦.


这道题显然是最小割

yy一下,假设你是狼王,你肯定在出口守着,因为此时跑出来的兔子最少

而此时跑出来兔子最多时即为最大流

so,一个定理:

网络的最大流==最小割

这个定理很有用


费用流

连续最短路算法

就是把dinic算法的bfs改成spfa,每次找最短路增广,这样能保证每次费用最小

如何区别最大(小)费用最大流和最大(小)费用流呢?

很简单,最大(小)费用流不需要跑满流,我们只要判断当前费用是否比上次增广更优,否则返回

代码

最大费用最大流

bool spfa()
{
	for(int i=0;i<=n*m*2;i++)
	d[i]=-inf,b[i]=0;//初始化 
	d[T]=-inf;
	d[S]=0;
	q.push(S);
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		b[x]=0;
		for(int i=fst[x];i;i=nxt[i])
		{
			if(k[i]&&d[v[i]]<d[x]+cst[i])//松弛操作 
			{
				d[v[i]]=d[x]+cst[i];
				if(!b[v[i]])b[v[i]]=1,q.push(v[i]);
			}
		
		}
	}
	return d[T]!=-inf;//如果汇点没有被访问到返回0 
}
int dfs(int x,int val)
{
	if(x==T||!val)
	{
		cost+=d[T]*val;//本次增广获得的费用 
		return val;
	}
	b[x]=1;
	int tmp=0;
	for(int i=fst[x];i;i=nxt[i])
	{
		if(!b[v[i]]&&d[v[i]]==d[x]+cst[i]&&k[i])//分层 
		{
			int flow=dfs(v[i],min(val,k[i]));
			val-=flow;
			k[i]-=flow;
			k[i^1]+=flow;
			tmp+=flow;
			
		}
		if(!val)break;
	}
	if(!tmp)d[x]=-inf;//阻塞 
	return tmp;
}
void dinic()
{
	while(spfa())
	{
	dfs(S,inf);	
	}
}

最大费用流

bool spfa()
{
	for(int i=0;i<=n*m*2;i++)
	d[i]=-inf,b[i]=0;//初始化 
	d[T]=-inf;
	d[S]=0;
	q.push(S);
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		b[x]=0;
		for(int i=fst[x];i;i=nxt[i])
		{
			if(k[i]&&d[v[i]]<d[x]+cst[i])//松弛操作 
			{
				d[v[i]]=d[x]+cst[i];
				if(!b[v[i]])b[v[i]]=1,q.push(v[i]);
			}
		
		}
	}
	if(d[T]<0)return 0; //如果不是更优则返回0 这是区别所在
	return 1;
}
int dfs(int x,int val)
{
	if(x==T||!val)
	{
		cost+=d[T]*val;//本次增广获得的费用 
		return val;
	}
	b[x]=1;
	int tmp=0;
	for(int i=fst[x];i;i=nxt[i])
	{
		if(!b[v[i]]&&d[v[i]]==d[x]+cst[i]&&k[i])//分层 
		{
			int flow=dfs(v[i],min(val,k[i]));
			val-=flow;
			k[i]-=flow;
			k[i^1]+=flow;
			tmp+=flow;
			
		}
		if(!val)break;
	}
	if(!tmp)d[x]=-inf;//阻塞 
	return tmp;
}
void dinic()
{
	while(spfa())
	{
	dfs(S,inf);	
	}
}
很好,网络流到这里就讲完了,其实,网络流的难点不在代码,而在发现以及建模上,希望大佬指出本文的错误,共同进步!


(555555

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值