这是网络流最基础的部分——求出源点到汇点的最大流(Max-Flow)。
最大流的算法有比较多,本次介绍的是其中复杂度较高,但是比较好写的EK算法。(不涉及分层,纯粹靠BFS找汇点及回溯找最小流量得到最终的答案)

EK算法,全名Edmonds-Karp算法(最短路径增广算法)。

首先简单介绍一下网络流的基本术语
源点:起点。所有流量皆从此点流出。只出不进
汇点:终点。所有流量最后汇集于此。只进不出
流量上限:有向边(u,v)(及弧)允许通过的最大流量
增广路:一条合法的从源点流向汇点的路径
计算最大流就是不停寻找增广路找到最大值的过程。

合法的网络流具有以下性质
1.f(i,j) <= c(i,j);//任意有向边的流量一定小于等于该边的流量限制
2.f(i,j) = -f(j,i);//从i流向j的流量与j流向i的流量互为相反数反向边
3.out(i) = in(i);//除源点、汇点外,任意一点i流入的流量与流出的相等(只是路过

(截自洛谷)

EK算法思路
1.通过BFS拓展合法节点(每个节点在本次BFS中仅遍历一次),找到汇点,并记录每个节点的前面节点(pre)(若找不到增广路,算法结束)
2.通过BFS的记录,从汇点回溯回源点,记录下每条弧流量的**最小值**minn, ans += minn(否则就会超出某条边的限制流量)
3.将所有经过的边的流量减去minn,反向边加上minn
4.重复上述步骤,直到找不到增广路,算法结束。

朴素版EK
最为简单的写法,通过邻接矩阵存储。
优点:代码简单,一目了然。
缺点:轻易爆内存,(N^2)的空间太大,N >10000基本就废了(MLE)。
源代码:

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. #define INF 0x3f3f3f
  4. #define maxn 10005
  5. int n, m, st, en, flow[maxn][maxn], pre[maxn];
  6. int q[maxn], curr_pos, st_pos, end_pos;
  7. bool wh[maxn];
  8. int max_flow;
  9. void Init()//初始化
  10. {
  11. int i, a, b, c;
  12. scanf("%d%d%d%d", &n, &m, &st, &en);
  13. for(i = 0; i != m; ++i)
  14. {
  15. scanf("%d%d%d", &a, &b, &c);
  16. flow[a][b] += c;
  17. }
  18. return ;
  19. }
  20. bool Bfs(int st, int en)//广搜找源点
  21. {
  22. st_pos = -1, end_pos = 0;
  23. memset(wh, 0, sizeof wh);
  24. wh[st] = 1;
  25. q[0] = st;
  26. while(st_pos != end_pos)
  27. {
  28. curr_pos = q[++st_pos];
  29. for(int i = 1; i != n+1; ++i)
  30. {
  31. if(!wh[i] && flow[curr_pos][i] > 0)
  32. {
  33. wh[i] = 1;
  34. pre[i] = curr_pos;
  35. if(i == en)
  36. {
  37. return true;
  38. }
  39. q[++end_pos] = i;
  40. }
  41. }
  42. }
  43. return false;
  44. }
  45. int EK(int start_pos, int end_pos)
  46. {
  47. int i, minn;
  48. while(Bfs(start_pos, end_pos))//回溯
  49. {
  50. minn = INF;
  51. for(i = end_pos; i != start_pos; i = pre[i])
  52. {
  53. minn = min(minn, flow[pre[i]][i]);
  54. }
  55. for(i = end_pos; i != start_pos; i = pre[i])
  56. {
  57. flow[pre[i]][i] -= minn;
  58. flow[i][pre[i]] += minn;//反向弧加上该值(具体原因下文详解)
  59. }
  60. max_flow += minn;
  61. }
  62. return max_flow;
  63. }
  64. int main()
  65. {
  66. //freopen("test.in", "r", stdin);
  67. //freopen("test.out", "w", stdout);
  68. Init();
  69. printf("%d", EK(st, en));
  70. //fclose(stdin);
  71. //fclose(stdout);
  72. }

经过洛谷测评(P3376 【模板】网络最大流),50分,MLE五个点

关于反向边加上最小流值的原因:
如果存在a->b有流,那么如果某一点c->b有流,则流量实际上可以再从b流到a,可以理解为把原本a->b的流量怼了回去
BFS遍历得到的剩下部分的增广路的流量实际上是原本a->b的流,而原本a->b之后流向汇点的增广路的部分流量变为由c->b的流量
增广路依旧合法

这证明了邻接矩阵是不可行的,于是我们想到了麻烦一点但是省空间的存储方法——边表至少从m*m优化到n*m,实则通常n*(m/10)便足够。

定义一个结构体

  1. struct qi{
  2. int st, en, num;//弧的始点,终点,权值
  3. }flow[maxm];

相对应的广搜也要稍作调整,注意一下pre的存储方法:
pre[i]=k,i是点,k是弧的编号

  1. bool Bfs(int st, int en)
  2. {
  3. st_pos = -1, end_pos = 0;
  4. memset(wh, 0, sizeof wh);
  5. wh[st] = 1;
  6. q[0] = st;
  7. while(st_pos != end_pos)
  8. {
  9. curr_pos = q[++st_pos];
  10. for(int i = 0; i < 2*m; ++i)
  11. {
  12. if(flow[i].st == curr_pos && flow[i].num > 0 && !wh[flow[i].en])
  13. {
  14. ne = flow[i].en;
  15. wh[ne] = 1;
  16. pre[ne] = i;
  17. if(ne == en)
  18. {
  19. return true;
  20. }
  21. q[++end_pos] = flow[i].en;
  22. }
  23. }
  24. }
  25. return false;
  26. }

所以Tracback的遍历循环也要改变。

  1. for(i = end_pos; i != st; i = flow[pre[i]].st)//注意一下i如何取下一个的
  2. {
  3. minn = min(minn, flow[pre[i]].num);
  4. }

事实证明:MLE的问题完美解决,测试结果:94ms , 18785kb,TLE三个点

那么还需要优化一下时间
注意BFS拓展时的循环:i:=0->2*m//m是弧的个数(因为有反向边,故是2*m)
由于题目范围是m <= 100000,每次最大循环达到2e5,太浪费了。
于是我们在边表再做一点手脚:开二维数组对每个始点所有的弧分别记录
x[i][m]指以i为始点的第m条弧(再开一个num[i]记录每个始点弧的个数)。
那么代码就更加繁琐了。

首先是读入优化:

  1. int read()
  2. {
  3. input = 0;
  4. a = getchar();
  5. while(a < '0' || a > '9')
  6. a = getchar();
  7. while(a >= '0' && a <= '9')
  8. {
  9. input = input*10+a-'0';
  10. a = getchar();
  11. }
  12. return input;
  13. }

然后是初始化:

  1. void Init()
  2. {
  3. int i, a, b, c;
  4. memset(num, -1, sizeof num);
  5. n = read(), m = read(), st = read(), en = read();
  6. for(i = 0; i != m; ++i)
  7. {
  8. flow[i].st = read();
  9. flow[i].en = read();
  10. flow[i].num = read();
  11. re[flow[i].st][++num[flow[i].st]] = i;
  12. re[flow[i].en][++num[flow[i].en]] = i+m;//初始化时将第i条弧的反向边编号为i+m
  13. }
  14. for(int i = m; i != 2*m; ++i)
  15. {
  16. flow[i].st = flow[i-m].en;
  17. flow[i].en = flow[i-m].st;
  18. flow[i].num = 0;
  19. }
  20. pre[st] = -1;
  21. return ;
  22. }

BFS部分:

  1. bool Bfs(int st, int en)
  2. {
  3. int i, j;
  4. st_pos = -1, end_pos = 0;
  5. memset(wh, 0, sizeof wh);
  6. wh[st] = 1;
  7. q[0] = st;
  8. while(st_pos != end_pos)
  9. {
  10. curr_pos = q[++st_pos];
  11. for(i = 0; i < num[curr_pos]+1; ++i)
  12. {
  13. j = re[curr_pos][i];//当前可拓展节点的候选节点,即当前节点的第i条弧的终点
  14. if(flow[j].st == curr_pos && flow[j].num > 0 && !wh[flow[j].en])
  15. {
  16. ne = flow[j].en;
  17. wh[ne] = 1;
  18. pre[ne] = j;
  19. if(ne == en)
  20. {
  21. return true;
  22. }
  23. q[++end_pos] = flow[j].en;
  24. }
  25. }
  26. }
  27. return false;
  28. }

Traceback只需改变反向边部分:

  1. flow[pre[i]+m].num += minn;

主函数部分不需要调整。

最终测评结果:392ms , 56988kb,AC。

通过上述过程,我们得知网络流的存储结构应该为边表而非邻接矩阵。当然,使用vector来存储弧也是可以的,这样会更大程度上节省空间,但会对时间造成一定影响,根据实际情况自行取舍。


最后附上完整代码:

  1. /*
  2. Max_flow- EK
  3. 2017/04/07
  4. While(BFS can find the end)
  5. for i,i->j:=st -> end:
  6. f[i][j] -= minn;f[j][i] += minn; ans += minn;//so we can delete at least one edge(the one which satisfies f[i][j] == minn)
  7. */
  8. #include <bits/stdc++.h>
  9. using namespace std;
  10. #define INF 0x3f3f3f
  11. #define maxm 200005
  12. #define maxn 10005
  13. struct qi{int st, en, num;}flow[maxm];
  14. int n, m, st, en, pre[maxn], re[maxn][maxn/10], num[maxn];
  15. int q[maxn], curr_pos, st_pos, end_pos, ne, max_flow;
  16. bool wh[maxn];
  17. int input;
  18. char a;
  19. int read()
  20. {
  21. input = 0;
  22. a = getchar();
  23. while(a < '0' || a > '9')
  24. a = getchar();
  25. while(a >= '0' && a <= '9')
  26. {
  27. input = input*10+a-'0';
  28. a = getchar();
  29. }
  30. return input;
  31. }
  32. void Init()
  33. {
  34. int i, a, b, c;
  35. memset(num, -1, sizeof num);
  36. n = read(), m = read(), st = read(), en = read();
  37. for(i = 0; i != m; ++i)
  38. {
  39. flow[i].st = read();
  40. flow[i].en = read();
  41. flow[i].num = read();
  42. re[flow[i].st][++num[flow[i].st]] = i;
  43. re[flow[i].en][++num[flow[i].en]] = i+m;
  44. }
  45. for(i = m; i != 2*m; ++i)
  46. {
  47. flow[i].st = flow[i-m].en;
  48. flow[i].en = flow[i-m].st;
  49. }
  50. return ;
  51. }
  52. bool Bfs(int st, int en)
  53. {
  54. int i, j;
  55. st_pos = -1, end_pos = 0;
  56. memset(wh, 0, sizeof wh);
  57. wh[st] = 1;
  58. q[0] = st;
  59. while(st_pos != end_pos)
  60. {
  61. curr_pos = q[++st_pos];
  62. for(i = 0; i < num[curr_pos]+1; ++i)
  63. {
  64. j = re[curr_pos][i];
  65. if(flow[j].st == curr_pos && flow[j].num > 0 && !wh[flow[j].en])
  66. {
  67. ne = flow[j].en;
  68. wh[ne] = 1;
  69. pre[ne] = j;
  70. q[++end_pos] = flow[j].en;
  71. if(ne == en)
  72. return true;
  73. }
  74. }
  75. }
  76. return false;
  77. }
  78. int EK(int start_pos, int end_pos)
  79. {
  80. int i, minn;
  81. while(Bfs(start_pos, end_pos))
  82. {
  83. minn = INF;
  84. for(i = end_pos; i != st; i = flow[pre[i]].st)
  85. {
  86. minn = min(minn, flow[pre[i]].num);
  87. }
  88. for(i = end_pos; i != st; i = flow[pre[i]].st)
  89. {
  90. flow[pre[i]].num -= minn;
  91. flow[pre[i]+m].num += minn;
  92. }
  93. max_flow += minn;
  94. }
  95. return max_flow;
  96. }
  97. int main()
  98. {
  99. //freopen("test.in", "r", stdin);
  100. //freopen("test.out", "w", stdout);
  101. Init();
  102. printf("%d", EK(st, en));
  103. //fclose(stdin);
  104. //fclose(stdout);
  105. }

自此,EK算法的讲解便结束了。
箜瑟_qi 2017.04.07 2:32


2017.04.21略加修改:

  1. /*
  2. Max_flow- EK
  3. 2017/04/07
  4. */
  5. #include <bits/stdc++.h>
  6. using namespace std;
  7. #define INF 0x3f3f3f
  8. #define maxm 200005
  9. #define maxn 10005
  10. struct Edge{
  11. int st, en, num;
  12. Edge(){}
  13. Edge(int s, int e, int n):
  14. st(s), en(e), num(n){}
  15. }flow[maxm];
  16. int n, m, st, en, pre[maxn], re[maxn][maxn/10], num[maxn];
  17. int q[maxn], curr_pos, st_pos, end_pos, ne, max_flow;
  18. bool wh[maxn];
  19. int input;
  20. char a;
  21. static int read()
  22. {
  23. a = getchar();
  24. input = 0;
  25. while(a < '0' || a > '9') a = getchar();
  26. while(a >= '0' && a <= '9')
  27. {
  28. input = input*10+a-'0';
  29. a = getchar();
  30. }
  31. return input;
  32. }
  33. static void Init()
  34. {
  35. int i, a, b, c, cur;
  36. memset(num, -1, sizeof num);
  37. n = read(), m = read(), st = read(), en = read();
  38. for(i = 0; i != m; ++i)
  39. {
  40. cur = 2*i;
  41. a = read(), b = read(), c = read();
  42. flow[cur] = Edge(a, b, c);
  43. flow[cur^1] = Edge(b, a, 0);
  44. re[flow[cur].st][++num[flow[cur].st]] = cur;
  45. re[flow[cur].en][++num[flow[cur].en]] = cur^1;
  46. }
  47. return ;
  48. }
  49. static bool Bfs(int st, int en)
  50. {
  51. int i, j;
  52. st_pos = -1, end_pos = 0;
  53. memset(wh, 0, sizeof wh);
  54. wh[st] = 1;
  55. q[0] = st;
  56. while(st_pos != end_pos)
  57. {
  58. curr_pos = q[++st_pos];
  59. for(i = 0; i < num[curr_pos]+1; ++i)
  60. {
  61. j = re[curr_pos][i];
  62. if(flow[j].st == curr_pos && flow[j].num > 0 && !wh[flow[j].en])
  63. {
  64. ne = flow[j].en;
  65. wh[ne] = 1;
  66. pre[ne] = j;
  67. q[++end_pos] = flow[j].en;
  68. if(ne == en)
  69. return true;
  70. }
  71. }
  72. }
  73. return false;
  74. }
  75. static int EK(int start_pos, int end_pos)
  76. {
  77. int i, minn;
  78. while(Bfs(start_pos, end_pos))
  79. {
  80. minn = INF;
  81. for(i = end_pos; i != st; i = flow[pre[i]].st)
  82. {
  83. minn = min(minn, flow[pre[i]].num);
  84. }
  85. for(i = end_pos; i != st; i = flow[pre[i]].st)
  86. {
  87. flow[pre[i]].num -= minn;
  88. flow[pre[i]^1].num += minn;
  89. }
  90. max_flow += minn;
  91. }
  92. return max_flow;
  93. }
  94. int main()
  95. {
  96. //freopen("test.in", "r", stdin);
  97. //freopen("test.out", "w", stdout);
  98. Init();
  99. printf("%d", EK(st, en));
  100. //fclose(stdin);
  101. //fclose(stdout);
  102. }

最大流算法之EK(最短路径增广算法)的更多相关文章

  1. 【BZOJ-3638&3272&3267&3502】k-Maximum Subsequence Sum 费用流构图 + 线段树手动增广

    3638: Cf172 k-Maximum Subsequence Sum Time Limit: 50 Sec  Memory Limit: 256 MBSubmit: 174  Solved: 9 ...

  2. 【Luogu】P3381最小费用最大流模板(SPFA找增广路)

    题目链接 哈  学会最小费用最大流啦 思路是这样. 首先我们有一个贪心策略.如果我们每次找到单位费用和最短的一条增广路,那么显然我们可以把这条路添加到已有的流量里去——不管这条路的流量是多大,反正它能 ...

  3. python数据结构与算法——图的最短路径(Floyd-Warshall算法)

    使用Floyd-Warshall算法 求图两点之间的最短路径 不允许有负权边,时间复杂度高,思路简单 # 城市地图(字典的字典) # 字典的第1个键为起点城市,第2个键为目标城市其键值为两个城市间的直 ...

  4. python数据结构与算法——图的最短路径(Dijkstra算法)

    # Dijkstra算法——通过边实现松弛 # 指定一个点到其他各顶点的路径——单源最短路径 # 初始化图参数 G = {1:{1:0, 2:1, 3:12}, 2:{2:0, 3:9, 4:3}, ...

  5. python数据结构与算法——图的最短路径(Bellman-Ford算法)解决负权边

    # Bellman-Ford核心算法 # 对于一个包含n个顶点,m条边的图, 计算源点到任意点的最短距离 # 循环n-1轮,每轮对m条边进行一次松弛操作 # 定理: # 在一个含有n个顶点的图中,任意 ...

  6. POJ 1273 - Drainage Ditches - [最大流模板题] - [EK算法模板][Dinic算法模板 - 邻接表型]

    题目链接:http://poj.org/problem?id=1273 Time Limit: 1000MS Memory Limit: 10000K Description Every time i ...

  7. LOJ 2979 「THUSCH 2017」换桌——多路增广费用流

    题目:https://loj.ac/problem/2979 原来的思路: 优化连边.一看就是同一个桌子相邻座位之间连边.相邻桌子对应座位之间连边. 每个座位向它所属的桌子连边.然后每个人建一个点,向 ...

  8. 数据增广imgaug库的使用

    记录一下这两天用imgaug库做数据增广的代码,由于是算用算学的,所以只能把代码写出来,具体每种增广算法的原理和一些参数就不得而知了,不过我觉得也没必要把这么些个算法搜搞懂,毕竟重点是扩种数据.所以, ...

  9. 一般增广路方法求网络最大流(Ford-Fulkerson算法)

    /* Time:2015-6-18 接触网络流好几天了 写的第一个模版————Ford-Fulkerson算法 作用:求解网络最大流 注意:源点是0 汇点是1 如果题目输入的是1到n 请预处理减1 * ...

随机推荐

  1. java初学代码,还不太熟练

    奇数和 public class Homework01{ public static void main(String [] args){  long t=1,s=0; do{  s=s+t;  t= ...

  2. cuda编程学习4——Julia

    书上的例子编译会有错误,修改一下行即可. __device__ cuComplex(float a,float b):r(a),i(b){} /* ========================== ...

  3. Android Things教程:电气基础之直流电路理论

    译者注:由于本人水平有限,译文中难免会出现概念模糊.晦涩难懂,如果实在没心思看下去,请发挥你的学习能动性,到原文中自行翻译,感谢!!!点这里,直达英文各种长句的世界. 好了,既然你选择继续往下看,那就 ...

  4. 极客君教你破解隔壁妹子的wifi密码,成功率高达90%

    首先,给大家推荐一个我自己维护的网站: 开发者网址导航:http://www.dev666.com/ 破解wifi密码听起来很复杂,实际上也不是非常的复杂,极客君(微信公众帐号:极客峰)今天教大家如何 ...

  5. 深入React组件生命周期

    上篇博文使用React开发的一些注意要点对React开发的一些重点进行了简单的罗列总结,虽然也提到了React生命周期,但只略微小结,在此单独写篇React生命周期的总结. 在组件的整个生命周期中,随 ...

  6. icheck样式绑定与翻页保持

    官方文档:http://icheck.fronteed.com/ 使用基本样式 $('input').iCheck({ checkboxClass : 'icheckbox_square-blue', ...

  7. Debian 8开启sftp服务

    看到某云的CDN居然是使用ftp这种早该淘汰的协议,不禁有些吐槽.ftp曾经作为互联网上最重要的协议,但漫长使用过程中体现出的各种缺点,已不适合再使用.其中最致命的问题就是明文传输用户密码.建议使用这 ...

  8. 老李推荐:第14章3节《MonkeyRunner源码剖析》 HierarchyViewer实现原理-HierarchyViewer实例化

    老李推荐:第14章3节<MonkeyRunner源码剖析> HierarchyViewer实现原理-HierarchyViewer实例化 poptest是国内唯一一家培养测试开发工程师的培 ...

  9. 关于JavaScript的模块化

    为什么需要模块化 最近在学习网易微专业的<前端系统架构>课程,里面讲到了关于JavaScript的模块化问题.具体指的是当随着Web系统不断强大起来,需要在客户端进行的操作就多了起来(比如 ...

  10. Java与面向对象之随感(2)

    我们知道Java语言的一大特性就是相比于c语言和c++语言,其更加安全.那么Java安全性的一个重要保证就是它取消了指针,并且坚决反对数组的出界(c++对当数组超出上限但是还进行读写操作时允许的!), ...