网络流-最大流

本文介绍了网络流问题的基本概念,包括网络流、后向弧、增广路等,并详细讲解了FF方法、EK算法以及Dinic算法的原理和应用。特别是Dinic算法,通过层次图和DFS进行最大流的求解,同时探讨了当前弧优化的技巧,提高了算法效率。

【前言】

网络流作为一个经典问题,在OI及实际生活中有着广泛的应用,值得我们仔细研究。

【何为网络流?】

网络流,是一种资源调配问题,如下图(以下图片均来自网络):
这里写图片描述
其中,S表示网络流中的源点,是资源的唯一出发点。
T表示网络流中的终点,是各种资源的目的地。
正如水管有粗细之分,道路有宽窄之分,
网络流中的每条边(这里称为弧)都有一个容量cap,表示单位时间最多能通过的资源量。
同时,每条弧都有一个流量flow,表示当前单位时间通过的资源量。
这里写图片描述
上图是流的一个可行方案。我们的目的就是要求最大流(单位时间到达终点的资源最大)

【后向弧】

我们定义后向弧为与前向弧顶点相同,方向相反的弧。且后向弧的流量永远为所对应前向弧的流量的相反数。(这里推荐一个实用方法:把边从2开始编号,那么x与x^1就是一对前向/后向弧)

【增广路】

所谓增广路,就是能使当前结果更优的一条路。在最大流中,增广路的定义如下:

1.增广路由源点出发,终点结束
2.增广路走过的每一条弧中,cap>flow(即这条路必须所有弧都可以增加流量)
3.增广路可以经过前向弧,也可以经过后向弧。

这里写图片描述
上图是一条增广路。其中BC是一条后向弧。同时也发现原来的状态不是最大流。
我们可以发现,后向弧为算法提供了纠正原来错误的可能。

【FF方法】

解决最大流的常见方法是FF方法,FF方法可以由很多不同搜索算法实现,衍生出很多不同的算法。
FF方法的过程:

1.初始时,各弧流量为02.寻找一条增广路,找不到增广路则说明已经获得最大流,退出
3.若找到增广路,取路上各弧的最小残余流量(cap-flow),记为Min
4.把增广路上各弧流量+Min,其对应弧流量作相应修改
5.重复2~4步骤,直至找不到增广路

我们可以发现,FF方法其实就是一个不断纠正,不断优化的过程。其中寻找增广路是次方法的时间瓶颈。

【EK算法】

实际应用时,我们常用BFS来寻找增广路,称为EK算法。

bool bfs(){
    memset(vis,0,sizeof(vis));
    int hed=0,til=1;
    que[1]=s;vis[s]=1;
    while (hed!=til)
     for (int j=a.lnk[que[++hed]];j;j=a.nxt[j])
      if (!vis[a.son[j]]&&a.cap[j]>a.flw[j]){
        que[++til]=a.son[j];vis[a.son[j]]=1;
        fa[a.son[j]]=que[hed];id[a.son[j]]=j;
        if (a.son[j]==t) return 1;
      }
    return 0;
}
void EK(){
    while (bfs()){
        int Min=0x3f3f3f3f;
        for (int x=t;x!=s;x=fa[x])
         Min=min(Min,a.cap[id[x]]-a.flw[id[x]]);
        ans+=Min;
        for (int x=t;x!=s;x=fa[x])
         a.flw[id[x]]+=Min,a.flw[id[x]^1]-=Min;
    }
}

显然,在最坏情况下,时间复杂度O(NE2)

【Dinic算法】

实际运用中,我们还有一个更为快捷的算法求解最大流问题,就是Dinic。

【层次图】

我们把源点到i所经过的最少边数称为i的距离。
距离相同的点归为同一个层次。
在残量网络中,只留下连向下一个层次的边,就构成了一个层次图。
这里写图片描述

【Dinic流程】

首先,如果有层次图,就说明当前解还可以增广
用BFS刷出层次图,然后就可以直接DFS增广

BFS很简单,直接遍历一下就可以了,不用多说
DFS的过程,可以理解为有一杯无限多的水,不断从源点倒入
模拟水流的扩张,遍历整个子图,并返回当前子图能进入的最大流量

代码是这样的:

int dfs(int x,int flow){
    if (x==T||flow==0) return flow;
    int res=0,f;
    for (int j=lnk[x];j;j=nxt[j])
     if (d[x]+1==d[son[j]]&&(f=dfs(son[j],min(flow,cap[j]-flw[j])))>0){
        flw[j]+=f;flw[j^1]-=f;
        res+=f;flow-=f;
        if (flow==0) break;
     }
    return res;
}

【当前弧优化】

其实,上面给出的代码并不是效率最高的。
试想,对于点x,它的一个儿子s如果已经被DFS增广过了
那么下次增广x的时候,就没有必要再来增广s。
因为DFS增广尽可能地使流量最大,再次DFS就不会再有增广的可能

所以我们每次创建lnk[]的一个副本pos[]
那么直接修改pos[]就能达到目的
正确姿势:

int dfs(int x,int flow){
    if (x==T||flow==0) return flow;
    int res=0,f;
    for (int &j=pos[x];j;j=nxt[j])
     if (d[x]+1==d[son[j]]&&(f=dfs(son[j],min(flow,cap[j]-flw[j])))>0){
        flw[j]+=f;flw[j^1]-=f;
        res+=f;flow-=f;
        if (flow==0) break;
     }
    return res;
}

完整模板如下(hihoCoder 1369):

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=505,maxe=40004,INF=0x3f3f3f3f;
int n,e,S,T;
int tot,lnk[maxn],nxt[maxe],cap[maxe],flw[maxe],son[maxe];
void add(int x,int y,int w){
    son[++tot]=y;cap[tot]=w;flw[tot]=0;nxt[tot]=lnk[x];lnk[x]=tot;
}
inline int red(){
    int tot=0,f=1;char ch=getchar();
    while (ch<'0'||'9'<ch) {if (ch=='-') f=-f;ch=getchar();}
    while ('0'<=ch&&ch<='9') tot=tot*10+ch-48,ch=getchar();
    return tot*f;
}
int d[maxn],pos[maxn],que[maxn];
bool bfs(){
    memset(d,63,sizeof(d));
    int hed=0,til=1;
    d[S]=0;que[1]=S;
    while (hed!=til)
     for (int j=lnk[que[++hed]];j;j=nxt[j])
      if (d[son[j]]==INF&&flw[j]<cap[j])
       que[++til]=son[j],d[son[j]]=d[que[hed]]+1;
    return d[T]!=INF;
}
int dfs(int x,int flow){
    if (x==T||flow==0) return flow;
    int res=0,f;
    for (int &j=pos[x];j;j=nxt[j])
     if (d[x]+1==d[son[j]]&&(f=dfs(son[j],min(flow,cap[j]-flw[j])))>0){
        flw[j]+=f;flw[j^1]-=f;
        res+=f;flow-=f;
        if (flow==0) break;
     }
    return res;
}
int main(){
    n=red(),e=red();S=1;T=n;tot=1;
    for (int i=1,x,y,z;i<=e;i++)
     x=red(),y=red(),z=red(),add(x,y,z),add(y,x,0);
    int ans=0;
    while (bfs()){
        memcpy(pos,lnk,sizeof(lnk));
        ans+=dfs(S,INF);
    }
    printf("%d",ans);
    return 0;
}
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值