zoukankan      html  css  js  c++  java
  • 浅谈网络最大流

    浅谈网络最大流

    本篇随笔简单讲解一下网络流中的网络最大流。

    由于这是本蒟蒻的网络流第一篇讲解,所以也会连带介绍网络流的一些基本概念等问题。在后续的网络流讲解中(如果没退役还有的话),就会直接链接引到本篇博客。

    本篇讲解的部分图片摘自知乎@Pecco、博客园@chc_1234567890,以及互联网。感谢各路大佬的支持。希望共同进步。


    一、网络最大流的一些基本概念

    首先,网络流的基本概念。

    分为两个部分:网络和流(。。。)

    网络的本质是一张带边权有向图。其中有一个点被称作源点,一个点被称作汇点。边权被称为这条边的容量。比如下图。

    然后流这个东西需要我们感性理解一下。这个网络可以被抽象成很多生活中的具体事物。比如我们把它抽象成水流,把网络想象成一个自来水管道网,那么流就像是水,每条边的流不能超过容量。

    那么什么是网络最大流呢?

    还是按上面的方法抽象,顾名思义,网络最大流肯定要求一个类似于最大流量的东西。所以这个问题实际上就是:假定源点流出的水无限多,求进入汇点的最大流量。


    二、网络流的实现原理

    网络流就是带反悔的贪心。——LLQ

    因为要找最大,所以肯定要在当前状态下找一个最优的抉择。那么我们自然而然地觉得应该是每次选容量最大的边去继续流。

    所以我们设计了一个找增广路的算法,其基本思路是贪心。

    还是拿这张图举例:

    增广路,是从源点到汇点的路径,其上所有边的残余容量均大于0。

    上图中我首先选择1->2->3,这是一条增广路,提供2流量;然后我们相应地扣除选择路径上各边的容量,1->2的容量变成1,2->3的容量变成0,这时的容量称为残余容量。然后我们再找到1->2->4->3这条路径,按残余容量计算流量,它提供1流量(选择这两条路的顺序可以颠倒)。1->2->4->3也是一条增广路。

    但是这个贪心并不对。有可能我们流到当前点是最优的,但是当前点到汇点有一个容量非常小的边,卡掉了,所以不如两条边都走,有可能更大。

    比如下面这个例子:

    我们找到一条增广路,其残余网络变成这样:

    于是我们求得最大流为1.但这显然不对,这就是我们刚刚分析的贪心错误的情况。

    于是我们引入:带反悔的贪心。

    具体说来,就是在每条边上建立反向边,作为“反悔边”。这些边的妙用会在下面提到。

    比如上面的贪心错误的例子,建好反悔边:

    在我们找增广路、扣除流量的同时,要把反向边加上相同的流量,以备反悔。这样,在找到一条增广路之后,剩下的参与网络还是可以找到增广路:

    这样,我们隐隐约约地感悟到,反向建边其实是一种“撤销”,通过这种撤销,我们就把之前走过这条边的一些流量拉了回去。所以反向边容量其实是“可以撤回的量”

    所以叫可反悔贪心。

    这样的话,我们就可以保证当找不到增广路的时候,流到汇点的流量就是最大流量了。

    三、最大流的代码实现

    网络流包括很多算法。这种最大流是一种算法,还有之后的费用流,上下界流等。

    刚刚我们讲到的算法叫做FF算法,也叫(Ford-Fulkerson)算法。这个算法的实现基于深搜。

    借助例题:洛谷模板讲解代码。

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #define int long long
    using namespace std;
    const int maxn=210;
    const int maxm=5010;
    const int INF=1e9;
    int n,m,s,t;
    int tot=1,to[maxm<<1],nxt[maxm<<1],head[maxn],val[maxm<<1];
    bool v[maxn];
    void add(int x,int y,int z)
    {
        to[++tot]=y;
        nxt[tot]=head[x];
        val[tot]=z;
        head[x]=tot;
    }
    int dfs(int x,int flow)
    {
        if(x==t)
            return flow;
        v[x]=1;
        for(int i=head[x];i;i=nxt[i])
        {
            int y=to[i];
            if(val[i]==0||v[y])
                continue;
            int tmp=0;
            if((tmp=dfs(y,min(val[i],flow)))>0)
            {
                val[i]-=tmp;
                val[i^1]+=tmp;
                return tmp;
            }
        }
        return 0;
    }
    signed main()
    {
        scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
        for(int i=1;i<=m;i++)
        {
            int x,y,z;
            scanf("%lld%lld%lld",&x,&y,&z);
            add(x,y,z);
            add(y,x,0);
        }
        int ans=0,tmp=0;
        while((tmp=dfs(s,INF))>0)
        {
            memset(v,0,sizeof(v));
            ans+=tmp;
        }
        printf("%lld
    ",ans);
        return 0;
    }
    

    其实是对上面过程的模拟,每次深搜找增广路。(v)数组来保证节点不会被重复经过。然后存储对边来进行反悔边的权值修改。

    这个算法的复杂度是(O(mf)),其中(m)是边数,(f)是最大流。这个复杂度竟然和流量有关,就很玄学。所以并不稳定,一般也不会用它。就是抛砖引玉。(上面的代码只有80pts)

    还有一个EK算法,全称是Edmond-Karp算法。

    EK算法较之FF算法的时间复杂度较优。是(O(nm^2))级别的,其原理是把刚刚的DFS实现找增广路变成BFS实现找增广路。这样的话,其不可能会绕远路,每次找到的都是最短的增广路。

    这时,由于我们不知道我们到底选择了哪条增广路,所以不能再BFS的同时对边进行反悔更新。这时,就需要我们记录一个pre数组,表示点的前驱边,这样,就能通过到达汇点后的原路返回更新这些边。

    代码:

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<queue>
    #define int long long
    using namespace std;
    const int maxn=210;
    const int maxm=5010;
    const int INF=1e18;
    int n,m,s,t;
    int tot=1,to[maxm<<1],nxt[maxm<<1],head[maxn],val[maxm<<1];
    int flow[maxn],pre[maxn];
    bool v[maxn];
    void add(int x,int y,int z)
    {
        to[++tot]=y;
        nxt[tot]=head[x];
        val[tot]=z;
        head[x]=tot;
    }
    bool bfs()
    {
        memset(pre,-1,sizeof(pre));
        queue<int> q;
        q.push(s);
        flow[s]=INF;
        while(!q.empty())
        {
            int x=q.front();
            q.pop();
            if(x==t)
                break;
            for(int i=head[x];i;i=nxt[i])
            {
                int y=to[i];
                if(val[i]==0||pre[y]>0)
                    continue;
                pre[y]=i;
                flow[y]=min(flow[x],val[i]);
                q.push(y);
            }
        }
        return pre[t]!=-1;
    }
    signed main()
    {
        scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
        for(int i=1;i<=m;i++)
        {
            int x,y,z;
            scanf("%lld%lld%lld",&x,&y,&z);
            add(x,y,z);
            add(y,x,0);
        }
        int ans=0;
        while(bfs())
        {
            ans+=flow[t];
            for(int i=t;i!=s;i=to[pre[i]^1])
            {
                val[pre[i]]-=flow[t];
                val[pre[i]^1]+=flow[t];
            }
        }
        printf("%lld
    ",ans);
        return 0;
    }
    

    这份代码已经可以得到满分的好成绩。

    以及最常用(因为最快)的Dinic算法

    这个算法涉及到一种优化——当前弧优化。

    因为这个优化,所以这种算法我们用一篇博客单讲。这里直接引入博客链接:

    浅谈网络流Dinic算法

  • 相关阅读:
    servlet中getWriter和getOutputStream的区别
    一个页面访问错误的问题
    sendRedirect实现原理分析
    servlet开发细节
    tomcat 目录分析
    servlet杂谈
    SQL 查询中的like子句的另一种实现方法,速度比like快
    让复合控件的子控件获得设计时支持
    bug管理工具——Gemini
    HtmlAgilityPack获取#开头节点的XPath
  • 原文地址:https://www.cnblogs.com/fusiwei/p/14051733.html
Copyright © 2011-2022 走看看