最大流问题

1 篇文章 0 订阅
1 篇文章 0 订阅

网络流问题:最大流及其算法

一、概念引入

流网络:

G=(V,E)是一个有向图,其中每条边(u,v)∈E均有一个非负容量c(u,v)>=0。如果(u,v)不属于E,则假定c(u,v)=0。流网络中有两个特别的顶点:源点s和汇点t。下图展示了一个流网络的实例(其中斜线左边的数字表示实际边上的流f(u,v),右边的数字表示边的最大容量c(u,v),1为源点,7为汇点):
图a
图a

残余网络:

对于已经找到一条从S 到T的路径的网络中,该边当前流量为f(u,v),只要在这条路径上,把f(u,v)的值更新为C(u,v)-f(u,v),并且添加反向弧C(v,u),即可得到残余网络。
例如有以下流网络
在这里插入图片描述
图b
其残余网络如下:
图c
图c

  • 在残余网络中有以下几个概念:
    • 饱和弧:即f(u,v)=c(u,v),当前流量为最大流量,图b中4-7;

    • 非饱和弧:即f(u,v)<c(u,v),当前流量小于最大流量,图b中2-3;

    • 零流弧:即f(u,v)=0,当前流量为零,图b中3-5;

    • 非零流弧:即f(u,v)>0,当前流量不为零.

    • 增广路径:
      流网络的增广路径Path为残留网络上从S到T的一条简单路径P,P满足以下条件:(向前的意义为沿着源点指向汇点的方向)

      • P中所有前向弧都是非饱和弧,
      • P中所有后向弧都是非零弧.

      图b中1,2,4,7就是一条增广路径,还有1,3,4,7。

  • 最大流问题:
    给定一个流网络,给出每条边能流过的最大流量,求源点到汇点的最大流量,称其为最大流(MaxFlow)。
    下面给出一个通俗点的解释
    比如你家是汇点,自来水厂是源点,然后自来水厂和你家之间修了很多条水管子接在一起,水管子规格不一,有的容量大,有的容量小。
    然后问自来水厂开闸放水,你家收到水的最大流量是多少,如果自来水厂停水了(也就是让每个管道里流过的水量都为0),你家那的流量就是0, 当然不是最大的流量;但是你给自来水厂交了很多钱,自来水厂拼命水管里通水,让每根管道都达到最大流量(容量),这时就达到了最大流。

二.算法描述

  • Ford-Fulkerson 方法
    FF(Ford-Fulkerson)方法的主要思想就是任意寻找增广路径,找出增广路径上每一段[容量-流量]的最小值delta,然后调整网络,直到找不出增广路径为止。最总所有找出的delta的和即为最大流。
    之所以FF称为方法,是因为它一般只作为描述算法的思想,实现时任意寻找增广路径,而有些路径并不能到达汇点,使得搜索效率降低,所以实际一般采用EK(Edmonds-Karp)算法来进行优化。
     Edmonds_Karp算法
    EK(Edmonds-Karp)算法的核心思想和EK一样,也是不断寻找增广路径,求最小delta,直到最后找不到增广路径时停止。但实现时采用了BFS(广度优先搜索)的方法来对EK算法进行优化,降低时间复杂度。对于一个有V个顶点,E条边的流网络来说,时间复杂度为O(VE^2)。

例题:现有一个供水网络,从水库到目标居民区有若干个中继站,这三者间又有若干型号不同的管道相连,为了简单起见,水库用1表示,中继站用2-7表示,目标居民区用8表示,整个网络有15条水管,各个管道的规格与题意相关的是其容量,一定不能超过该容量,否则会发生爆管的危险事故。问:水库怎样供水能够让目标居民区得到最大的水量?注:中继站不会消耗水,各管道有方向。

例题网络

思路:(N为节点数,m为源点,t为汇点)

  • 采用EK算法求最大流。

  • 用邻接矩阵保存流网络,记录各个管道的容量capacity[N+1][N+1](数组为N+1长度是因为节点序号从1开始,代码中N直接采用的可能的最大值)。

  • 再创建一个各个管道上当前流量的邻接矩阵flow[N+1][N+1],然后从以各个管道中流量为0作为初始条件进行BFS搜索增广路径。

  • 其中需要一个向前节点的数组pre[N+1]来记录每一轮寻找的增广路径,并且因为初始化为-1,最后填入的节点号[1,N]可作为访问的标志,并且pre[t]为-1时还能作为没有增广路径的标识来退出循环。

  • 每一轮搜索出一条增广路径,在搜索期间,不断对比前后路径中当前管的容量中流量和前一段管中流量,取最小值,则可在每一轮得出flow[t]为这一轮找出的增广路径的最小容量delta,加到最终结果sumflow中,然后进行调整capacity邻接矩阵。

  • 调整的方法:根据pre数组从汇点往回找,不停的将每一段的正向减去delta,负向加上delta形成反向弧。直到源点。

  • EK算法的精华是:负向加上delta,这一步能避免在做增广路时可能会阻塞后面的增广路,因为做增广路本来是有个顺序才能找完最大流的(举例),但我们是任意找的,为了修正,就每次将流量加在了反向弧上,让后面的流能够进行自我调整,剩余网络的更新。

具体举例

  • 人为去找:
    1. 1->3->7->8, delta =9
    2. 1->2->6->8, delta = 5
    3. 1->4->5->8, delta = 10
    4. 1->3->6->8, delta = 1
    5. 1->4->5->2->6->8, delta = 3
  • 但程序并不是这样走的
    1. 1->2->6->8, delta= 5
    2. 1->3->6->8, delta = 5
    3. 1->3->7->8, delta = 5
    4. 1->4->5->8, delta = 10
    5. 1->4->5->2->6->3->7->8, delta = 3
      可以发现6->3为逆向,因为有了反向弧调整了网络,所以才让其行的通,可以将反向弧理解为当前管道“退回”一部分水,改走另一条路。

代码

#include <iostream> 
#include <queue>
#include <string.h>
using namespace std;
#define arraysize 201
int maxData = 0x7fffffff;
int capacity[arraysize][arraysize]; //记录残留网络的容量
int flow[arraysize];                //标记从源点到当前节点实际还剩多少流量可用
int pre[arraysize];                 //标记在这条路径上当前节点的前驱,同时标记该节点是否在队列中
int n,m;
queue<int> myqueue;

/*
Sample 

Input
15 8
1 3 10
1 2 5
1 4 15
3 2 4
2 4 4
5 2 6
3 7 9
3 6 15
2 6 8
4 5 16
7 6 15
6 5 15
7 8 10
6 8 10
5 8 10

Output
28

*/

int BFS(int src,int des)//每一次BFS 找一条增广路径 
{
    int i,j;
    while(!myqueue.empty())       //队列清空
        myqueue.pop();
    for(i=1;i<m+1;++i)
    {
        pre[i]=-1;
    }
    pre[src]=0;
    flow[src]= maxData;
    myqueue.push(src);
    while(!myqueue.empty())
    {
        int front_index = myqueue.front();
        myqueue.pop();
        if(front_index == des)            //找到了增广路径
            break;
        for(i=1;i<m+1;++i)
        {
        	//if(i!=src)printf("test:capacity[%d][%d]=%d and pre[%d]=%d ",front_index,i,capacity[front_index][i],i,pre[i]) ;
            if(i!=src && capacity[front_index][i]>0 && pre[i]==-1)
            {
                 pre[i] = front_index; //记录前驱
                 flow[i] = min(capacity[pre[i]][i],flow[front_index]);   //关键:迭代的找到增量
                 //printf("pre=%d and flow[%d]=%d,capacity[%d][%d]=%d  flow[%d]=%d\n",front_index,front_index,flow[front_index],front_index,i,capacity[pre[i]][i],i,flow[i]);
                 myqueue.push(i);
            }
        }
    }
    //printf("\n");
    if(pre[des]==-1)      //残留图中不再存在增广路径
        return -1;
    else
        return flow[des];
}
int maxFlow(int src,int des)
{
    int increasement= 0;
    int sumflow = 0;
    while((increasement=BFS(src,des))!=-1)
    {
         int k = des;          //利用前驱寻找路径
         while(k!=src)
         {
              int last = pre[k];
              capacity[last][k] -= increasement; //改变正向边的容量
              capacity[k][last] += increasement; //改变反向边的容量
              k = last;
         }
         sumflow += increasement;
    }
    return sumflow;
}
int main()
{
    int i,j;
    int start,end,ci;
    while(cin>>n>>m)
    {
        memset(capacity,0,sizeof(capacity));
        memset(flow,0,sizeof(flow));
        for(i=0;i<n;++i)
        {
            cin>>start>>end>>ci;
            if(start == end)               //考虑起点终点相同的情况
               continue;
            capacity[start][end] +=ci;     //此处注意可能出现多条同一起点终点的情况
        }
        cout<<maxFlow(1,m)<<endl;
    }
    return 0;
}


参考
coursera上FF课程的例题
初步理解比较好的教程
增广路径解释
负向弧的作用解释

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值