网络流--最大流

开始总觉得网络流是多么高深的东西,一直不敢去接受,然而学完之后发现好像也不是太难哦,只是好多基础东西的一些整合。ios

文章中可能会有多出纰漏,敬请读者不吝赐教。 算法

 

咱们以一个经典的问题引入算法。数组

你所在的村庄新开通了地下流水管道,自来水厂源源不断的提供水,村民们用水直接或间接用水,而村庄用完的废水统一回收于另外一点(设排来的水所有回收)。固然每一个管道有必定的容量,废水站求出最多能够汇聚多少水?网络

固然这是一个有向图。ide

首先明确几个概念:

容量:每条边都有一个容量(水管的最大水流容量)优化

源点:出发点(水厂)。spa

汇点:结束点(废水站)。指针

流:一个合法解称做一个流,也就是一条能够从源点到汇点的一条合法路径。code

流量:每条边各自被通过的次数称做其流量,最终收集的总数为整个流的流量。blog

 

图中会有这么几个限制:

容量限制:每条边的流量不超过其容量(水管会爆的)。

流量平衡:对于除源点和汇点之外的点来讲,其流入量必定等于流出量。

 

如今,咱们先简化一下这个图,来解决这个问题。

x/y表示总流量为y,已经流了x.

首先咱们会想到,随机找路径,然而若是走到如上图所示。

当走完,1->2->3->4咱们就找不到其余路径了,那么答案为1吗?不答案为2.

如今咱们改进算法,给流过的路径建反向边,像这样:

给程序有反悔的机会。

定义一跳变得残量为:容量 - 已流过的流量。

反向边的流量值=正向流过的总流量,也就是说正向流过多少,反向能够流回多少。

从而咱们又找到1->3->2->4的一条路径。

再次建路径上的反向边,咱们发现没有路径能够到达4点,因此答案为2.

 

小结:

总结一下上面求最大流的步骤:

1.在图上找到一条从源点到汇点的路径(称为‘增广路’)。

2.去增广路上的残量最小值v。(也就是流过的路径中流量最小的那一个)

3.将答案加上v。

4,.将增广路上全部边的残量减去v,反向边的残量加上v。

重复上边4个步骤直到找不到增光路为止,这称做 FF 方法

 

算法的正确性一会进行证实,咱们先看一下这个算法的效率。

首先这个算法应定不会死循环的,应为每次增广都会致使流量增长(而且增长的是整数),并且流量有一个客观存在最大值,因此它一定结束。(不理解不重要啦QAQ)

因为咱们并无指定它走哪一条边,因此优先考虑随便走一条边。

咱们考虑一种极限的状况:

现增广1->2->3->4,会出现一条3->2容量为1的边。

再增广1->3->2->4,再增广1->2->3->4....

这浪费大量的时间,若是脸黑的话最多200000次。

然而咱们若是先1->2->4,而后1->3->4,走两次就行了,上面的作法是咱们不指望的。

咱们能够考虑每次增广最短路。

 

EK算法:

EK算法是以上算法的实现:每次寻找最短路进行增广。

时间复杂度$O(m2n)$

首先咱们定义几个数组以及变量:

结构体:储存三个变量,nxt,to,dis   [邻接表建边]

flow[ i ] :表示流过 i 点的 v 值,也就是说目前通过到 i 点的路径上的最小的残量。

dis[ i ]:表示 i 点距离源点的距离,S,T表示源点以及汇点。

明确一个观点:

位运算符 ^ :1^1=0  0^1=1  2^1=3  3^1=2.

能够大体明白它的运算效果。

代码推演:

建边的时候,为了方便 ^ 运算符使用,咱们能够提早建好反向边,以后一条边,^ 一下就是另外一条边了。

首先咱们利用bfs处理图的连通性以及全部点与源点的距离,固然,当这条边上的残量已经为0的时候,咱们他已经不能通过,咱们能够直接不考虑。

在bfs中pre数组是记录每一个点最短路的前驱,last数组记录上条边的编号,从而记录出最短路径,而后从汇点进行更新便可。

bool bfs(int s,int t)
{
    
    memset(flow,0x7f,sizeof(flow));
    memset(dis,0x7f,sizeof(dis));
    memset(vis,0,sizeof(vis));
    Q.push(s);vis[s]=1;dis[s]=0,pre[t]=-1;
    
    while(!Q.empty())
    {
        int temp=Q.front();
        Q.pop();
        vis[temp]=0;
        for(int i=head[temp];i!=-1;i=edge[i].nxt)
        {
            int v=edge[i].to; 
            
            if(edge[i].flow>0&&dis[v]>dis[temp]+edge[i].dis)
            {
                dis[v]=dis[temp]+edge[i].dis;
                pre[v]=temp;
                last[v]=i;
                flow[v]=min(flow[temp],edge[i].flow);
                if(!vis[v])
                {
                    vis[v]=1;
                    Q.push(v); 
                }
            }
        }
    }
    return pre[t]!=-1;
}

 

从汇点向前更新。

while(bfs(s,t))
{
     int now=t;
     maxflow+=flow[t];
     mincost+=flow[t]*dis[t];
     while(now!=s)
     {
         edge[last[now]].flow-=flow[t];
         edge[last[now]^1].flow+=flow[t];
         now=pre[now];
     }
 }

 

EK算法还能优化么?

在此以前咱们先了解一个定理:.

最大流最小割定理

什么是割?

这么来讲吧,有我的住在废水收集站站附近,他不想然人们江水流到那,晚上偷偷在某个管道处切了一刀,图成为不联通的两块,从没有水流源点流到汇点。

选出一些管道,切断之后,图不连通,这些管道的集合就叫

这些边的容量之和叫作这个割的容量

任取一个割,其容量大于最大流的流量,why?

从源点到汇点每次都会通过割上的最少一条边。

割掉这条边之后把源点能到达的边放在左边,不能到达的放在右边。

显然源点到会点的流量不会超过从左边走向右边的次数,而这又不会从左边到右边的容量之和。、

直观一点:

当n管道在一块儿的时候,你一刀所有切断,不在一块儿的时候你也不至于切n+1刀吧。

最小割的容量等于最大流的流量

这个定理如何证实呢?

■考虑FF算法时,残量网络上没有了增广路。

那么咱们假设这时候,从源点通过残量网络能到达的点组成的集合为$X$,不能到达的点为$Y$。显然汇点在$Y$里,而且残量网络上没有从$X$到$Y$的边。

能够发现如下事实成立:

1.$Y$到$X$的边的流量为0.若是不为0,那么必定存在一条从X到Y的反向边,因而矛盾。

2.$X$到$Y$的边流量等于其容量。只有这样它才不会在残量网络中出现。

■根据第一个条件得知:没有流量从$X$到$Y$后又回到$X$。因此当前流量应该等于从$X$到$Y$的边的流量之和,而根据第二个条件他又等于$X$到$Y$的边容量之和。

■而全部从X到Y的边又构成了一个割,其容量等于这些边的容量之和。

★这意味着咱们找到一个割和一个流,使得前者的流量等于后者的容量。而根据前边的结论,最大流的流量不超过这个割的容量,因此这个流必定是最大流。

■一样的,最小割的容量也不会小于这个流的流量,因此这个割也必定是最小割。

■而这也正是FF方法的最后局面,由此咱们对出结论:

FF是正确的,而且最小割等于最大流

(听说还能够经过线性规划对偶定理证实 ...orz)

 

 EK优--Dinic

 EK时间复杂度过高,虽然大多数状况跑不到上界。

有一个显然的优化:

若是增广一次后发现最短路没有变化,那么能够继续增广,直到源点到汇点的增广路增大,才须要一边bfs。

bfs以后咱们去除那些可能在最短路上的边,即dis[终点]=dis[起点]+1的那些边。

显然这些边构成的图中没有环。

 咱们只须要延这些边尽量的增广便可。

 

 实现:

bfs处直接上代码,比较简单。

int bfs()
{
    memset(dis,-1,sizeof(dis));
    dis[S]=0;
    Q.push(S);
    while(!Q.empty())
    {
        int u=Q.front();
        Q.pop() ;
        for(int i=head[u];i!=-1;i=edge[i].nxt)
        {
            int v=edge[i].to;
            if(dis[v]==-1&&edge[i].w>0)
            {
                dis[v]=dis[u]+1;    //更新 
                Q.push(v); 
            }
        }
    }
    return dis[T]!=-1;    //判断是否联通。 
}

 

dfs:

当图联通时进行dfs,目前节点为u,每次通过与u距离最近的点,而且这条边的残量值要大于0,而后日后进行dfs。

咱们在dfs是要加一个变量,做为流量控制(后边的流量不能超过前边流量的最小值)。

dfs中变量flow记录这条管道以后的最大流量。

bool dfs(int u,int exp)
{
    if(u==T)return exp;    //到达重点,所有接受。 
    int flow=0,tmp=0;    
    for(int i=head[u];i!=-1;i=edge[i].nxt)
    {
        int v=edge[i].to;    //下一个点。 
        if(dis[v]==dis[u]+1&&edge[i].w>0)
        {
            tmp=dfs(v,min(exp,edge[i].w));    //往下进行 
            if(!tmp)continue;
            
            exp-=tmp;    //流量限制-流量,后边有判断。 
            flow+=tmp;
            
            edge[i].w-=tmp;        //路径上的边残量减小 
            edge[i^1].w+=tmp;    //流经的边的反向边残量增长。 
            if(!exp)break;    //判断是否在限制边缘 
        }
    }
    return flow;
}

 

重复上边若是图联通(有最短路径),就一直进行增广。

while(bfs())ans+=dfs(S,inf);

时间复杂度: 

Dinic复杂度能够证实是$O(n2m)$

在某些特殊状况下(每一个点要么只有一条入边且容量为1,要么仅有一条出边且容量为1)其时间复杂度甚至能作到$O(m \sqrt n )$

 

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
using namespace std;
#define inf 0x7fffffff
int head[10010],tot;
struct ahah{
    int nxt,to,w;
}edge[100010];
void add(int x,int y,int z)
{
    edge[tot].nxt=head[x];
    edge[tot].to=y;
    edge[tot].w=z;
    head[x]=tot++;
}

int n,m,x,y,z;
int ans,flow;
int dis[10010];
queue <int> Q;
int S,T;

int bfs()
{
    memset(dis,-1,sizeof(dis));
    dis[S]=0;
    Q.push(S);
    while(!Q.empty())
    {
        int u=Q.front();
        Q.pop() ;
        for(int i=head[u];i!=-1;i=edge[i].nxt)
        {
            int v=edge[i].to;
            if(dis[v]==-1&&edge[i].w>0)
            {
                dis[v]=dis[u]+1;    //更新 
                Q.push(v); 
            }
        }
    }
    return dis[T]!=-1;    //判断是否联通。 
}

bool dfs(int u,int exp)
{
    if(u==T)return exp;    //到达重点,所有接受。 
    int flow=0,tmp=0;    
    for(int i=head[u];i!=-1;i=edge[i].nxt)
    {
        int v=edge[i].to;    //下一个点。 
        if(dis[v]==dis[u]+1&&edge[i].w>0)
        {
            tmp=dfs(v,min(exp,edge[i].w));    //往下进行 
            if(!tmp)continue;
            
            exp-=tmp;    //流量限制-流量,后边有判断。 
            flow+=tmp;
            
            edge[i].w-=tmp;        //路径上的边残量减小 
            edge[i^1].w+=tmp;    //流经的边的反向边残量增长。 
            if(!exp)break;    //判断是否在限制边缘 
        }
    }
    return flow;
}

int main()
{
    memset(head,-1,sizeof(head));
    scanf("%d%d%d%d",&n,&m,&S,&T);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);add(y,x,0);    //相邻建边。 
    }
    while(bfs())ans+=dfs(S,inf);
    printf("%d",ans);
 } 
网络最大流模板

 

当前弧优化:

这优化我也不是太熟悉啦。

当前弧优化的意思就是说每次开始跑邻接表遍历不是从第一条边开始跑而是从上一次点i遍历跑到的点.

咱们用cur[i]表示这个点,以后每次建完分层图以后都要进行初始化,且见分层图时不存在当前弧优化.

int deep[N+1];
int q[N+1]= {0},h,t;
int cur[N+1];
bool bfs(int S,int T)
{
    for (int i=0; i<=n; i++) deep[i]=0;    //初始化深度为0
    h=t=1;
    q[1]=S;
    deep[S]=1;
    while (h<=t)
    {
        for (int i=lin[q[h]]; i; i=e[i].next)
            if (!deep[e[i].y]&&e[i].v)       //若未计算过深度且这条边不能是空的
            {
                q[++t]=e[i].y;      //入队一个节点
                deep[q[t]]=deep[q[h]]+1;      //计算深度
            }
        ++h;
    }
    if (deep[T]) return true;
    else return false;
}
int dfs(int start,int T,int minf)
{
    if (start==T) return minf;      //若到了汇点直接返回前面流过来的流量
    int sum=0,flow=0;
    for (int &i=cur[start]; i; i=e[i].next)    //当前弧优化,运用指针在修改i的同时,将cur[start]顺便修改
        if (e[i].v&&deep[start]+1==deep[e[i].y])
        {
            flow=dfs(e[i].y,T,min(minf,e[i].v));      //继续找增广路
            if (!flow) deep[e[i].y]=0;      //去掉已经增广完的点
            sum+=flow;      //统计最大流
            minf-=flow;      //剩余容量
            e[i].v-=flow;
            e[i^1].v+=flow;      //更新剩余容量
            if (!minf) return sum;      //若前面已经流完了,直接返回
        }
    return sum;      //返回最大流量
}
int maxflow(int S,int T)
{
    int sum=0,minf;
    while (1)       //while(1) 控制循环
    {
        if (!bfs(S,T)) return sum;      //bfs求出分层图,顺便判断是否有增广路
        for (int i=1; i<=n; i++)
            cur[i]=lin[i];      //当前弧的初始化
        minf=dfs(S,T,INF);      //dfs求出流量
        if (minf) sum+=minf;      //若流量不为0,加入
        else return sum;      //流量为0,说明没有增广路,返回最大流
    }
}
代码