hdu 3549 EK算法 Flow Problem

首先谈一下自己对EK算法的一些理解,其实是看了别人博客才理解的

因为是初学教程,所以我会尽量避免繁杂的数学公式和证明。也尽量给出了较为完整的代码。
本文的目标群体是网络流的初学者,尤其是看了各种NB的教程也没看懂怎么求最大流的小盆友们。本文的目的是,解释基本的网络流模型,最基础的最大流求法,即bfs找增广路法,也就是EK法,全名是Edmond-Karp,其实我倒是觉得记一下算法的全名和来历可以不时的拿出来装一装。
比如说这个,EK算法首先由俄罗斯科学家Dinic在1970年提出,没错,就是dinic算法的创始人,实际上他提出的也正是dinic算法,在EK的基础上加入了层次优化,这个我们以后再说,1972年Jack Edmonds和Richard Karp发表了没有层次优化的EK算法。但实际上他们是比1790年更早的时候就独立弄出来了。
你看,研究一下历史也是很有趣的。
扯远了,首先来看一下基本的网络流最大流模型。
有n个点,有m条有向边,有一个点很特殊,只出不进,叫做源点,通常规定为1号点。另一个点也很特殊,只进不出,叫做汇点,通常规定为n号点。每条有向边上有两个量,容量和流量,从i到j的容量通常用c[I,j]表示,流量则通常是f[I,j]。通常可以把这些边想象成道路,流量就是这条道路的车流量,容量就是道路可承受的最大的车流量。很显然的,流量<=容量。而对于每个不是源点和汇点的点来说,可以类比的想象成没有存储功能的货物的中转站,所有”进入”他们的流量和等于所有从他本身”出去”的流量。
把源点比作工厂的话,问题就是求从工厂最大可以发出多少货物,是不至于超过道路的容量限制,也就是,最大流。
比如这个图。每条边旁边的数字表示它的容量。



最大流模板【EdmondsKarp算法,简称EK算法,O(m^2n)】



下面我们来考虑如何求最大流。
首先,假如所有边上的流量都没有超过容量(不大于容量),那么就把这一组流量,或者说,这个流,称为一个可行流。一个最简单的例子就是,零流,即所有的流量都是0的流。
我们就从这个零流开始考虑,假如有这么一条路,这条路从源点开始一直一段一段的连到了汇点,并且,这条路上的每一段都满足流量<容量,注意,是严格的<,而不是<=。那么,我们一定能找到这条路上的每一段的(容量-流量)的值当中的最小值delta。我们把这条路上每一段的流量都加上这个delta,一定可以保证这个流依然是可行流,这是显然的。
这样我们就得到了一个更大的流,他的流量是之前的流量+delta,而这条路就叫做增广路。
我们不断地从起点开始寻找增广路,每次都对其进行增广,直到源点和汇点不连通,也就是找不到增广路为止。当找不到增广路的时候,当前的流量就是最大流,这个结论非常重要。
寻找增广路的时候我们可以简单的从源点开始做bfs,并不断修改这条路上的delta量,直到找到源点或者找不到增广路。
这里要先补充一点,在程序实现的时候,我们通常只是用一个c数组来记录容量,而不记录流量,当流量+1的时候,我们可以通过容量-1来实现,以方便程序的实现。

Bfs过程的半伪代码:下面另给一个C++版的模板

int BFS()
{
    int i,j,k,v,u;
    memset(pre,-1,sizeof(pre));
    for(i=1;i<=n;++i)flow[i]=max_int;
    queue<int>que;
    pre[start]=0;
    que.push(start);
    while(!que.empty())
    {
        v=que.front();
        que.pop();
        for(i=1;i<=n;++i)
        {
            u=i;
            if(u==start||pre[u]!=-1||map[v][u]==0)continue;
            pre[u]=v;
            flow[u]=MIN(flow[v],map[v][u]);
            que.push(u);
        }
    }
    if(flow[end]==max_int)return -1;
    return flow[end];
}

但事实上并没有这么简单,上面所说的增广路还不完整,比如说下面这个网络流模型。



我们第一次找到了1-2-3-4这条增广路,这条路上的delta值显然是1。于是我们修改后得到了下面这个流。(图中的数字是容量)


这时候(1,2)和(3,4)边上的流量都等于容量了,我们再也找不到其他的增广路了,当前的流量是1。
但这个答案明显不是最大流,因为我们可以同时走1-2-4和1-3-4,这样可以得到流量为2的流。
那么我们刚刚的算法问题在哪里呢?问题就在于我们没有给程序一个”后悔”的机会,应该有一个不走(2-3-4)而改走(2-4)的机制。那么如何解决这个问题呢?回溯搜索吗?那么我们的效率就上升到指数级了。
而这个算法神奇的利用了一个叫做反向边的概念来解决这个问题。即每条边(I,j)都有一条反向边(j,i),反向边也同样有它的容量。
我们直接来看它是如何解决的:

在第一次找到增广路之后,在把路上每一段的容量减少delta的同时,也把每一段上的反方向的容量增加delta。即在Dec(c[x,y],delta)的同时,inc(c[y,x],delta)
我们来看刚才的例子,在找到1-2-3-4这条增广路之后,把容量修改成如下


这时再找增广路的时候,就会找到1-3-2-4这条可增广量,即delta值为1的可增广路。将这条路增广之后,得到了最大流2。


那么,这么做为什么会是对的呢?我来通俗的解释一下吧。
事实上,当我们第二次的增广路走3-2这条反向边的时候,就相当于把2-3这条正向边已经是用了的流量给”退”了回去,不走2-3这条路,而改走从2点出发的其他的路也就是2-4。(有人问如果这里没有2-4怎么办,这时假如没有2-4这条路的话,最终这条增广路也不会存在,因为他根本不能走到汇点)同时本来在3-4上的流量由1-3-4这条路来”接管”。而最终2-3这条路正向流量1,反向流量1,等于没有流量。
这就是这个算法的精华部分,利用反向边,使程序有了一个后悔和改正的机会。而这个算法和我刚才给出的代码相比只多了一句话而已。

#include<iostream>
#include<queue>
using namespace std;
const int maxn=205;
const int inf=0x7fffffff;
int r[maxn][maxn]; //残留网络,初始化为原图
bool visit[maxn];
int pre[maxn];
int m,n;
bool bfs(int s,int t)  //寻找一条从s到t的增广路,若找到返回true
{
    int p;
    queue<int > q;
    memset(pre,-1,sizeof(pre));
    memset(visit,false,sizeof(visit));
    pre[s]=s;
    visit[s]=true;
    q.push(s);
    while(!q.empty())
    {
        p=q.front();
        q.pop();
        for(int i=1;i<=n;i++)
        {
            if(r[p][i]>0&&!visit[i])
            {
                pre[i]=p;
                visit[i]=true;
                if(i==t) return true;
                q.push(i);
            }
        }
    }
    return false;
}
int EdmondsKarp(int s,int t)
{
   int flow=0,d,i;
   while(bfs(s,t))
   {
       d=inf;
       for(i=t;i!=s;i=pre[i])
           d=d<r[pre[i]][i]? d:r[pre[i]][i];
       for(i=t;i!=s;i=pre[i])
       {
           r[pre[i]][i]-=d;
           r[i][pre[i]]+=d;
       }
       flow+=d;
   }
   return flow;
}

int main()
{
    while(scanf("%d%d",&m,&n)!=EOF)
    {
        int u,v,w;
        memset(r,0,sizeof(r));///
        for(int i=0;i<m;i++)
        {
            scanf("%d%d%d",&u,&v,&w);
            r[u][v]+=w;
        }
        printf("%d\n",EdmondsKarp(1,n));
    }
    return 0;
}

题意::

首先给定有多少组测试数据,然后给定一个顶点数,边数,然后给定一个单向的流量,求从第一个点到最后一个点的最大流量。注意该流网络是有向图,所以对于流量的控制是给定x 到 y 的流量是 z , 那么只需要给cap[x][y]+= z 即可。求解网络流就是在图中寻找增广路径,一条增广路径是指能够从起点到终点寻找到一条大于零的流量路径,并试图将其填充满,注意抑制一次寻找过程中回流现象,这是无意义的,因为在每一次队列的计算中,并没有及时的更新管道的容量,所以回流的形成会无限制的循环,肯定会超出内存,实质上也是一个死循环。

Description

Network flow is a well-known difficult problem for ACMers. Given a graph, your task is to find out the maximum flow for the weighted directed graph.
 

Input

The first line of input contains an integer T, denoting the number of test cases. 
For each test case, the first line contains two integers N and M, denoting the number of vertexes and edges in the graph. (2 <= N <= 15, 0 <= M <= 1000) 
Next M lines, each line contains three integers X, Y and C, there is an edge from X to Y and the capacity of it is C. (1 <= X, Y <= N, 1 <= C <= 1000)
 

Output

For each test cases, you should output the maximum flow from source 1 to sink N.
 

Sample Input

       
       
2 3 2 1 2 1 2 3 1 3 3 1 2 1 2 3 1 1 3 1
 

Sample Output

       
       
Case 1: 1 Case 2: 2
 

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 20;
const int INF = 1000000+10;

int cap[maxn][maxn];
int flow[maxn][maxn];
int a[maxn],p[maxn];
int main()
{
    int T,n,m;
    scanf("%d",&T);
     for(int i=1;i<=T;i++)
     {
         scanf("%d %d",&n,&m);
         memset(cap,0,sizeof(cap));
         memset(flow,0,sizeof(flow));
         int x,y,c;
         for(int j=1;j<=m;j++)
         {
             scanf("%d %d %d",&x,&y,&c);
             cap[x][y]+=c;
         }
         int s=1,t=n;
         queue<int>q;
         int f=0;
         for(;;)
         {
             memset(a,0,sizeof(a));
             a[s]=INF;
             q.push(s);
             while(!q.empty())
             {
                 int u=q.front();
                 q.pop();
                for(int v = 1; v <= n; v++)
                if(!a[v] && cap[u][v] > flow[u][v])
                {
                    p[v] = u; q.push(v);
                    a[v] = min(a[u], cap[u][v]-flow[u][v]);
                }
             }
                 if(a[t]==0)
                    break;
                 for(int u=t;u!=s;u=p[u])
                 {
                     flow[p[u]][u]+=a[t];
                     flow[u][p[u]]-=a[t];
                 }
                 f+=a[t];
         }
           printf("Case %d: %d\n", i, f);
     }
     return 0;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值