网络流之最大流

网络流之最大流

一、 问题引入。

有n个排水口,不同的排水口之间有m条水管连接,水一开始从源点s流出,最终到达t。每条边(水管)都有一个最大的流量。除了s,t外,每个排水口的流入量都要等于流出量,询问最多能有多少水到达终点t ?

如图所示:

 

(即poj 1273)

可以将问题进行如下整理:

(1)用c[e]表示每条边最大的可能流量

(2)每条边对应的实际流量为f[e]

(3)根据条件,可知所有的f[e]<=c[e]

(4)目标是最大化从s发出的水流量

二、问题分析:

很容易会想到贪心算法,就是不断地从源节点s出发寻找能到达t的路径,能流就流,一路上对c[e]取min,流了之后将路上的c[e]减掉此次的流量mina。直到找不到路径为止。

但是,其实这样的算法是不正确的。

以下是《挑战程序竞赛》中的反例。

 

 

    左图为贪心算法运行出的结果,答案为10,但是右图答案为11。不妨找找这两图流量的差来分析原因。

 

 

    通过分析反例,可以发现,我们在流过某一条边之后,可能会导致以后无法经过此边。如左图,s->1->2->t使得s->2->t无法走,而且还限定了s->1->3->t的流量。因此,一开始就流s->1->2->t很可能不是最优的。

    因此,我们需要给每条边一个“反悔”的机会,要考虑到实际上某次不流这条边的情况。

    在此基础上,增广路算法横空出世。下文介绍其中的一种——Ford-fulkerson算法。

 

二、 Ford-fulkerson算法

    我们在原先算法的基础上,将算法进行如下改进:

    我们给每条边增加一条反向边,初始上限流量为0。

    如前文的贪心算法那样,不断寻找s到t的路径。某一条边为e,反向边为g 。每次找到一条可行路径(流量为   res),则ans+=res,并将路径上边的上限流量减去res, 其相对的反向边的上限流量加上res。直到找不到一条流量大于0的路径,则停止算法。

    将这一算法应用为样例中:

 

    很显然,答案正确。

    为何?增加反向边的意义,其实就是给了每条边一个反悔的机会。比如说第一次流过了边e,又流了一次其反向边g,则边的上限流量就又回到了初始状态,相当于没有流。每条边都可以流,也可以反悔,就保证了算法的正确性。

三、 复杂度分析

    设最大流流量为F,则最坏的时间复杂度为O(F|E|)

    不过实际应用中,还是很快的。

四、 代码实现

    Poj1273模板题源代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=205,INF=1e7+5;
int m,n,cur,s,t,a,b,va;
int head[MAXN],v[MAXN];
long long ans;
struct wyy
{
    int to,va,next,type;
}edge[2*MAXN];
void add(int from,int to,int va,int type)
{
    cur++;
    edge[cur].to=to;
    edge[cur].va=va;
    edge[cur].next=head[from];
    edge[cur].type=type;
    head[from]=cur;
}
int dfs(int now,int mina)
{
    if(now==t)    return mina;
        
    v[now]=1;
    
    int h=head[now];
    while(h!=-1)
    {
        int to=edge[h].to,va=edge[h].va,ty=edge[h].type;
        if(v[to]==0&&va!=0)
        {
            if(int res=dfs(to,min(mina,va)))    
            {
                edge[h].va-=res;
                if(ty==0)    edge[h+1].va+=res;//求相对反向边的下标
                else    edge[h-1].va+=res;//实际上就是,在链表中的下标为奇数就加1,为偶数就减1。可用异或1来解决(前提是链表的下标从0开始)
                return res;
            }                
        }            
        h=edge[h].next;
    }
    return 0;
}
int main()
{
    ios::sync_with_stdio(false);
    while(cin>>m>>n)
    {
        s=1,t=n;
        cur=-1;
        ans=0;
        memset(head,-1,sizeof(head));
        for(int i=1;i<=m;i++)
        {
            cin>>a>>b>>va;
            add(a,b,va,0);//存边的类型,0为正向边,1为反向边。其实也可以不用记这个参数。圆桌问题中就换了一种写法。
            add(b,a,0,1);
        }
        while(1)
        {
            memset(v,0,sizeof(v));
            v[s]=1;
            int res=dfs(s,INF);
            if(res==0)    break;
            ans+=res;
        }
        cout<<ans<<endl;
    }
    return 0;
}

 

posted @ 2017-08-19 16:39  littlewyy  阅读(166)  评论(0编辑  收藏  举报