背景
刚刚学习了网络流,觉得很神奇难懂,但又觉得有道理
序言
照例放上概念~~
网络流
没了。。
一般来说,网络流分为两大类:最大流和费用流
费用流又分为最大(小)费用流和最大(小)费用最大流,这个我们一会再详细讲解
下面是一些定义
源点(S):入度为0
汇点(T):出度为0
在有向图中,每条边(弧)都有一个边权,我们叫它容量
满足有唯一的源点和汇点的图称为网络流图
我们称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]
这道题显然是最小割
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