网络流之sap算法

xiaoxiao2021-02-28  48

         现在想将一些物资从S运抵T,必须经过一些中转站。连接中转站的是公路,每条公路都有最大运载量。

         每条弧代表一条公路,弧上的数表示该公路的最大运载量。最多能将多少货物从S运抵T?

                  这是一个典型的网络流模型。为了解答此题,我们先了解网编流的有关定义和概论。

         若有向图G=(V,E)满足下列条件:       

1.      有且仅有一个顶点S,它的入度为零,即d-(S)=0,这个顶点S便称为源点,或称为发点。

2.      有且仅有一个顶点T,它的出度为零,即d+(T)=0,这个顶点T便称为汇点,或称为收点。

3.      每一条弧都有非负数,叫做这条边的容量。边(vi,vj)的容量用cij表示。

则称之为网络流图,记为G=(V,E,C)。

 

可行流

对于网络流图G,每一条弧(i,j)都给定一个非负数fij,这一组数满足下列三条件时称为这网络的可行流,用f表示它。

1.      每一条弧(i,j)都有fij<Cij

2.      流量平衡

除了源点S和汇点T之外的所有点vi,恒有:∑j(fij)=k(fjk),该等式说明中间点vi的流量守恒,输入与输出量相等。

3.      对于源点S和汇点T有,∑i(fSi)= j(fjT)=V(f)

可增广路

         给定一个可行流f={fij}。若fij=Cij,称<vi,vj>为饱和弧;否则称<vi,vj>为非饱和弧。若fij=0,称<vi,vj>为零流弧;否则称<vi,vj>为非零流弧。

         先定义一条道路P,起点是S,终点是T。把P上所有与P方向一致的弧定义为正向弧,正向弧的全体记为P+;把P上所有与P方向相悖的弧定义为反向孤,反向弧的全体记为P-。

         譬如在图中,P={S,V1,V2,V4,T},那么P+={<S,V1>,<V1,V2>,<V2,V3>,<V4,T>},P-={<V4,V3>}

         给定一个可行流f,P是从S到T的一条道路,如果满足:fij是非饱和流,并且<i,j>∈ P+,fij是非零流,并且<i,j>∈P-,那么就称P是f的一条可增广路。之所以称作“可增广”,是因为可改进路上弧的流量通过一定的规则修改,可以令整个流量放大。

剩余图

剩余图G’=(V,E’)

流量网络=(V,E)中,对于任意一条边(a,b),若flow(a,b)<capacity(a,b) or flow(b,a)>0,则(a,b)∈E’。(可以沿着aàb方向增广。

 

剩余图的权值代表能沿边增广的大小

剩余图中,每条边都可以沿其方向增广

剩余图中,从源点到汇点的每一条路径都对应着一条增广路

 

割切

G={V,E,C}是已知的网络流图,设U是V的一个子集,W=V\U,满足S∈U,T∈W。即U、W把V分成两个不相交的集合,且源点和汇点分属不同的集合。

对于弧尾在U,弧头在W的弧所构成的集合称之为割切,用(U,W)表示。把割切(U,W)中所有弧的容量之和叫做此割切的容量,记为C(U,W),即:

割切示例

        

         上例中,令U={S,V1},则W={V2,V3,V4,T},那么,C(U,W)=<S,V2>+<V1,V2>+<V1,V3>+<V1,V4>=8+4+4+1=17

流量算法的基本理论

定理1:对于已知的网络流图,设任意一可行流为f,任意一割切为(U,W),必有:V(f)<C(U,W).

定理2:可行流f是最大流的充分必要条件是:f中不存在可改进路。

定理3:整流定理。

         如果网络中所有的弧的容量是整数,则存在整数值的最大值。

定理4:最大流最小割定理

         最大流等于最小割,即maxV(f)=minC(U,W)。

 SAP()

求最大流有一种经典的算法,就是每次找增广路时用BFS找,保证找到的增广路是弧数最少的,也就是所谓的Edmonds-Karp算法。可以证明的是在使用最短路增广时增广过程不会超过V*E次,每次BFS的时间都是;O(E),所以Edmonds-Karp的时间复杂度就是O(V*E^2)。

         如果能让每次寻找增广路的时间复杂度降低下来,那么就能够提高算法效率了,使用距离标号的最短增广路算法就是这样的。所谓距离标号,就是某个点到汇点的最小的弧的数量(另外一种距离标号是从源点到该点的最小的弧的数量,本质上没有什么区别)。设点i的标号为D[i],那么如果将满足D[i]=D[j]+1的弧称为允许弧,且增广时只走允许弧,那么就可以达到“怎么走老师最短路”的效果。每个点的初始标号可以在一开始用一次从汇点沿所有的反向边BFS求出,实践中可以初始设全部点的距离标号为0,问题就是如何在增广过程中维护这个距离标号。

         维护距离标号的方法是这样的:当找增广路过程中发现某点出发没有允许弧时,将这个点的距离标号设为由它出发的所有弧的终点的距离标号的最小值加一。维护这个距离标号的方法的正确性我就不证了。由于距离标号的存在,由于“怎么走都是最短路”,所以就可以采用DFS找增广路,用一个栈保存当前路径的弧即可。当某个点的距离标号被改变时,栈中指向它的那条弧肯定还是允许弧了,所以就让它出栈,并继续用栈顶的弧的端点增广。为了使每次找增广路的时间均摊成O(V),还有一个重要的优化是对于每个点保存“当前弧”:初始是当前弧是邻接表的第一条弧;在邻接表中查找时从当前弧开始查找,找到了一条允许弧,就把这条弧设为当前弧;改变距离标号时,把当前弧重新设为邻接表的第一条弧,还有一种在常数上有所优化的写法是改变距离标号的时把当前弧设为那条提供了最小标号的弧。当前弧的写法之所以正确就在于任何时间,我们都能保证在邻接表中当前弧的前面肯定不存在允许弧。

         还有一种常数优化是在每次找到路径并增广完毕之后还要将路径中的所有的顶点退栈,而是只将瓶颈边以及之后的边退栈,这是借鉴了Dinic算法的思想。注意任何时间待增广的“当前点”都应该是栈顶的点的终点。这的确只是一个常数优化,由于当前边结构的存在,我们肯定可以在O(n)的时候内复原路径中瓶颈边之前的所有边。

 

优化:

1.      邻接表优化:如果顶点多,往往n^2存不下,这时候就要存边:存每条边的出发点,终点点和价值,然后排序一下,再记录每个出发点的位置。以后要调用从出发点出发的边时候,只需要从记录的位置开始找就可以(其实可以用链表)。优化是时间快空间节省,缺点是编程复杂度将变大,所以在题目允许的情况下,建议使用邻接矩阵。

2.      GAP优化:如果一次重标号时,出现断层,则可以证明ST无可行流,此时则可以直接退出算法

3.      当前弧优化:为了使每次打增广路的时间变成均摊O(v),还有一个重要的优化是对于每个点保存“当前弧”:初始时当前弧是邻接表的第一条弧;在邻接表中查找时从当前弧开始查找,找到了一条弧,就把这条弧设为当前弧;改变距离标号时,把当前弧设为邻接表的第一条弧。

 

最近发现自己最大流的版有问题,更新了下。使用的时候,除了添加边(之前要初始化),还要设定MaxFlow的起点,终点和点的数目。

[cpp] view plain copy print ? const int MaxN=2005;  const int MaxE=400005;  #define INF 0x3f3f3f3f  struct Edge  {      int u,v,flow,cap,pre;      Edge(){}      Edge(int u,int v,int cap,int pre) :          u(u),v(v),cap(cap),pre(pre) {flow=0;}  }edge[MaxE];    int head[MaxN],nEdge;    void addEdge(int u,int v,int cap)  {      edge[nEdge]=Edge(u,v,cap,head[u]);      head[u]=nEdge++;      edge[nEdge]=Edge(v,u,0,head[v]);      head[v]=nEdge++;  }  void edgeInit() //添加边之前要先初始化。  {      nEdge=0;      memset(head,-1,sizeof(head));  }  struct MaxFlow  {      int st,ed,n,mx_flow;//表示起点,终点,有多少个点,所求的最大流是多少。      int dis[MaxN],gap[MaxN],cur[MaxN],aug[MaxN],path[MaxN];      //dis表示每个点的距离标记,gap表示距离为i的点有多少个,cur用于当前孤优化,      //aug记录找到的增广路流量,path记录找到的增广路的路径。      MaxFlow(){}      MaxFlow(int st,int ed,int n) :          st(st),ed(ed),n(n) { init(); }      void init()      {          for(int i=0;i<MaxN;i++)          {              aug[i]=gap[i]=dis[i]=0;              cur[i]=head[i];          }          aug[st]=INF;    gap[0]=n;          mx_flow=0;      }      int augment(int &point)//修改找到的增广路上的边的容量,当前点修改为起点。      {          for(int i=ed;i!=st;i=edge[path[i]].u)          {              int pair=path[i]^1;              edge[ path[i] ].flow+=aug[ed];              edge[ pair ].flow-=aug[ed];          }          point=st;          return aug[ed];      }      int solve()      {          int u=st;          while(dis[st]<n)          {              if(u==ed) mx_flow+=augment(u);                bool flag=1;              for(int i=head[u];i!=-1;i=edge[i].pre)              {                  int v=edge[i].v;                  if(edge[i].cap-edge[i].flow>0&&dis[u]==dis[v]+1)                  {                      path[v]=i;  cur[u]=i;                      aug[v]=min(aug[u],edge[i].cap-edge[i].flow);                      u=v;                      flag=0; break;                  }              }              if(flag)              {                  if(--gap[dis[u]]==0) return mx_flow;                  dis[u]=MaxN;                  for(int i=head[u];i!=-1;i=edge[i].pre)                  {                      int v=edge[i].v;                      if(edge[i].cap-edge[i].flow>0) dis[u]=min(dis[u],dis[v]+1);                  }                  gap[dis[u]]++;                  cur[u]=head[u];                  if(u!=st) u=edge[path[u]].u;              }          }          return mx_flow;      }  }sap;   const int MaxN=2005; const int MaxE=400005; #define INF 0x3f3f3f3f struct Edge { int u,v,flow,cap,pre; Edge(){} Edge(int u,int v,int cap,int pre) : u(u),v(v),cap(cap),pre(pre) {flow=0;} }edge[MaxE]; int head[MaxN],nEdge; void addEdge(int u,int v,int cap) { edge[nEdge]=Edge(u,v,cap,head[u]); head[u]=nEdge++; edge[nEdge]=Edge(v,u,0,head[v]); head[v]=nEdge++; } void edgeInit() //添加边之前要先初始化。 { nEdge=0; memset(head,-1,sizeof(head)); } struct MaxFlow { int st,ed,n,mx_flow;//表示起点,终点,有多少个点,所求的最大流是多少。 int dis[MaxN],gap[MaxN],cur[MaxN],aug[MaxN],path[MaxN]; //dis表示每个点的距离标记,gap表示距离为i的点有多少个,cur用于当前孤优化, //aug记录找到的增广路流量,path记录找到的增广路的路径。 MaxFlow(){} MaxFlow(int st,int ed,int n) : st(st),ed(ed),n(n) { init(); } void init() { for(int i=0;i<MaxN;i++) { aug[i]=gap[i]=dis[i]=0; cur[i]=head[i]; } aug[st]=INF; gap[0]=n; mx_flow=0; } int augment(int &point)//修改找到的增广路上的边的容量,当前点修改为起点。 { for(int i=ed;i!=st;i=edge[path[i]].u) { int pair=path[i]^1; edge[ path[i] ].flow+=aug[ed]; edge[ pair ].flow-=aug[ed]; } point=st; return aug[ed]; } int solve() { int u=st; while(dis[st]<n) { if(u==ed) mx_flow+=augment(u); bool flag=1; for(int i=head[u];i!=-1;i=edge[i].pre) { int v=edge[i].v; if(edge[i].cap-edge[i].flow>0&&dis[u]==dis[v]+1) { path[v]=i; cur[u]=i; aug[v]=min(aug[u],edge[i].cap-edge[i].flow); u=v; flag=0; break; } } if(flag) { if(--gap[dis[u]]==0) return mx_flow; dis[u]=MaxN; for(int i=head[u];i!=-1;i=edge[i].pre) { int v=edge[i].v; if(edge[i].cap-edge[i].flow>0) dis[u]=min(dis[u],dis[v]+1); } gap[dis[u]]++; cur[u]=head[u]; if(u!=st) u=edge[path[u]].u; } } return mx_flow; } }sap;

转载请注明原文地址: https://www.6miu.com/read-81346.html

最新回复(0)