• (通俗易懂小白入门)网络流最大流——EK算法


    网络流

    网络流是模仿水流解决生活中类似问题的一种方法策略,来看这么一个问题,有一个自来水厂S,它要向目标T提供水量,从S出发有不确定数量和方向的水管,它可能直接到达T或者经过更多的节点的中转,目前确定的是每条水管中水流的流向是确定的(单向),且每个水管单位时间内都有属于自己的水流量的上限(超过会爆水管),问题是求终点T单位时间内获得的最大水流量是多少?如下图:

    1. 首先,我们用正常的思路去解决这个问题,对于上图的情况而言,我们可以先选择一条水流的路线1->2->4,而且我们得知1->2水管水流量上限为40,而2->4水管水流量上限只有20,则这一条路径通往终点T的水流最大值为20,而这么走的话,1->2的水管中剩余水流量则为40-20=20,2->4水管中的剩余水量为20-20=0,注意:这里水管剩余流量为0后说明这条路径就再也不能接通了,或者说它已经被完全占用了,此时终点T单位时间获得水量0+20=20

    2. 然后我们继续选择一条不包含0流量的从S到T的路径,比如1->2->3->4,此时1->2上水流量剩余20,2->3剩余30,3->4剩余10,很显然这条路径上最终能为终点T提供的单位时间的水量由路径上的最小剩余流量10决定(就像是木桶的短板效应一样),这么走的话,1->2水管中剩余的水量为40-20-10=10,2-3水管中剩余的水量为30-10=20,3->4水管中剩余的水量为10-10=0,它也被完全占用了,此时终点T单位时间获得水量为20+10=30

    3. 我们继续找还能找到一条从S流向T的路径1->4,水流上限20,因为是直达的,所以路径上最小剩余流量就是它本身,则这么走的话,1->4上的水流量全部耗尽,20-20=0,它也完全被占用了,而终点T再一次获得20水量,30+20=50,至此整张网络流图再也找不到一条能从S到T的不包含0流量的通路,终点T单位时间获得的最大水流也计算出来得到50

     但是,上述的推理只是碰巧数字比较合适让我们轻而易举得到了50这个看似正确的结果,实际上存在着缺陷,假如我第一次找的一条路径并不是此时的最优解,并且这么选使得我下一次选择别的路径的时候有的水管流量已经被占完了,我想反悔怎么办

    如下图:如果我们第一次选择的是1->2->3->4这条路径(这很符合程序设计的观念,计算机很容易就按照序号去查找),那么1->2水管剩余流量为1-1=0,2->3水管剩余流量为1-1=0,3->4水管的剩余流量为1-1=0,至此我们发现所有的通路都被这三个0流量的水管阻断了,而此时终点T获得的单位水量只有0+1=1

    按正常的人为的思路,此时我想要反悔之前选的那条路径,同时选择别的路径,而下图的1->2->4和1->3->4这两条路径的选择才是这张网络流图的最优选择,终点T得到的最大水量为1+1=2,而2->3的这条水管我们放弃不用

    上述反悔的过程在我们的思维看来确实可以(假装)没有走过2->3这条路,但是计算机并不能这么去主动理解这种情况,它应该按并不是最优的情况去尝试,当遇到可能更优的情况时更改之前的选择,在我们设计程序的时候,如何能做到这个反悔的过程呢,关键来了:我们对于每次选择的一条道路上的每个水管都要减去路径上的最小残量的同时,为这两个相邻的点添加一条反向弧,反向弧的数值加上最小残量的数值,如上上图那个并不是最优的走法,假设计算机确实第一次就选择了这条路径,那么我们做出如下处理:1->2剩余流量依旧为1-1=0,而同时添加一条2->1的反向弧,流量为0+1=1(反向弧也是一条通路,它的存在就像是为我们提供了一个返回的机会),2->3剩余流量为1-1=0,添加3->2的反向弧,剩余流量为0+1=1,同样3->4的剩余流量为1-1=0,而4->3的反向弧上的剩余流量为0+1=1,所以本次选择之后终点T依旧获得了1的单位流量

    接下来我们继续选择一条路径,1->3->2->4(当然也只剩下这条了),此时我们如上述操作,为1->3剩余流量,3->2剩余流量,2->4剩余流量都减去这条路径上的最小残量1,同时为它们两两之间的反向弧增加上这个最小残量1(图可能有点乱,但是原理是不变的,实线方向-,虚线方向+),至此,1->2=0,2->1=1,1->3=0,3->1=1,2->3=1,3->2=0,2->4=0,4->2=1,3->4=0,4->3=1,从S到T已经没有一条额外的路径不包含0流量了,此时T获得最大单位水量2,并且通过这种建立反向弧的方式,我们对于从2到3再从3到2都走了一遍,这不就好像是模拟了一遍反悔的过程吗(具体证明这里不作详述),接下来讲解EK算法

    铺垫了这么多终于要讲解EK算法了

    别着急在讲解EK之前我们需要介绍几个网络流中最重要的概念,我们称起点S,也就是水流的出发点为源点,将水流的终点T称为汇点,而我们每一次找一条从源点到汇点的不包含0流量的路径称为增广路,(增广路顾名思义,如果能找到一条从S到T的增广路,则到终点的水流量一定还可以增加至少1,所以就满足了增广这个要求了),其实求网络流最大流的问题的各个算法都是在模拟一个找寻增广路的过程,如果找不到了就说明此时终点获得的水量将无法变得更大,答案就算出来了

    EK算法:从S点出发不断找一条到T的增广路的过程,我们通过BFS向周围搜索与S直接相连的剩余流量不为0的节点(这个节点一定要是没走过的,因为一条增广路每个点肯定值出现了一次),将他们加入队列,每次从队列中取出一个元素继续向周围查询,直到目标点为T点,且这一条道路上不包含流量为0的水管,则说明这是一条增广路,为沿途的所有节点两两之间的剩余流量减去该条增广路的最小残量,而同时为它们的反向弧加上最小残量,不断循环直到无法从S点到T找到一条增广路为止

    这里推荐一题网络流最大流的模板题,POJ1273,题面讲的是有n个水管,有m个点,源点为1,汇点为m,求汇点T单位时间的最大水流量,当然这题有个小坑,就是输入会重复,如果1->2 40代表从1流向2有40流量,那可能会有多次1->2 40,1->2 30之类的,要累加成1->2 70

    代码:

     1 #include<iostream>
     2 #include<stdio.h>
     3 #include<queue> 
     4 #include<string.h>
     5 using namespace std;
     6 
     7 const int N = 205;
     8 const int INF = 0x3f3f3f3f;
     9 int c[N][N];        //记录i到j的剩余流量 
    10 int p[N];            //p[i]记录流向i点的前驱节点
    11 int v[N];            //记录在一条增广路中,流到i点时,此刻增广路上残余量的最小值,直到i == m时就是整条增广路上的残余量最小值 
    12 int n, m;
    13 
    14 int min(int a, int b){
    15     return a <= b ? a : b;
    16 }
    17 
    18 void EK(){
    19     //从1出发,不断找可以到达m的增广路
    20     int ans = 0;
    21     while(true){
    22         //EK算法的核心是通过bfs不断查找增广路,同时建立反向弧
    23         //每次循环都要对v数组和p数组进行清空,因为是意图查找一条新的增广路了
    24         memset(p, 0, sizeof(p));
    25         memset(v, 0, sizeof(v)); 
    26         queue<int> q;
    27         q.push(1);
    28         v[1] = INF;
    29         //每次只找一条增广路,同时修改c[i][j]的值 
    30         while(!q.empty()){
    31             int f = q.front();
    32             q.pop();
    33             for(int i = 1; i <= m; i++){
    34                 if(v[i] == 0 && c[f][i] > 0){        //v[i]原本是记录增广路实时的残量最小值,v[i]==0代表这个点还没有走过,且从p到i的残量大于0说明通路 
    35                     v[i] = min(v[f], c[f][i]);        //实时更新v[i]的值,v[f]存储1条增广路中i点前所有水管残量的最小值,v[i]为该条增广路到i点为止,路径上的最小残量 
    36                     p[i] = f;                        //p[i]实时保存i点的前驱节点,这样就当i==m时整条增广路就被记录下来 
    37                     q.push(i);                        //将i点入队 
    38                 }
    39             } 
    40         }
    41         if(v[m] == 0) break;                          //如果v[m]==0则代表找不到增广路了(中途出现了c[i][j]==0的情况) 
    42         ans += v[m];
    43         int temp = m;
    44         while(p[temp] != 0){                        //类似并查集的查操作,不断查上一个元素且将剩余残量减去最小残联,反向弧增加最小残量 
    45             c[p[temp]][temp] -= v[m];
    46             c[temp][p[temp]] += v[m];
    47             temp = p[temp];
    48         } 
    49     } 
    50     printf("%d\n", ans); 
    51 }
    52 
    53 int main(){
    54     while(scanf("%d%d", &n, &m) != EOF){
    55         memset(c, 0, sizeof(c));
    56         for(int i = 1; i <= n; i++){
    57             int x, y, z;
    58             scanf("%d%d%d", &x, &y, &z);
    59             c[x][y] += z;                //初始时,从x流向y的剩余流量就是输入的值 
    60         }
    61         EK(); 
    62     }
    63     return 0;
    64 } 
  • 相关阅读:
    Sql to Linq 小工具
    datagridview后台添加列
    datatable之distinct用法
    生成器
    函数练习一
    函数初识
    文件操作练习
    文件操作
    迭代器 递归
    基础数据类型上机题
  • 原文地址:https://www.cnblogs.com/YLTFY1998/p/11314610.html
Copyright © 2020-2023  润新知