【2018.2.8-】网络流学习笔记(含ISAP!)

网络流的基础内容就不详细发了,网上到处都是,可自学。 总版点这里

ps:以下有些链接是hihocoder的题目(题面有详细讲解),请确保先登录hihocoder,再点击进入相应题目网页。

 

最大流

基础知识

EK算法

Dinic算法(接弧优化)

EK与Dinic算法的对比讲解

 

如果实在看不懂,这里简短地跟你说明白(本人已经好长一段时间没钻网络流了,这段是靠记忆手打的,如有误可以指正):

增广路径:从起点到终点的一条路径,该路径上所有边都没有满流。当残余网络中再没有增广路径时,也就是说原网络已经无法再往任何一条从起点到终点的路径中加流了,此时增广过的所有路径的流的总和就是最大流

 

普通dfs:dfs找到一条增广路径,增广这条路径后回溯,找新的增广路。(这个算法效率低是因为增广一条路径后,回溯时要从这条路径上的某个地方跳出该路径,改走残余网络中其它子路径到终点,从而形成新的增广路。但这条增广路前面一段已经被增广过,也就是说边的剩余容量普遍较低,从而导致回溯出新的增广路能新加的流较少,增广一次只能加少量流,因此所需的增广次数较多,增广效率低)

 

Ford-Fulkerson:这个方法跟普通dfs没多少区别,区别就是该算法增广一条路径后从起点重新找增广路。(但实际上它下一次找的增广路正常情况下还是按照上一次增广的路径走,只是找到第一条满流的边后改道,这跟普通dfs回溯到的位置是一模一样的……所以说这个方法效率也不高,也没什么人用)

 

EK:从起点开始bfs找增广路,当某条路径延伸到终点时,增广这条路径并从起点重新开始bfs找增广路。(bfs的优势就在于从每个点向所有边扩张,并更新从起点到每个点当前在一条路径中能通过的最大流量,这样从起点延伸到终点的流量是该残余网络中能通过流很大的增广路(目前还未想通是不是最大),这样增广一次就能加很多流,因此所需的增广次数较少,增广效率高。这就是为什么EK(bfs)面对10000个点的网络跑得飞快而dfs直接卡爆)

 

Dinic:用bfs给残余网络分层(建议从终点开始分层,原因在下文有补充),这里假设大家按照我的建议从终点开始分层,那么分层就可以理解为算出每个点到终点的最短距离。每次从起点开始,只能走邻层的点,这样可以确保增广路是残余网络的最短路径。

   Q:为什么要确保增广路是最短路径? A:上面我发的讲Dinic算法的那篇文章里有一个栏目叫“朴素算法的低效之处”,分层图就是解决那种例子的——优先让入终点的边满流(很显然从起点到终点的流必须得经过入终点的边),这样就不用担心像那个坑爹的例子一样,被多余的小流边把增广路的流卡得特别小,然后所需的增光次数就很多,拉低增广效率。

   按照走邻层的规则,用dfs找到一条增广路后,更新这条路径上的流,然后重新做残余网络的分层图,并回到起点重新用dfs找增广路。

   Q:那这里又为什么要用dfs找增广路? A:由于增广路上的流会被更新,因此至少会有一条边满流,不能出现在分层图中,因此每次增广一条路后要重新做分层图。如果只找一条不要求流量的增广路的话(要求邻层即可),用dfs一般情况下能直接搜出这条路径,效率较高。bfs的队列扩展就显得多余了。

 

模板题:luogu3376

2018.3.21补充:第一份30分普通dfs代码有问题(没有回溯到满流边之前,有可能把满流边再加流至溢出),但不知道为什么能过数据啊,难道是数据太水了?

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 inline int read(){
 4     int x=0,f=1; char c=getchar();
 5     for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
 6     for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0';
 7     return x*f;
 8 }
 9 int n,m,s,t,fa[10001],faedge[10001],ans;
10 bool vis[10001];
11 struct edge{
12     int v,w,next;
13 }e[100001];
14 int head[10001],cnt;
15 inline void add(int w,int v,int u){
16     e[++cnt]=(edge){v,w,head[u]};
17     head[u]=cnt;
18 }
19 void dfs(int cur){
20     int i;
21     if(cur==t){
22         i=t;
23         int flow=2147483647;
24         while(i!=s) flow=min(flow,e[faedge[i]].w), i=fa[i];
25         i=t;
26         while(i!=s) e[faedge[i]].w-=flow, i=fa[i];
27         ans+=flow;
28         return;
29     }
30     vis[cur]=1;
31     for(i=head[cur];i;i=e[i].next){
32         if(vis[e[i].v]) continue;
33         fa[e[i].v]=cur;
34         faedge[e[i].v]=i;
35         dfs(e[i].v);
36     }
37     vis[cur]=0;
38 }
39 int main(){
40     n=read(),m=read(),s=read(),t=read();
41     for(int i=1;i<=m;i++){
42         add(read(),read(),read());
43     }
44     dfs(s);
45     printf("%d\n",ans);
46     return 0;
47 }
30分普通dfs(有问题)
30分普通dfs(已改正)
 1 #include<bits/stdc++.h>
 2 #define maxn 10001
 3 #define maxm 100001
 4 #define inf 0x7f7f7f7f
 5 using namespace std;
 6 inline int read(){
 7     int x=0,f=1; char c=getchar();
 8     for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
 9     for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0';
10     return x*f;
11 }
12 int n,m;
13 struct edge{
14     int to,cap,flow,next;
15 }e[maxm<<1];
16 int head[maxn],cnt,rhead[maxn];
17 int fa[maxn],faedge[maxn],flow[maxn];
18 struct EdmondsKarp{
19     inline void init(){
20         memset(head,-1,sizeof head);
21         memset(rhead,-1,sizeof rhead);
22     }
23     inline void addedge(int from,int to,int cap){
24         e[cnt]=(edge){to,cap,0,head[from]};
25         head[from]=cnt++;
26         e[cnt]=(edge){from,0,0,rhead[from]};
27         rhead[from]=cnt++;
28     }
29     int maxflow(int s,int t){
30         int ans=0,u,v,i;
31         while(1){
32             memset(flow,0,sizeof flow);
33             queue<int>q;
34             q.push(s);
35             flow[s]=inf;
36             while(!q.empty()){
37                 u=q.front(); q.pop();
38                 for(i=head[u];i!=-1;i=e[i].next){
39                     v=e[i].to;
40                     if(!flow[v] && e[i].cap>e[i].flow){
41                         fa[v]=u;
42                         faedge[v]=i;
43                         flow[v]=min(flow[u],e[i].cap-e[i].flow);
44                         q.push(v);
45                     }
46                 }
47                 if(flow[t]) break;
48             }
49             if(!flow[t]) break;
50             
51             i=t;
52             while(i!=s){
53                 e[faedge[i]].flow+=flow[t];
54                 e[faedge[i]^1].flow-=flow[t];
55                 i=fa[i];
56             }
57             ans+=flow[t];
58         }
59         return ans;
60     }
61 }aug;
62                 
63 int main(){
64     int s,t,u,v,w;
65     n=read(),m=read(),s=read(),t=read();
66     aug.init();
67     for(int i=0;i<m;i++){
68         u=read(),v=read(),w=read();
69         aug.addedge(u,v,w);
70     }
71     printf("%d\n",aug.maxflow(s,t));
72     return 0;
73 }
100分EK(最慢点128ms)
 1 #include<bits/stdc++.h>
 2 #define inf 0x7f7f7f7f
 3 #define maxn 10001
 4 #define maxm 100001
 5 using namespace std;
 6 inline int read(){
 7     int x=0,f=1; char c=getchar();
 8     for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
 9     for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0';
10     return x*f;
11 }
12 class Dinic_Enhancer
13 {
14 private:
15     struct edge{
16         int v,w,next;
17     }e[maxm<<1];
18     int cnt,head[maxn];
19     int depth[maxn],cur[maxn];//cur就是记录当前点u循环到了哪一条边(弧优化) 
20 public:
21     int n,m,s,t;
22     void init()
23     {
24         cnt=0;
25         memset(head,-1,sizeof(head));
26         
27         n=read(),m=read(),s=read(),t=read();
28         int u,v,w;
29         for(int i=0;i<m;i++){
30             u=read(),v=read(),w=read();
31             add_edge(u,v,w);
32         }
33     }
34     void add(int u,int v,int w)
35     {
36         e[cnt].v=v;
37         e[cnt].w=w;
38         e[cnt].next=head[u];
39         head[u]=cnt++;
40     }
41     void add_edge(int u,int v,int w)
42     {
43         add(u,v,w);
44         add(v,u,0);
45     }
46     int dfs(int u,int flow)
47     {
48         if(u==t) return flow;
49         for(int& i=cur[u];i!=-1;i=e[i].next){ //注意这里的&符号,这样i增加的同时也能改变cur[u]的值,达到记录当前弧的目的
50             if(depth[e[i].v]==depth[u]+1 && e[i].w)
51             {
52                 int di=dfs(e[i].v,min(flow,e[i].w));
53                 if(di>0)
54                 {
55                     e[i].w-=di;
56                     e[i^1].w+=di;
57                     return di;
58                 }
59             }
60         }
61         return 0;
62     }
63     int bfs()
64     {
65         queue<int> Q;
66         memset(depth,0,sizeof(depth));
67         depth[s]=1;
68         Q.push(s);
69         int i,u;
70         while(!Q.empty())
71         {
72             u=Q.front(); Q.pop();
73             for(i=head[u];i!=-1;i=e[i].next)
74                 if(depth[e[i].v]==0 && e[i].w>0)
75                 {
76                     depth[e[i].v]=depth[u]+1;
77                     if(e[i].v==t) return 1;
78                     Q.push(e[i].v);
79                 }
80         }
81         return 0;
82     }
83     int dinic()
84     {
85         int ans=0,i,flow;
86         while(bfs())
87         {
88             for(i=1;i<=n;i++) cur[i]=head[i];//每一次建立完分层图后都要把cur置为每一个点的第一条边 感谢@青衫白叙指出这里之前的一个疏漏
89             while(flow = dfs(s,inf)) ans+=flow;
90         }
91         return ans;
92     }
93 }de;
94 
95 int main(){
96     de.init();
97     printf("%d\n",de.dinic());
98     return 0;
99 }
100分Dinic(最慢点44ms)

 

考虑到ISAP算法网上没几个说得清楚的,我在这里单独说一下吧。

  ISAP的算法究竟在哪里做了优化呢?其实Dinic算法的弧优化给了我们很好的启示。既然能从上一次走过的边开始走,那我们就可以考虑一下是否可以每次dfs出一条增广路径后只更改单个点的层数(深度),而不用每次都bfs更新分层图。Dinic算法中,当我们沿着残量网络找到一条增广路增广后,残量网络肯定会变化(至少有一条边的流量满了,因此这条边相当于不在图中了),因此决定允许弧的d数组要进行相应的更新(Dinic 的做法就是每次增广后 都重新计算整个残量网络的d数组)。而ISAP改进的地方之一就是,其实没有必要马上更新d数组。这是因为,去掉一条边只可能令最短路径变得更长,而如果增广之前的残量网络存在另一条最短路,并且在增广后的残量网络中仍存在,那么这条路径毫无疑问是最短的。

  当残量网络中的一个点 与分层图中的所有邻层点连的边都满流 时,这时说明这个点到终点的最短路径都增广过了,要增广更长的路径了。因此要把它的弧指向剩下的未满流的边。但指向哪条呢?剩下的未满流的边指向的点都与这个点不是邻层的,由于当前走的一定是上一次残量网络的最短路径,因此 沿着这些不是邻层的点到终点的距离 一定是更长的,且连向这些不是邻层的点的边一定都未满流(可自行思考)。为了确保下一次仍然走的是残量网络中的最短路径,只要把弧指向 剩下的未满流的边指向的点 中 距离终点最近的一个,这样就走的是残量网络中的最短路径了。(具体证明可以自己接着意会一下)

  这样ISAP算法就只需要在最开始bfs一次,也就是给图分一次层就可以了。实际写代码的时候可以从终点开始分层(即终点深度为0),这样一个点的深度就是它到终点的最短距离了。当这个点所在的最短路都被增广过,也就是它 与分层图中的所有邻层点连的边都满流 时,把它的深度改为 所有与它相连的非邻层的点 中深度最低(即到终点距离最近)的加1 就可以了。其实从终点分层还有一个好处,见下面附录。

 

gap优化:如果你听说过ISAP算法也就应该听过gap优化。gap优化是个什么东西?可以这么说:

  gap优化可以提前结束程序,很多时候提速非常明显(高达 100 倍以上)。通过上面描述的ISAP算法,我们知道了可以通过不断更改满流边所连点的层数 来寻找新的增广路径。gap优化就是说,更改一个点u的层数时(设它的弧原来指向t,它原来的层数depth为d[u],点t原来的层数depth为d[t]),u,t之间的连通性会消失,但如果在这之前u是残量网络最后一个和t距离d[u](更新前)的点,那么此时源点和汇点也不连通了。从分层图的角度来说,两点之前连通时是邻层的,即d[u]-1=d[t],而如果残量网络中连接d[u]和d[t]两层的边只有这一条,那么当这条边的流量流完时(即需要更改点u的层数),整个网络的分层图就没有连接d[u]和d[t]两层的边了,而我们又知道只能走邻层点,且源点层数最大、汇点层数最小,因此此时源点和汇点就不连通了。gap优化的实现非常简单,用一个数组记录并在适当的时候判断要更改层数的点是否满足gap优化,若满足就结束程序即可。

 

  1 //ISAP
  2 #include<bits/stdc++.h>
  3 #define inf 0x7f7f7f7f
  4 #define maxn 10001
  5 #define maxm 100001
  6 using namespace std;
  7 inline int read(){
  8     int x=0,f=1; char c=getchar();
  9     for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
 10     for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0';
 11     return x*f;
 12 }
 13 class ISAP
 14 {
 15 private:
 16     struct edge{
 17         int v,w,next;
 18     }e[maxm<<1];
 19     int cnt,head[maxn];
 20     int fa[maxn],faedge[maxn],num[maxn];
 21     int depth[maxn],cur[maxn];//cur就是记录当前点u循环到了哪一条边(弧优化) 
 22 public:
 23     int n,m,s,t;
 24     void init()
 25     {
 26         cnt=0;
 27         memset(head,-1,sizeof(head));
 28         
 29         n=read(),m=read(),s=read(),t=read();
 30         int u,v,w;
 31         for(int i=0;i<m;i++){
 32             u=read(),v=read(),w=read();
 33             add_edge(u,v,w);
 34         }
 35     }
 36     void add(int u,int v,int w)
 37     {
 38         e[cnt].v=v;
 39         e[cnt].w=w;
 40         e[cnt].next=head[u];
 41         head[u]=cnt++;
 42     }
 43     void add_edge(int u,int v,int w)
 44     {
 45         add(u,v,w);
 46         add(v,u,0);
 47     }
 48     int bfs()
 49     {
 50         int i,u; queue<int> Q;
 51         for(i=1;i<=n;i++) depth[i]=n;
 52         depth[t]=1;
 53         Q.push(t);
 54         while(!Q.empty())
 55         {
 56             u=Q.front(); Q.pop();
 57             for(i=head[u];i!=-1;i=e[i].next){
 58                 if(depth[e[i].v]>depth[u]+1 && e[i^1].w) //注意ISAP中只做一次bfs,而Dinic要做若干次分层图。ISAP做bfs的目的不是做分层图,而是为了在主程序中从每个点到终点的最短距离开始计算路径,求的是每个点到终点的最短距离,因此可以不断更新;而Dinic的分层图是为了找到从起点到终点的最短路径,使在dfs过程中按照这条最短路径走,而不拐到最短路径外的点,所以分层图中每个点的层必须是第一次广搜遍历到的,不能做更改。这就是为啥这里判的是depth[e[i].v]>depth[u]+1 
 59                 {
 60                     depth[e[i].v]=depth[u]+1;
 61                     Q.push(e[i].v);
 62                 }
 63             }
 64         }
 65         if(depth[s]>0) return 1;
 66         return 0;
 67     }
 68     int augment(){
 69         int u=t,flow=inf;
 70         while(u!=s) u=fa[u], flow=min(flow,e[cur[u]].w);
 71         u=t;
 72         while(u!=s) u=fa[u], e[cur[u]].w-=flow, e[cur[u]^1].w+=flow;
 73         return flow;
 74     }
 75     int isap()
 76     {
 77         int ans=0,flow,u=s,i;
 78         bfs();
 79         for(i=1;i<=n;i++) num[depth[i]]++, cur[i]=head[i];//每一次建立完分层图后都要把cur置为每一个点的第一条边 感谢@青衫白叙指出这里之前的一个疏漏
 80         while(depth[s]<=n){
 81             if(u==t){
 82                 ans+=augment();
 83                 u=s;
 84             }
 85             bool done=0;
 86             for(i=cur[u];i!=-1;i=e[i].next){
 87                 if(depth[u]==depth[e[i].v]+1 && e[i].w){
 88                     fa[e[i].v]=u;
 89                     cur[u]=i; //记录当前弧!在回溯边时也可以用! 
 90                     done=1;
 91                     u=e[i].v;
 92                     break;
 93                 }
 94             }
 95             if(!done){
 96                 int mn=n;
 97                 for(i=head[u];i!=-1;i=e[i].next)
 98                     if(e[i].w) mn=min(mn,depth[e[i].v]);
 99                 if(--num[depth[u]]==0) return ans;
100                 num[depth[u]=mn+1]++;
101                 cur[u]=head[u];
102                 if(u!=s) u=fa[u]; //一定要返回!因为这个点之前和前一个点在一条增广路上,两点深度相邻,而修改了这个点的深度后这个点和前一个点的深度就不相邻了(前一个点到起点的路径深度依然连续相邻),为了维护路径深度的连续性,要回到前一个点去找其他路径(第一次交这个代码没写这行,改完这个点的深度就接着从这条边往下走了,白搜了无数次) 
103             }
104         }
105         return ans;
106     }
107 }ISAP;
108 
109 int main(){
110     ISAP.init();
111     printf("%d\n",ISAP.isap());
112     return 0;
113 }
100分ISAP(最慢点20ms)

 

补充:Dinic和ISAP算法中 从终点开始分层的好处(有人可能不知道,不知道的话一定要看看)

作为一个资深OIer的你应该知道为什么做分层图时,要从终点开始分层。如果不知道,你画个图yy一下就明白了。

从起点开始分层标号后的分层图如上。那么你的程序从起点开始搜索增广路时,就可能先搜索长的那条路,即1-2-3-4-3。此时得走到4-3边时,程序才发现这条路径上有两个相邻点不是邻层(这里的邻层:后一个点的深度是前一个点的+1。不能逆过来),从而返回。但是这条更长的子路都走完了才发现它不是最短(即路径的最后两个点不满足深度递增1),假如有一组黑数据,把上面这张图中的长路径改得更长(比如经过十万八万个点的),那把这条路再走一遍,增广过程就退化成dfs寻找从起点到终点了,效率还不低死?有没有什么办法能让它在进入路径的地方就判断这条子路是不是最短(即是不是当前图的增广路的一部分)呢?

当然有了。从终点开始分层标号就好了。

 

此时,从起点出发的话,上面那条更长路径刚一进去就发现两个点不邻层了(这里的邻层:后一个点的深度是前一个点的-1。不能逆过来),从而立刻判断这条路不是最短,也就不是增广路中的一部分,从而退回去寻找其他路径。这样整个图每次成功前进时(即不退回)都走的是最短路,那么就能一次沿最短路走到终点,这条最短路也就是增广路了。

相信大家都已经理解了吧!

 

最小割最大流

讲解

posted @ 2018-02-08 10:49  大本营  阅读(785)  评论(0编辑  收藏  举报