网络流

8、网络流
8.1、最大流的介绍
一个水厂需要从河里取水,河边有一个水泵,从河到水厂的中间有很多中转站,中转站、水泵和水厂之间有水管相连。每根水管有一个容量,表示每秒通过该水管的最大流量是多少立方米。现在问,水泵每秒最多能向水厂输送多少立方米的水?

这就是典型的最大流问题。下面给出比较形式化的定义:
给定一个有向图G=(V,E),在E集合里的每条边都由三个元素来描述(u,v,c),分别表示起点、终点和容量,并指定一个源点S和一个汇点T,一个流必须满足容量限制和流量守恒。
容量限制:f(u,v)<=c(u,v),形象地理解就是流速不能超过水管的限制。
流量守恒:

形象地理解就是中转站不能把水吞掉了。
上面说的一个可行流,而所谓最大流就是要最大化:

其实就是流进汇点的流量。一般我们说一个流的流量都是指流进汇点的流量。
8.2、最大流最小割定理
割的定义:对于边集E’,若满足去掉该边集中所有边以后,源点和汇点不再连通,那么称边集E’为一个割。割的权定义为对应边集中的所有边的容量之和。最小割指的就是权最小的割。
最大流最小割定理实际上就是说:最大流的流量和最小割的权在数值上相等。
8.3、残余网络
残余网络指的其实是把所有边的权更新为w(u,v)=c(u,v)-f(u,v)。
显然初始时有
w(u,v)=c(u,v)
f(u,v)=0
形象地看,就是说这个水管还能容纳多少流量。一般地,我们只需要注意残余网络就可以求出最大流。为了方便下面都用w(u,v)来表示(u,v)这条边的残余流量。
为了实现下面的增广路算法,这里引入反向弧的概念。所谓反向弧,其实是用以修正前面错误的流的一种修正边。对于原图的每条有向边,我们都给它配对一条反向弧,用以修正该条有向边上错误的流。
假设给定的有向边是(u,v),那么给它配对的反向弧就是一条(v,u)这样反向的边并且满足以下这个性质:
w(v,u)=f(u,v)
所以有推论w(v,u)+w(u,v)=c(u,v)
因为流的正反向抵消性质(水从u流到v,再从v流回u,其实就是什么都没发生):
1、如果通过反向弧(v,u)的残余流量增加了流量t,其实相当于f(u,v)=f(u,v)-t,按照上面的性质,其实就是
w(v,u)=w(v,u)-t
w(u,v)=w(u,v)+t
2、如果通过正向弧(v,u)的残余流量增加了流量t,其实相当于f(u,v)=f(u,v)+t,按照上面的性质,其实就是
w(v,u)=w(v,u)+t
w(u,v)=w(u,v)-t
发现了吗?它们是轮换对称的。所以在实际实现中并不需要区分哪条是正向边,哪条是反向弧,假如利用当前边增加了流量t,那么就将当前边的残余流量减去t,再将对应的另一条加上t就可以了。为了方便,我们称正向边和反向弧互为反边,并用op(e)表示e这条边的反边。
8.4、基于最大流最小割定理求最大流的算法(统称Ford-Fulkerson算法)
Ford-Fulkerson是求最大流很经典的算法。该算法就是不断在残余网络中寻找增广路并增广,直到找不到增广路为止(也就是说,此时源点和汇点不连通,存在割)。下面给出增广路和增广的含义。
增广路:一条从起始于源点S,终止于汇点T的路径。且必须满足路径上的每条边的残余流量都大于0。
增广:增广是对于一条增广路来说的。令拼接成增广路的有向边组成边集E’。再令 ,然后对于,进行下面操作:
w(e)=w(e)-delta
w(op(e))=w(op(e))+delta
然后流进汇点T的流就加上delta
下面介绍的各种方法其实本质是利用不同的方法来找增广路以达到提高效率的目的。
8.4.1、增广路算法

const
  maxn=200;
var
  c:array[1..maxn,1..maxn] of longint;
  b:array[1..maxn] of longint;
  sum,s,t,n,m:longint;

function min(a,b:longint):longint;
begin
   if a>b then exit(b) else exit(a);
end;

function findflow(k:longint):boolean;
var
  i:longint;
begin
  if k=t then exit(true);
  for i:=1 to n do
    if (b[i]=-1) and (c[k,i]>0)
      then begin
             b[i]:=k;
             if findflow(i) then exit(true);
           end;
  exit(false);
end;

procedure addflow;
var
  i,d:longint;
begin
  d:=maxlongint;
  i:=t;
  while b[i]<>0 do
    begin
      if c[b[i],i]>0 then d:=min(d,c[b[i],i]);
      i:=b[i];
    end;
  i:=t;
  while b[i]<>0 do
    begin
      dec(c[b[i],i],d);
      inc(c[i,b[i]],d);
      i:=b[i];
    end;
  inc(sum,d);
end;

procedure init;
var
  i,x,y,w:longint;
begin
  readln(m,n);
  s:=1;t:=n;
  for i:=1 to m do
    begin
       readln(x,y,w);
       inc(c[x,y],w);
    end;
end;

procedure main;
var
  i,j:Longint;
begin
  for i:=1 to n do b[i]:=-1;
  b[s]:=0;
  while findflow(s) do
     begin
       addflow;
       for i:=1 to n do b[i]:=-1;
       b[s]:=0;
     end;
end;

procedure print;
var
  i,j:longint;
begin
  writeln(sum);
end;

begin
   init;
   main;
   print;
end.

8.4.2、最大容量增广路算法
这个算法在USACO上有介绍。实际上它每次利用类似dijkstra和prim算法的方式,找出增广时delta值最大的增广路,并对这条增广路进行增广。这个算法一般有不错的效率,但是在实际运用中,往往比不上后面介绍的几种最短增广路算法。
8.4.3、最短增广路算法
下面给出的几种算法都是每次找包含边数最少的增广路增广的。运用在信息竞赛上基本都是这几种方法。
8.4.3.1、Edmonds-Karp
该算法是最简单的最短增广路算法。每次使用广度优先搜索寻找增广路,并对其进行增广。

8.4.3.2、Dinic
这个算法的效率是相当高的,算法流程如下:
1、每次以源点为起始点bfs,求出每个点的编号d[i],d[i]表示从源点到i点通过至少几条残余流量大于0的边,能够到达i点。
2、只有那些满足d[u]+1=d[v]的边(u,v)才被视为存在,然后在这里面不断DFS找增广路并增广(其实随便走都可以),如果没有增广路了,那么返回步骤1,如果BFS不到汇点,证明算法结束。
该算法在第二步DFS有些优化,实现时可以自己想一想。一般不怎么加优化这个算法的效率都很不错了。
8.4.3.3、SAP(推荐使用)
SAP同样是效率很高的一个算法,与dinic想比,他只需要两种优化,就能达到dinic在DFS里面加一大堆优化的效率。下面描述该算法的流程:
首先给每个点赋予一个距离标号:d[i]。只有满足d[i]=d[j]+1时,边(i,j)才被视为存在的(暂且称为容许边)。这个标号的意义是到汇点的最短距离。
算法的一开始,我们从汇点进行一次BFS求出距离标号(实际上这一步可以省去)。然后从源点开始递归操作。
对于一个点i的操作:
如果存在容许边(i,j),当j是汇点时增广并从源点开始递归。否则就令i是j的前驱,并递归j。
如果不存在容许边,则对i重标号,使得d[i]=min{d[j]}+1 (w(i,j)>0),并回溯。(可以证明重标号操作必然是令d[i]增加的)
当d[s]>=n时算法停止,此时不存在任何一条增广路。
两个重要优化分别是当前弧优化和间隙优化。
当前弧优化:对于一个点i,如果之前递归到这里,并且已经枚举过一些不能增广的边,那么可以证明在这个点重标号之前,之前已无法增广的边不会再次成为容许边。所以记录每个点的出边枚举到那一条,下次直接从这一条开始枚举即可。
间隙优化:假如整个图中,不存在d[i]=k且存在d[i]>k和d[i]

var 
  e,vs,vt,e,n,flow:longint; 
  lastside,d,cur,num,fa:array [0..1000000] of longint; //cur是弧优化的数组 d是标号数组 num是记录这个标号有多少个点的数组。 
  side:array [1..1000000] of record 
                 x,y,c,op,next:longint; 
                end; 
procedure add(x,y,c:longint); 
begin 
  inc(e); 
  side[e].x:=x; side[e].y:=y; side[e].c:=c; side[e].op:=e+1; 
  side[e].next:=lastside[x]; lastside[x]:=e; 
  inc(e); 
  side[e].y:=x; side[e].x:=y; side[e].c:=0; side[e].op:=e-1; 
  side[e].next:=lastside[y]; lastside[y]:=e; 
end; 
procedure init; 
var 
  i,j,x,y,c,m:longint; 
begin 
  readln(n,m); 
  for i:=1 to m do 
    begin 
      readln(x,y,c); 
      add(x,y,c); 
    end; 
   vs:=1; vt:=n; 
end; 

procedure remark(k:longint); //重标号
var 
  i,min:longint; 
begin 
  min:=n-1; 
  cur[k]:=lastside[k]; 
  i:=cur[k]; 
  while i>0 do   //与当前点相连的边中找出最标号
    with side do   
      begin 
        if (c>0) and (d[y]<min) then min:=d[y]; 
        i:=next; 
      end; 
  d[k]:=min+1;  
end; 

procedure change; 
var 
  j,nf:longint; 
begin 
  j:=vt; nf:=maxlongint; 
  while j<>vs do 
    with side[fa[j]] do 
      begin 
        if c<nf then nf:=c; 
        j:=x; 
      end; 
  j:=vt; 
  inc(flow,nf); 
  while j<>vs do 
    with side[fa[j]] do 
      begin 
        dec(c,nf); 
        inc(side[op].c,nf); 
        j:=x; 
      end; 
end; 

procedure sap; 
var 
  i,temp:longint; 
begin 
  for i:=1 to n do 
    cur[I]:=lastside[I]; 
  num[0]:=n; //用桶来标记D[i]值的个数 ,num[i]表示到终点的距离为i的点有多少个
  i:=vs; 
  while d[vs]<n do 
    begin 
      while cur[I]>0 do 
        with side[cur[I]] do 
          if (c>0)and(d[y]+1=d[x]) then break  //找到容许边
                        else cur[I]:=next; 
      if cur[I]=0 then //找不到容许边
        begin 
          dec(num[d[I]]); 
          if num[d[I]]=0 then break;   //间隙优化,表示断层
          remark(i); 
          inc(num[d[I]]); 
          if i<>vs then i:=side[fa[I]].x; //如果不是源点,退上一层
        end 
      else 
        begin 
          fa[side[cur[I]].y]:=cur[I]; //记录父节点
          i:=side[cur[I]].y; 
          if i=vt then //找一增广路
            begin 
              change;                   //修改增广路流量 
              i:=vs; 
            end; 
        end; 
    end; 
end; 
begin 
  init; 
  sap; 
  writeln(flow); 
end.  

8.4.3、总结
网络流算法还有另一类算法,它们都是基于预流推进思想的,有兴趣可以看一看。然而用于竞赛中,sap的效率在绝大多数情况下已经足够了。所以如果想要熟练使用一种算法,并在竞赛中快速敲出,推荐sap。另外如果把dinic的优化仔细推敲好了,dinic的效率不会逊色于sap。当然,前提是能在竞赛时熟练敲出。
另外,费用流也是用到这种Ford-Fulkerson算法。
POJ 1273 Drainage Ditches
POJ 1274 The Perfect Stall (二分图匹配)
POJ 1698 Alice’s Chance
POJ 1459 Power Network
POJ 2112 Optimal Milking (二分)
POJ 2455 Secret Milking Machine (二分)
POJ 3189 Steady Cow Assignment (枚举)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值