浅谈网络流

来源:互联网 发布:2ne1 解散 知乎 编辑:程序博客网 时间:2024/05/16 11:37

网络流

网络流的基本定义就不说了,这里主要讲讲我学习网络流的一些心得.

注意

①最大流的图一定是一个有向图

②FF,EK算法的核心在于添加反向边

③SAP和Dinic分别对于稀疏图稠密图更占优势!两个算法都要掌握!

④对于每一次添加完反向边后得到的新图可称之为“残余网络”!

⑤一条弧即对应着一条边

⑥增广路用的很广,网络流,匈牙利等算法都运用到了这个思想,其实就是每次用DFS或BFS找一条路径,去更新答案.


最大流


模型

给定一个源点s和汇点t,中间有一些管道把它们有向的连接起来,每个管道可以流一定限度量的水,而源点有无限的水,求从源点s开始,最多能流多少的水到汇点t中.


算法一:Ford-Fulkerson

此算法的想法非常简单:

每次在图里从源点到汇点找一条可行路径,然后把这条路塞满.

这条路径上最小的容量限度就是这整条路径的可行流量.

然后再把路径上的每条边的限度流量,都减去可行流量.

再把新得到的图继续以上操作,直到不能再有流量通过,即完成了求解.

但如果只这么贪心,显然是有问题的.

如下图:
这里写图片描述

源点A,汇点F.

一条路径ABEF,最大流量为3,并把对应的边AB,BE,EF都减去了3流量,然后发现不能再流水了,于是这么走的流为3.

但显然,有一种更优的策略:
ABCF2流量)
ABEF1流量)
ADEF2流量)
总共5流量.

然后发现,在走BE时,只需退回去一些流量,走CF,就会有最优解产生.

所以此算法最重要的一步就是添加反向边.

我们把对应的边BA,EB,FE连上,那么就有路径ADEBCF2流量).

于是添加反向边后,就有总共2+3=5流量.

添加反向边的目的就是让贪心起来,倒一些水回去,使得贪心最优.


如何理解反向边

首先,为什么要去理解它?

是因为,不知道为什么添加了反向边答案只会优而不会错?为什么添了反向边留回来的水一定会被另一条道再流过去?

这里写图片描述

看上图.

原本有一条b->a的路径,假设可以流n的流量,添加反向边a->b后,假设可以再流n-k的流量过来.

于是汇点可能多了总共2n-k的流量.

现在要证明的是,这2n-k的流量是可以从源点到达汇点的.

很显然,到a点的流量,通过a->b这一条路径流向了b,再由b流向汇点.

但是从a->b的流量一定不会超过n,所以从a->b的流量可以直接从a流向汇点,同理,b->a的流量也可以直接从b流向汇点.

于是这2n-k的流量就可以到汇点了.

具体的流量分配如下:
这里写图片描述

当然,这样子的路径只有一条,所以好理解.

当边多时其实只要明白对于每一条路径添加反向边都是正确的,答案只会优而不会错,从而对于全局一条路径都填上反向边也是正确的.

FF弊端

但这种方法也有很大的弊端,因为如果有一个流量的很大的管道,却每次连接的是一个很小的管道,效率就低了.

如何避免此算法的弊端?


算法二:Edmonds-Karp

对于算法一FF,我们有两个优化:

1.每次找弧数最少的一条增广路增广,保证不多走,其实就是把FF的深搜改成bfs.

2.每次增广时找一条流量限度尽量大的路,保证不少流.

这两点就可有效的避免上述算法的弊端.

具体实现中还设了ai表示当前源点到每个点的累积流量.

假设当前拓展点为x,则我们只需把与x所连的所有点都找一遍,看看是否能更新对应的a[xi],最后at即为最大流.

因为每次bfs的时间复杂度是O(e),增广的次数不超过O(Ve),这样的时间复杂度是O(ve2)的.

注意模板中实现的一些细节,可以使常数变小,如下面这个code是蒟蒻我无意中找到的一个模板,用了两个while,还比较好理解.

Code

var        n,m,i,j,x,y,p,max,tot,total:longint;        f:array[0..2000,0..2000] of longint;        a,b:array[0..2000] of longint;        bz:array[0..2000] of boolean;function min(x,y:longint):longint;begin        if x<y then exit(x) else exit(y);end;begin        readln(n,m);        for i:=1 to n do        begin                readln(x,y,p);                inc(f[x,y],p);        end;        while true do        begin                fillchar(a,sizeof(a),0);                fillchar(b,sizeof(b),0);                fillchar(bz,sizeof(bz),0);                a[1]:=maxlongint;   //很重要的一步,是一个处理技巧.                while true do                begin                        tot:=0;                        for i:=1 to m do                                if not bz[i] and (a[i]>a[tot]) then tot:=i;  //每次找当前没有走过点的最大容量的点,并从这个点继续往下输送流量                        bz[tot]:=true;              //tot即为当前输送流量的点——拓展点                        max:=a[tot];                        if (tot=0) then                        begin                                writeln(total);                                halt;                        end                        else                        if tot=m then break;                        for i:=1 to m do                        begin                                p:=min(max,f[tot,i]);   //保证不能超过拓展点的最大容量                                if p>a[i] then                                begin                                        a[i]:=p;        //取最大流                                        b[i]:=tot;      //记录路径                                end;                        end;                end;                inc(total,a[m]);                       //ai即代表从源点到i当前bfs路径的最大容量                i:=m;                while i>1 do                begin                        dec(f[b[i],i],a[m]);                        inc(f[i,b[i]],a[m]);                        i:=b[i];                end;                                   //添加反向边        end;        writeln(total);end.

算法三:SAP

SAP的核心思想是减少增广的次数,提高效率.

一个定义

我们设dis(i)表示第i个点到汇点t所需经过弧的最少数量.

dis(i)表示的是i对于t的距离标号.

这也是SAP的核心

允许弧

当满足dis(i)=dis(j)+1时,则(i,j)这条弧称为允许弧,每次增广时只要保证走的是允许弧,那么就可以提高效率了.

因为会有残余网络,所以dis(i)肯定是会变的.

一开始的dis(i)全部为0,那么如果从一个拓展点i找不到允许弧时怎么办?

距离标号

当找不到允许弧时,SAP的思想是改变距离标号.

我们就可以把dis(i)赋值为 其所有还存在弧项相连的点中的最小dis(j)+1.

正确性显然的.

储存流量

此外我们还需要一个bz(标记)数组,用于储存拓展点的剩余流量,有什么用呢?

这里写图片描述

我们走1>2>3时,2>3这条路经拓展时,dis[2]=dis[3]=0,所以dis[2]会赋值为与其相连的点的最小dis(j)+1,也就是赋值为min{dis(3),dis(4)}+1,当dis[2]=1时,再走2>3这条路径.

可是走完后,我们发现,还有2>4可走,所以,当我们走完2>3这条路径反回时,要把当前的最大流量更新为2的最大流量40

经过标记后正确性显然.

从中也可以很明显地看出“储存流量”的作用.

从头再来

SAP一定要注意点,就是当找到一条增广路的时候,不是再继续往下找,而是从源点再找!

这点也是与dinic最不相同的地方,导致了为什么适用于不同的图.

SAP-1

var        flag:boolean;        f:array[0..1000,0..1000] of longint;        dis,pre,bz:array[0..10000] of longint;        x,y,z,n,m,i,j,k,max,min,ans:longint;begin             assign(input,'SAP1.in'); reset(input);        assign(output,'SAP1.out'); rewrite(output);        readln(n,m);        if n=0 then                           //特殊判断        begin                writeln(0);                halt;        end;        for i:=1 to n do        begin                readln(x,y,z);                 inc(f[x,y],z);               //构图        end;         i:=1;        max:=maxlongint;        while dis[1]<m do                    //dis[1]即1的距离标号        begin                bz[i]:=max;                  //储存流量                flag:=false;                for j:=1 to m do                        if (f[i,j]>0) and (dis[j]+1=dis[i]) then                        begin                                if f[i,j]<max then max:=f[i,j];   //更新流量                                flag:=true;                                pre[j]:=i;               //记录前驱                                i:=j;                                if i=m then              //找到一条增广路,并从源点开始重新拓展                                begin                                        inc(ans,max);                                        while i<>1 do                                        begin                                                dec(f[pre[i],i],max);                                                inc(f[i,pre[i]],max);                                                i:=pre[i];                                        end;                                        max:=maxlongint;                                end;                                break;                        end;                if flag then continue;   //找到了增广路就无需更新距离标号                min:=m-1;                for j:=1 to m do                        if (f[i,j]>0) and (dis[j]<min) then                        begin                                k:=j;                                min:=dis[j];                        end;           //更新距离标号                dis[i]:=min+1;                            if i<>1 then                begin                        i:=pre[i];                        max:=bz[i];    //退一步重新再找                end;        end;        writeln(ans);        close(input); close(output);end.

但实际上,SAP还有两个优化,即当前弧优化和GAP断层优化.


当前弧优化

我们先用一个h数组记录当前可以流的点.

显然初始值h全为1.

那么每次从一个节点i,拓展到另一个节点j,则可以肯定ii+1j1中没有可拓展的点,那j肯定也不能拓展这些点,所以下次拓展时,就直接从j开始.

如果是在更新距离标号时,也可以同样更新h中的值.


SAP-2

var        flag:boolean;        f:array[0..1000,0..1000] of longint;        dis,pre,bz,h:array[0..10000] of longint;        x,y,z,n,m,i,j,k,max,min,ans:longint;begin        assign(input,'SAP2.in'); reset(input);        assign(output,'SAP2.out'); rewrite(output);        readln(n,m);        if n=0 then        begin                writeln(0);                halt;        end;        for i:=1 to n do        begin                readln(x,y,z);                inc(f[x,y],z);        end;        for i:=1 to m do                h[i]:=1;                  //初值        i:=1;        max:=maxlongint;        while dis[1]<m do        begin                bz[i]:=max;                flag:=false;                for j:=h[i] to m do        //当前弧优化                        if (f[i,j]>0) and (dis[j]+1=dis[i]) then                        begin                                if f[i,j]<max then max:=f[i,j];                                flag:=true;                                pre[j]:=i;                                h[i]:=j;       //注意,不可以是j+1                                i:=j;                                if i=m then                                begin                                        inc(ans,max);                                        while i<>1 do                                        begin                                                dec(f[pre[i],i],max);                                                inc(f[i,pre[i]],max);                                                i:=pre[i];                                        end;                                        max:=maxlongint;                                end;                                break;                        end;                if flag then continue;                min:=m-1;                for j:=1 to m do                        if (f[i,j]>0) and (dis[j]<min) then                        begin                                k:=j;                                min:=dis[j];                        end;                h[i]:=k;             //注意,不可以是k+1                dis[i]:=min+1;                if i<>1 then                begin                        i:=pre[i];                        max:=bz[i];                end;        end;        writeln(ans);        close(input); close(output);end.

GAP优化

假设一次重新标号后,出现距离断层,也就是距离标号为i的点没有了,那么自然不可能流向距离标号为i+1的点,那么可证此图就不存在可行流了.


虽然这个优化非常简单,但实际意义很大.

SAP-3

var        flag:boolean;        f:array[0..1000,0..1000] of longint;        h,d,bz,dis,pre:array[0..100000] of longint;        x,y,z,n,m,i,j,k,max,ans,min:longint;begin        readln(n,m);        for i:=1 to n do        begin                readln(x,y,z);                inc(f[x,y],z);        end;        d[0]:=m;                      //di表示距离标号为i的点的个数        for i:=1 to m do                h[i]:=1;        i:=1;        max:=maxlongint;        while dis[1]<m do        begin                bz[i]:=max;                flag:=false;                for j:=h[i] to m do                        if (f[i,j]>0) and (dis[i]=dis[j]+1) then                        begin                                if f[i,j]<max then max:=f[i,j];                                h[i]:=j;                                pre[j]:=i;                                i:=j;                                flag:=true;                                if i=m then                                begin                                        inc(ans,max);                                        while i>1 do                                        begin                                                dec(f[pre[i],i],max);                                                inc(f[i,pre[i]],max);                                                i:=pre[i];                                        end;                                        max:=maxlongint;                                end;                                break;                        end;                if flag then continue;                min:=m-1;                for j:=1 to m do                        if (f[i,j]>0) and (dis[j]<min) then                        begin                                min:=dis[j];                                k:=j;                        end;                h[i]:=k;                dec(d[dis[i]]);             //更新                if d[dis[i]]=0 then break;  //GAP优化                dis[i]:=min+1;                              inc(d[dis[i]]);             //更新                if i>1 then                begin                        i:=pre[i];                        max:=bz[i];                end;        end;        writeln(ans);end.

算法四:Dinic

我是先学Sap,再学dinic,两个算法其实核心思想没有什么太大的不同,关键在于细节方面的不同导致了两个算法的优缺点.

dinic之所以快,在于他每次找完一条路径后是利用Dfs的特性直接往回跳到还能继续走的地方,而sap是从头再来,所以这里dinic在边数较多的情况下比较占优.

但dinic需要每次都分层,而sap是直接在“一次bfs”中分层,基于这一点,sap更优.


好啦好啦,以上都是关于dinic和Sap的不同,下面我们来具体讲解一下有关dinic的相关知识.


在了解dinic之前,我们可以接触一下这个算法:MPLA

这个算法的步骤是:

①初始化流量,计算出剩余图
②根据剩余图计算层次图。若汇点不在层次图内,则算法结束
③在层次图内不断用bfs增广,直到层次图内没有增广路为止
④转步骤②


然后我们可以惊奇的发现,dinic其实就是把步骤③中的用bfs增广改成dfs.

但这样可以大大提高效率,具体原因在上面讨论sap和dinic不同时已经讲过.

贴一个优(chou)美(lou)的代码:

var        d,tov,next,last,len,dis:array[1..400] of longint;        n,m,i,j,tot,head,tail,x,y,z,ans:longint;function min(x,y:longint):longint; begin if x<y then exit(x); exit(y); end;procedure insert(x,y,z:longint);begin        inc(tot);        len[tot]:=z;        tov[tot]:=y;        next[tot]:=last[x];        last[x]:=tot;end;function bfs:boolean;begin        head:=0;        tail:=1;        d[tail]:=1;        fillchar(dis,sizeof(dis),0);        dis[tail]:=1;        while head<tail do        begin                inc(head);                x:=last[d[head]];                while x>0 do                begin                        if (dis[tov[x]]=0) and (len[x]>0) then                        begin                                inc(tail);                                d[tail]:=tov[x];                                dis[tov[x]]:=dis[d[head]]+1;                        end;                        x:=next[x];                end;        end;        exit(dis[m]>0);end;function dfs(k,max:longint):longint;var        x,flow:longint;begin        if (k=m) or (max=0) then exit(max);        dfs:=0;        x:=last[k];        while x>0 do        begin                if (dis[tov[x]]=dis[k]+1) and (len[x]>0) then                begin                        flow:=dfs(tov[x],min(max,len[x]));                        if flow>0 then                        begin                                dec(len[x],flow);                                inc(len[x+1],flow); //前向星导致如此更值                                dec(max,flow); //注意这一步,dfs的关键                                inc(dfs,flow); //返回一个ans值供答案更新                                if max=0 then break;//这里只是为了常数                        end;                end;                x:=next[x];        end;end;begin        readln(n,m);        for i:=1 to n do        begin                readln(x,y,z);                insert(x,y,z);                insert(y,x,0);        end;        while bfs do ans:=ans+dfs(1,maxlongint);        writeln(ans);end.


最小割

割的定义

若在一个图里,每条边都有一些权值,删去一些边后,源点和汇点不联通,则删去的这些边叫做图的一个割集.

抽象化的说,如果把边集V中删掉集合V中的边,剩下的边集V′′如果不是一个联通分量,则可称V是图的一个割集.

最小割的定义

既然都知道了割,那么显然最小割就是每种割法中删去的边的权值和的最小值.

一个定理

最小割=最大流.

于是最小割就被完美解决了.

最大权闭合子图

闭合子图的定义

若一个图中任意点的后继也都在图中,则这个图称为闭合子图.

最大权闭合子图

顾名思义就是所有闭合子图中点权和最大的图.

分集——二分图

我们把点集V划分为两个集合S,T,其中源sS,汇tT,把点权为正的全部向s连一条边,点权为负的全部向t连一条边,原图的所有边设为正无穷.

这样的分集后形成的图为二分图.

那么有以下几个简单的结论:

①最小割为简单割

②闭合图是简单割

③简单割是闭合图

④最小割所产生的两个联通分量中,源点所在的联通分量的点权和为最大权闭合子图

证明:
我们先定义s所在集合为Xt所在集合为Y.
ZX表示集合X中点权为正的点权和,ZY表示集合Y中点权为正的点权和,FX表示集合X中点权为负的点权和,FY表示集合Y中点权为负的点权和.

先设一个简单割的容量为C,则有C=ZY+FX.
再设闭合图X集合的权值和为W,则有W=ZX|FX|

C+W=TOT=ZY+ZX|FX|+FX=ZY+ZX

tot为整个图里所有点权为正的点权和.

因为tot是个定值,而我们想让W最大,则C必须最小,于是就把简单割C转化为求其最小割,最小割时的C自然最小,此时其源点s所在的闭合图X的点权和最大,为最大权闭合子图,于是得证.

0 0
原创粉丝点击