浅谈网络最大流

浅谈网络最大流

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

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

本篇讲解的部分图片摘自知乎@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\n",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\n",ans);
    return 0;
}

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

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

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

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

浅谈网络流Dinic算法

posted @ 2020-11-28 10:10  Seaway-Fu  阅读(593)  评论(1编辑  收藏  举报