最大流算法之三:ISAP <

最大流算法之三:ISAP <转>

  (2009-08-14 19:24:27)
标签: 

it

分类: 理论

    通常的 SAP 类算法在寻找增广路时总要先进行 BFS,BFS 的最坏情况下复杂度为 O(E),这样使得普通 SAP 类算法最坏情况下时间复杂度达到了 O(VE2)。为了避免这种情况,Ahuja 和 Orlin 在1987年提出了Improved SAP 算法,它充分利用了距离标号的作用,每次发现顶点无出弧时不是像 Dinic 算法那样到最后进行 BFS,而是就地对顶点距离重标号,这样相当于在遍历的同时顺便构建了新的分层网络,每轮寻找之间不必再插入全图的 BFS 操作,极大提高了运行效率。国内一般把这个算法称为 SAP...显然这是不准确的,毕竟从字面意思上来看 E-K 和 Dinic 都属于 SAP,我还是习惯称为 ISAP 或改进的 SAP 算法。

    与 Dinic 算法不同,ISAP 中的距离标号是每个顶点到达终点 t 的距离。同样也不需显式构造分层网络,只要保存每个顶点的距离标号即可。程序开始时用一个反向 BFS 初始化所有顶点的距离标号,之后从源点开始,进行如下三种操作:(1)当前顶点 i 为终点时增广 (2) 当前顶点有满足 dist[i] = dist[j] + 1 的出弧时前进 (3) 当前顶点无满足条件的出弧时重标号并回退一步。整个循环当源点 s 的距离标号 dist[s] >= n 时结束。对 i 点的重标号操作可概括为 dist[i] = 1 + min{dist[j] : (i,j)属于残量网络Gf}。具体算法描述如下:

algorithm Improved-Shortest-Augmenting-Path
 1 f <-- 0
 2 从终点 t 开始进行一遍反向 BFS 求得所有顶点的起始距离标号 d(i)
 3 i <-- s
 4 while d(s) < n do
 5     if i = t then    // 找到增广路
 6         Augment
 7         i <-- s      // 从源点 s 开始下次寻找
 8     if Gf 包含从 i 出发的一条允许弧 (i,j)
 9         then Advance(i)
10         else Retreat(i)    // 没有从 i 出发的允许弧则回退
11 return f

procedure Advance(i)
1 设 (i,j) 为从 i 出发的一条允许弧
2 pi(j) <-- i    // 保存一条反向路径,为回退时准备
3 i <-- j        // 前进一步,使 j 成为当前结点

procedure Retreat(i)
1 d(i) <-- 1 + min{d(j):(i,j)属于残量网络Gf   // 称为重标号的操作
2 if i != s
3     then i <-- pi(i)    // 回退一步

procedure Augment
1 pi 中记录为当前找到的增广路 P
2 delta <-- min{rij:(i,j)属于P}
3 沿路径 P 增广 delta 的流量
4 更新残量网络 Gf

 算法中的允许弧是指在残量网络中满足 dist[i] = dist[j] + 1 的弧。Retreat 过程中若从 i 出发没有弧属于残量网络 Gf 则把顶点距离重标号为 n 。

 虽然 ISAP 算法时间复杂度与 Dinic 相同都是 O(V2E),但在实际表现中要好得多。要提的一点是关于 ISAP 的一个所谓 GAP 优化。由于从 s 到 t 的一条最短路径的顶点距离标号单调递减,且相邻顶点标号差严格等于1,因此可以预见如果在当前网络中距离标号为 k (0 <= k < n) 的顶点数为 0,那么可以知道一定不存在一条从 s 到 t 的增广路径,此时可直接跳出主循环。在我的实测中,这个优化是绝对不能少的,一方面可以提高速度,另外可增强 ISAP 算法时间上的稳定性,不然某些情况下 ISAP 会出奇的费时,而且大大慢于 Dinic 算法。相对的,初始的一遍 BFS 却是可有可无,因为 ISAP 可在循环中自动建立起分层网络。实测加不加 BFS 运行时间差只有 5% 左右,代码量可节省 15~20 行。

 

算法效率

O(E^2*V)

 

模板:

#include<cstdio>
#include<memory>
using namespace std;

const int maxnode = 1024;
const int infinity = 2100000000;

struct edge{
   int ver;    // vertex
   int cap;    // capacity
   int flow;   // current flow in this arc
   edge *next; // next arc
   edge *rev;  // reverse arc
   edge(){}
   edge(int Vertex, int Capacity, edge *Next)
   :ver(Vertex), cap(Capacity), flow(0), next(Next), rev((edge*)NULL){}
   void* operator new(size_t, void *p){
   return p;
   }
}*Net[maxnode];
int dist[maxnode]= {0}, numbs[maxnode] = {0}, src, des, n;

void rev_BFS(){
   int Q[maxnode], head = 0, tail = 0;
   for(int i=1; i<=n; ++i){
   dist[i] = maxnode;
   numbs[i] = 0;
   }

   Q[tail++] = des;
   dist[des] = 0;
   numbs[0] = 1;
   while(head != tail){
   int v = Q[head++];
   for(edge *e = Net[v]; e; e = e->next){
      if(e->rev->cap == 0 || dist[e->ver] < maxnode)continue;
      dist[e->ver] = dist[v] + 1;
      ++numbs[dist[e->ver]];
      Q[tail++] = e->ver;
   }
   }
}

int maxflow(){
   int u, totalflow = 0;
   edge *CurEdge[maxnode], *revpath[maxnode];
   for(int i=1; i<=n; ++i)CurEdge[i] = Net[i];
   u = src;
   while(dist[src] < n){
   if(u == des){    // find an augmenting path
      int augflow = infinity;
      for(int i = src; i != des; i = CurEdge[i]->ver)
      augflow = min(augflow, CurEdge[i]->cap);
      for(int i = src; i != des; i = CurEdge[i]->ver){
      CurEdge[i]->cap -= augflow;
      CurEdge[i]->rev->cap += augflow;
      CurEdge[i]->flow += augflow;
      CurEdge[i]->rev->flow -= augflow;
      }
      totalflow += augflow;
      u = src;
   }

   edge *e;
   for(e = CurEdge[u]; e; e = e->next)
      if(e->cap > 0 && dist[u] == dist[e->ver] + 1)break;
   if(e){    // find an admissible arc, then Advance
      CurEdge[u] = e;
      revpath[e->ver] = e->rev;
      u = e->ver;
   } else {    // no admissible arc, then relabel this vertex
      if(0 == (--numbs[dist[u]]))break;    // GAP cut, Important!
      CurEdge[u] = Net[u];
      int mindist = n;
      for(edge *te = Net[u]; te; te = te->next)
      if(te->cap > 0)mindist = min(mindist, dist[te->ver]);
      dist[u] = mindist + 1;
      ++numbs[dist[u]];
      if(u != src)
      u = revpath[u]->ver;    // Backtrack
   }
   }
   return totalflow;
}

int main(){
   int m, u, v, w;
   freopen("ditch.in", "r", stdin);
   freopen("ditch.out", "w", stdout);
   while(scanf("%d%d", &m, &n) != EOF){    // POJ 1273 need this while loop
   edge *buffer = new edge[2*m];
   edge *data = buffer;
   src = 1; des = n;
   while(m--){
      scanf("%d%d%d", &u, &v, &w);
      Net[u] = new((void*) data++) edge(v, w, Net[u]);
      Net[v] = new((void*) data++) edge(u, 0, Net[v]);
      Net[u]->rev = Net[v];
      Net[v]->rev = Net[u];
   }
   rev_BFS();
   printf("%d\n", maxflow());
   delete [] buffer;
   }
   return 0;
}

ISAP 是图论求最大流的算法之一,它很好的平衡了运行时间和程序复杂度之间的关系,因此非常常用。

约定

我们使用邻接表来表示图,表示方法可以见文章带权最短路 Dijkstra, SPFA, Bellman-Ford, ASP, Floyd-Warshall 算法分析二分图的最大匹配、完美匹配和匈牙利算法的开头(就不重复贴代码了)。在下文中,图的源点(source)表示为 s 汇点(sink)表示为 t ,当前节点为 u 。建图时,需要建立双向边(设反向的边容量为0)才能保证算法正确。

引入

求解最大流问题的一个比较容易想到的方法就是,每次在残量网络(residual network)中任意寻找一条从 s t 的路径,然后增广,直到不存在这样的路径为止。这就是一般增广路算法(labeling algorithm)。可以证明这种不加改进的贪婪算法是正确的。假设最大流是 f ,那么它的运行时间为 O( fE) 。但是,这个运行时间并不好,因为它和最大流 f 有关。

人们发现,如果每次都沿着残量网络中的最短增广路增广,则运行时间可以减为 O(E2V) 。这就是最短增广路算法。而 ISAP 算法则是最短增广路算法的一个改进。其实,ISAP 的意思正是「改进的最短增广路」 (Improved Shortest Augmenting Path)。

顺便说一句,上面讨论的所有算法根本上都属于增广路方法(Ford-Fulkerson method)。和它对应的就是大名鼎鼎的预流推进方法(Preflow-push method)。其中最高标号预流推进算法(Highest-label preflow-push algorithm)的复杂度可以达到 O(V2E) 。虽然在复杂度上比增广路方法进步很多,但是预流推进算法复杂度的上界是比较紧的,因此有时差距并不会很大。

算法解释

概括地说,ISAP 算法就是不停地找最短增广路,找到之后增广;如果遇到死路就 retreat,直到发现 s, t 不连通,算法结束。找最短路本质上就是无权最短路径问题,因此采用 BFS 的思想。具体来说,使用一个数组 d ,记录每个节点到汇点 t 的最短距离。搜索的时候,只沿着满足 d[u]=d[v]+1 的边 uv (这样的边称为允许弧)走。显然,这样走出来的一定是最短路。

原图存在两种子图,一个是残量网络,一个是允许弧组成的图。残量网络保证可增广,允许弧保证最短路(时间界较优)。所以,在寻找增广路的过程中,一直是在残量网络中沿着允许弧寻找。因此,允许弧应该是属于残量网络的,而非原图的。换句话说,我们沿着允许弧,走的是残量网络(而非原图)中的最短路径。当我们找到沿着残量网络找到一条增广路,增广后,残量网络肯定会变化(至少少了一条边),因此决定允许弧的 d 数组要进行相应的更新(顺便提一句,Dinic 的做法就是每次增广都重新计算 d 数组)。然而,ISAP 「改进」的地方之一就是,其实没有必要马上更新 d 数组。这是因为,去掉一条边只可能令路径变得更长,而如果增广之前的残量网络存在另一条最短路,并且在增广后的残量网络中仍存在,那么这条路径毫无疑问是最短的。所以,ISAP 的做法是继续增广,直到遇到死路,才执行 retreat 操作。

说到这里,大家应该都猜到了,retreat 操作的主要任务就是更新 d 数组。那么怎么更新呢?非常简单:假设是从节点 u 找遍了邻接边也没找到允许弧的;再设一变量 m ,令 m 等于残量网络中 u 的所有邻接点的 d 数组的最小值,然后令 d[u] 等于 m+1 即可。这是因为,进入 retreat 环节说明残量网络中 u t 已经不能通过(已过时)的允许弧相连,那么 u t 实际上在残量网络中的最短路的长是多少呢?(这正是 d 的定义!)显然是残量网络中 u 的所有邻接点和 t 的距离加 1 的最小情况。特殊情况是,残量网络中 u 根本没有邻接点。如果是这样,只需要把 d[u] 设为一个比较大的数即可,这会导致任何点到 u 的边被排除到残量网络以外。(严格来说只要大于等于 V 即可。由于最短路一定是无环的,因此任意路径长最大是 V1 )。修改之后,只需要把正在研究的节点 u 沿着刚才走的路退一步,然后继续搜索即可。

讲到这里,ISAP 算法的框架内容就讲完了。对于代码本身,还有几个优化和实现的技巧需要说明。

  1. 算法执行之前需要用 BFS 初始化 d 数组,方法是从 t s 逆向进行。
  2. 算法主体需要维护一个「当前节点」 u ,执行这个节点的前进、retreat 等操作。
  3. 记录路径的方法非常简单,声明一个数组 p ,令 p[i] 等于增广路上到达节点 i 的边的序号(这样就可以找到从哪个顶点到的顶点 i )。需要路径的时候反向追踪一下就可以了。
  4. 判断残量网络中 s, t 不连通的条件,就是 d[s]≥∣V 。这是因为当 s, t 不连通时,最终残量网络中 s 将没有任何邻接点,对 s 的 retreat 将导致上面条件的成立。
  5. GAP 优化。GAP 优化可以提前结束程序,很多时候提速非常明显(高达 100 倍以上)。GAP 优化是说,进入 retreat 环节后, u, t 之间的连通性消失,但如果 u 是最后一个和 t 距离 d[u] (更新前)的点,说明此时 s, t 也不连通了。这是因为,虽然 u, t 已经不连通,但毕竟我们走的是最短路,其他点此时到 t 的距离一定大于 d[u] (更新前),因此其他点要到 t ,必然要经过一个和 t 距离为 d[u] (更新前)的点。GAP 优化的实现非常简单,用一个数组记录并在适当的时候判断、跳出循环就可以了。
  6. 另一个优化,就是用一个数组保存一个点已经尝试过了哪个邻接边。寻找增广的过程实际上类似于一个 BFS 过程,因此之前处理过的邻接边是不需要重新处理的(残量网络中的边只会越来越少)。具体实现方法直接看代码就可以,非常容易理解。需要注意的一点是,下次应该从上次处理到的邻接边继续处理,而非从上次处理到的邻接边的下一条开始。

最后说一下增广过程。增广过程非常简单,寻找增广路成功(当前节点处理到 t )后,沿着你记录的路径走一遍,记录一路上的最小残量,然后从 s t 更新流量即可。

实现

poj1273&hdu1523Drainage Ditches(ISAP)

分类: 网络流 图论 30人阅读 评论(0) 收藏 举报

题目请戳这里

题目大意:略。

题目分析:网络流模版题。不过数据很弱,只能测很烂的模版。

第一道网络流

详情请见代码:

  1. #include <iostream>
  2. #include<cstdio>
  3. #include<cstring>
  4. #include<algorithm>
  5. using namespace std;
  6. const int N = 205;
  7. const int M = 410;
  8. const int inf = 0x3f3f3f3f;
  9. struct node
  10. {
  11. int to,next,pre,c,f;
  12. }arc[M];
  13. int num;
  14. int head[N];
  15. int que[N];//bfs用
  16. int sta[N];//保存当前弧
  17. int rpath[N];//保存反向弧
  18. int cnt[N];
  19. int dis[N];
  20. int m,n;
  21. void build(int s,int e,int cap)//建图
  22. {
  23. arc[num].to = e;
  24. arc[num].c = cap;
  25. arc[num].f = 0;
  26. arc[num].next = head[s];
  27. head[s] = num ++;
  28. arc[num - 1].pre = num;//反向弧
  29. arc[num].pre = num - 1;
  30. arc[num].to = s;
  31. arc[num].c = 0;
  32. arc[num].f = 0;
  33. arc[num].next = head[e];
  34. head[e] = num ++;
  35. }
  36. void re_Bfs()
  37. {
  38. int i,front,rear;
  39. front = rear = 0;
  40. for(i = 1;i <= n;i ++)
  41. {
  42. dis[i] = inf;
  43. cnt[i] = 0;
  44. }
  45. que[rear ++] = n;
  46. cnt[0] = 1;
  47. dis[n] = 0;
  48. while(front != rear)
  49. {
  50. int u = que[front ++];
  51. for(i = head[u];i != -1;i = arc[i].next)
  52. {
  53. if(arc[arc[i].pre].c == 0 || dis[arc[i].to] < inf)
  54. continue;
  55. dis[arc[i].to] = dis[u] + 1;
  56. cnt[dis[arc[i].to]] ++;
  57. que[rear ++] = arc[i].to;
  58. }
  59. }
  60. }
  61. int ISAP()
  62. {
  63. int i,u,v,ret = 0;
  64. u = 1;
  65. for(i = 1;i <= n;i ++)
  66. sta[i] = head[i];
  67. while(dis[1] < n)
  68. {
  69. if(u == n)
  70. {
  71. int curflow = inf;
  72. for(i = 1;i != n;i = arc[sta[i]].to)
  73. curflow = min(curflow,arc[sta[i]].c);
  74. for(i = 1;i != n;i = arc[sta[i]].to)
  75. {
  76. arc[sta[i]].c -= curflow;
  77. arc[arc[sta[i]].pre].c += curflow;
  78. arc[sta[i]].f += curflow;
  79. arc[arc[sta[i]].pre].f -= curflow;
  80. }
  81. ret += curflow;
  82. u = 1;
  83. }
  84. for(i = sta[u];i != -1;i = arc[i].next)//寻找允许弧
  85. if(arc[i].c > 0 && dis[arc[i].to] + 1 == dis[u])
  86. break;
  87. if(i != -1)
  88. {
  89. sta[u] = i;
  90. rpath[arc[i].to] = arc[i].pre;
  91. u = arc[i].to;
  92. }
  93. else
  94. {
  95. if((--cnt[dis[u]]) == 0)//gap优化
  96. break;
  97. sta[u] = head[u];
  98. int Min = N;
  99. for(i = head[u];i != -1;i = arc[i].next)
  100. if(arc[i].c > 0)
  101. Min = min(Min,dis[arc[i].to]);
  102. dis[u] = Min + 1;
  103. cnt[dis[u]] ++;
  104. if(u != 1)
  105. u = arc[rpath[u]].to;
  106. }
  107. }
  108. return ret;
  109. }
  110. int main()
  111. {
  112. int i,u,v,c;
  113. while(scanf("%d%d",&m,&n) != EOF)
  114. {
  115. memset(head,-1,sizeof(head));
  116. num = 0;
  117. while(m --)
  118. {
  119. scanf("%d%d%d",&u,&v,&c);
  120. build(u,v,c);
  121. }
  122. re_Bfs();
  123. printf("%d\n",ISAP());
  124. }
  125. return 0;
  126. }  
 
 



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值