初见 | 图论 | 网络流初步

前言

上次写二分图的时候说过要学网络流里比较初步的东西,顺便把上次那个利用网络流建模求二分图最大匹配的坑补上。

因为学的总时长不长,可能会有一定的理解误区,希望各位发现后及时指正。

缺省源

还是惯例,因为我用的缺省源确实挺长的,所以都放在前面。

#include <iostream>
#include <stdio.h>
#include <math.h>
#include <algorithm>
#include <string.h>
#include <queue>
#define Heriko return
#define Deltana 0
#define Romanno 1
#define S signed
#define LL long long
#define R register
#define I inline
#define CI const int
#define mst(a, b) memset(a, b, sizeof(a))
#define ON std::ios::sync_with_stdio(false)
using namespace std;

template<typename J>
I void fr(J &x)
{
    short f=1;
    char c=getchar();
    x=0;
    while(c<'0' or c>'9')
    {
        if(c=='-') f=-1;
        c=getchar();
    }
    while (c>='0' and c<='9') 
    {
        x=(x<<3)+(x<<1)+c-'0';
        c=getchar();
    }
    x*=f;
}

template<typename J>
I void fw(J x,bool k)
{
    if(x<0) putchar('-'),x=-x;
    static short stak[35];
    short top=0;
    do
    {
        stak[top++]=x%10;
        x/=10;
    }
    while(x);
    while(top) putchar(stak[--top]+'0');
    if(k) putchar('\n');
    else putchar(' ');
}

那么下面就开始罢!

网络流

基本概念

网络

既然是网络流,那么我们首先需要知道什么是网络。其实网络的定义很简单,即:

  • 网络 \(G=(V,E)\) 是一张有向图。

比如下面这一张有向图就是一个网络:

容量

对于网络中的每一条有向边,都有一个给定的权值:容量,即:

  • \(\forall\ (x,y)\in E\),有权值 \(c(x,y)\),称为其容量。

下面这张图上的那些权值就是对应边的容量啦~

同时我们定义:

  • \(\forall\ (x,y)\notin E\),有 \(c(x,y)=0\)

源点和汇点

我们还是先用刚才那张图:

这里的 S 和 T 就为源点和汇点,一张网络的源点和汇点是可以随意指定的。

流 & 流量 & 剩余容量

\(f(x,y)\) 为定义在结点二元组 \((x\in V,y\in V)\) 上的实数函数,且满足:

  1. \(f(x,y)\le c(x,y)\)

  2. \(f(x,y)=-f(y,x)\)

  3. \(\forall x\ne S , x\ne T ,\sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)\)

\(f\) 则称为网络的流函数。对于 \((x,y)\in E\)\(f(x,y)\) 称为边的流量\(c(x,y)-f(x,y)\) 称为边的剩余容量[1]

对于这张图来说,我们把每条边的流量和容量以“\(f\ /\ c\)”的格式表示出来就是:

我们可以假设一个情景来帮助理解:假设这个网络是给你家供水的网络,

即,供水厂为 1 号结点,你家为 4 号结点:

虽然你计划把连通你家的两个水管的容量都开的很大,但是你发现因为供水厂出水的两个管子都太小了,能流到你家的水完全跑不满你的水管:

这个时候你觉得既然如此,安装两个水管那么费钱还不如去掉一个,于是你就看哪个剩余容量大就干掉哪个,于是你肯定保留下来的是下面红色这条:

最大流

这里用一种通俗易懂的解释:

  • 网路的最大流,即为使 \(\sum_{(S,v)\in E}f(S,v)\) 最大的流函数。

解决最大流问题的做法有多种,因为我没学那么多,于是就只说说 \(\tt{FF,EK,Dinic}\) 罢。

FF 算法

\(\tt{Ford-Fullkerson}\) 算法,这是网络流的一个基础算法,因为这个算法的想法相当直接。

其核心思想为寻找图中的增广路,实际就是网络中从源点到汇点的仍有剩余流量的路径,也就在残量网络(剩余容量大于 0 的边和所有结点构成的网络)上进行操作。[2]

主体的思路就是寻找增广路 & 利用反向边反悔

这里用一下 \(\tt{Dfkuaid}\)例子,我比较懒不再去写了(

大体的过程就是:用 DFS 找到增广路之后,利用前面流函数的第二条性质建立一条反向边更新残量网络,直到找不到增广路。

因为 FF 的做法还是挺暴力的,平常也没太有人用,就不写了。

EK 算法

\(\tt{Edmond-Karp}\) 算法,本身这个算法的思想是和 FF 一样的,只不过这里是用 BFS 去实现,这里之所以 EK 的时间复杂度上会更优,是因为 BFS 找到的增广路是最短的,而 DFS 找到的增广路不能保证是最短的,这样我们就省去了递归去找最短增广路的时间。

Code

这里用洛谷的模板 P3376 为例。


template<typename J>
I J Hmin(J x,J y) {Heriko x<y?x:y;}

CI inf=(1<<29),MXX=5005,NXX=205;

struct node
{
    LL to,nex,val;
}

r[MXX<<1];

LL head[NXX],cnt(1),incf[NXX],pre[NXX],n,m,maxflow,s,t;

bool vis[NXX];

I void Ass_we_can(LL x,LL y,LL z)
{
    r[++cnt].to=y;r[cnt].nex=head[x];r[cnt].val=z;head[x]=cnt;
    r[++cnt].to=x;r[cnt].nex=head[y];r[cnt].val=0;head[y]=cnt;
}

I bool BFS()
{
    mst(vis,0);
    queue<LL> qwq;
    qwq.push(s);vis[s]=1;
    incf[s]=inf;
    while(qwq.size())
    {
        LL x=qwq.front();qwq.pop();
        for(R LL i=head[x];i;i=r[i].nex)
        {
            if(r[i].val)
            {
                LL y=r[i].to;
                if(vis[y]) continue;
                incf[y]=Hmin(incf[x],r[i].val);
                pre[y]=i;
                qwq.push(y);vis[y]=1;
                if(y==t) Heriko Romanno;
            }
            
        }
    }
    Heriko Deltana;
}

I void Update()
{
    LL x=t;
    while(x!=s)
    {
        LL i=pre[x];
        r[i].val-=incf[t];
        r[i^1].val+=incf[t];
        x=r[i^1].to;
    }
    maxflow+=incf[t];
}

LL z,y,x;

S main()
{
    fr(n),fr(m),fr(s),fr(t);
    for(R LL i=1;i<=m;++i)
    {
        fr(z),fr(y),fr(x);
        Ass_we_can(z,y,x);
    }
    while(BFS()) Update();
    fw(maxflow,1);
    Heriko Deltana;
}

EK 的时间复杂度是 \(O(nm^2)\) 的,在处理非稠密图的时候还是可以的。

Dinic 算法

前面说到 EK 在处理非稠密图的时候是可以的,但是如果遇到稠密图就完全炸了啊QwQ

于是我们就需要一个在稠密图上更优的算法:Dinic 算法。

其实某种意义上来说,Dinic 是 EK 的进一步优化,所以 Dinic 是 FF 的优化的优化(bushi

考虑到 EK 算法每轮都可能会为了找到 1 条增广路遍历整个残量网络,而这一点还是可以继续优化的。

于是我们就需要用到一个东西来优化我们 BFS 的次序:分层图。

定义从 S 到 x 最少要经历的边数为 dis[x],而在残量网络中,满足 dis[y]=dis[x]+1 的边 \((x,y)\) 构成的子图显然就是一个分层图,而分层图显然是一张有向无环图。

Dinic 主要是以下两步,最终目标是不能让 S 到达 T:

  1. 在残量网路上 BFS 求出结点的 dis,构造分层图。

  2. 在分层图上 DFS 求增广路,在回溯的时候更新剩余容量。

下面是代码(当然内含玄学的当前弧优化)

Code

template<typename J>
I J Hmin(J x,J y) {Heriko x<y?x:y;}

CI inf=(1<<29),MXX=5005,NXX=205;

struct node
{
    LL to,nex,val;
}

r[MXX<<1];

LL head[NXX],cnt(1),incf[NXX],pre[NXX],n,m,maxflow,s,t;

bool vis[NXX];

I void Ass_we_can(LL x,LL y,LL z)
{
    r[++cnt].to=y;r[cnt].nex=head[x];r[cnt].val=z;head[x]=cnt;
    r[++cnt].to=x;r[cnt].nex=head[y];r[cnt].val=0;head[y]=cnt;
}

I bool BFS()
{
    mst(vis,0);
    queue<LL> qwq;
    qwq.push(s);vis[s]=1;
    incf[s]=inf;
    while(qwq.size())
    {
        LL x=qwq.front();qwq.pop();
        for(R LL i=head[x];i;i=r[i].nex)
        {
            if(r[i].val)
            {
                LL y=r[i].to;
                if(vis[y]) continue;
                incf[y]=Hmin(incf[x],r[i].val);
                pre[y]=i;
                qwq.push(y);vis[y]=1;
                if(y==t) Heriko Romanno;
            }
        }
    }
    Heriko Deltana;
}

I void Update()
{
    LL x=t;
    while(x!=s)
    {
        LL i=pre[x];
        r[i].val-=incf[t];
        r[i^1].val+=incf[t];
        x=r[i^1].to;
    }
    maxflow+=incf[t];
}

LL z,y,x;

S main()
{
    fr(n),fr(m),fr(s),fr(t);
    for(R LL i=1;i<=m;++i)
    {
        fr(z),fr(y),fr(x);
        Ass_we_can(z,y,x);
    }
    while(BFS()) Update();
    fw(maxflow,1);
    Heriko Deltana;
}

ISAP 算法

坑填上了(

于是求最大流的部分到这里大约就结束了罢(

网络最大流流建模解决二分图匹配

欸,上次我挖了个坑,于是这次就来填坑啦~

主要的思想就是新建两个虚拟点,一个是源点,一个是汇点,这源点连接全部左侧端点,而全部右侧端点连向汇点。

于是我们就有了复杂度爆切匈牙利的如下 Dinic 代码:

template<typename J>
I J Hmin(J x,J y) {Heriko x<y?x:y;}

template<typename J>
I void Clear(queue<J> &x) {while(x.size()) x.pop();}

CI inf=998244353,MXX=1e6+5,NXX=2005;

struct node
{
    LL to,nex,val;
}

r[MXX<<1];

LL head[NXX],cnt(1),dis[NXX],now[NXX],n,m,maxflow,s,t;

queue<LL> qwq;

I void Ass_we_can(LL x,LL y,LL z)
{
    r[++cnt].to=y;r[cnt].nex=head[x];r[cnt].val=z;head[x]=cnt;
    r[++cnt].to=x;r[cnt].nex=head[y];r[cnt].val=0;head[y]=cnt;
}

I bool BFS()
{
    mst(dis,0);
    Clear(qwq);
    qwq.push(s);dis[s]=1;now[s]=head[s];
    while(qwq.size())
    {
        LL x=qwq.front();qwq.pop();
        for(R LL i=head[x];i;i=r[i].nex)
        {
            if(r[i].val and !dis[r[i].to])
            {
                LL y=r[i].to;
                qwq.push(y);
                now[y]=head[y];
                dis[y]=dis[x]+1;
                if(y==t) Heriko Romanno;
            }
        }
    }
    Heriko Deltana;
}

LL Dinic(LL x,LL flow)
{
    if(x==t) Heriko flow;
    LL rst=flow,k,i,y;
    for(i=now[x];i and rst;i=r[i].nex)
    {
        y=r[i].to;
        if(r[i].val and dis[y]==dis[x]+1)
        {
            k=Dinic(y,Hmin(rst,r[i].val));
            if(!k) dis[y]=0;
            r[i].val-=k;
            r[i^1].val+=k;
            rst-=k;
        }
    }
    now[x]=i;
    Heriko flow-rst;
}

LL x,y,flow,e;

S main()
{
    fr(n),fr(m),fr(e);
    t=n+m+1;
    for(R int i=1;i<=n;++i) Ass_we_can(0,i,1);
    for(R int i=n+1;i<=n+m;++i) Ass_we_can(i,t,1);
    for(R int i=1;i<=e;++i)
    {
        fr(x),fr(y);
        if(x>n or y>m) continue; 
        Ass_we_can(x,y+n,1);
    }
    while(BFS()) 
        while((flow=Dinic(s,inf))) maxflow+=flow;
    fw(maxflow,1);
    Heriko Deltana;
}

最小费用最大流

费用流(最小费用最大流)与普通最大流的区别就是它在每条边上加了一个单位流量的费用,需要在满足最大流的同时满足费用最小。[3]

因为我从网上找到的大部分说自己写的 Dinic 的博客里发现的是 SSP(即 EK),发现的唯一真 Dinic 又不是很好写,于是这里就只有 EK 罢。

SSP 算法

SSP(Successive Shortest Path)算法:在最大流的 EK 算法求解最大流的基础上,把用 BFS 求解任意增广路改为用 SPFA 求解单位费用之和最小的增广路即可。[4]

简单来说就是:SSP = EK - BFS + SPFA.

众所周知,关于 SPFA ,他死了。但是据说 NOI2019 没卡

但是在这里一般来说应该没有人会去卡 SPFA 罢,虽然是有用 Dijkstra 的,但是还要对边权有一系列操作,我就没学。

于是上代码罢,是洛谷板子 P3381 的 Code。

Code

template<typename J>
I J Hmin(J x,J y) {Heriko x<y?x:y;}

CI inf=0X3f3f3f3f,MXX=50005,NXX=5005;

int n,m,maxflow,dis[NXX],head[NXX],cnt(1),incf[NXX],pre[NXX],s,t,mincost;

bool vis[NXX];

struct node
{
    int nex,to,dis,val;
}

r[MXX<<1];

I void Ass_we_can(int x,int y,int z,int w)
{
    r[++cnt].to=y,r[cnt].nex=head[x],r[cnt].val=z,r[cnt].dis=w;head[x]=cnt;
}

I bool SPFA()
{
    queue<int> q;
    mst(dis,inf);mst(vis,0);
    q.push(s);dis[s]=0;vis[s]=1;
    incf[s]=(1<<30);
    while(q.size())
    {
        int x=q.front();q.pop();
        vis[x]=0;
        for(R int i=head[x];i;i=r[i].nex)
        {
            if(!r[i].val) continue;
            int y=r[i].to;
            if(dis[y]>dis[x]+r[i].dis)
            {
                dis[y]=dis[x]+r[i].dis;
                incf[y]=Hmin(incf[x],r[i].val);
                pre[y]=i;
                if(!vis[y]) q.push(y),vis[y]=1;
            }
        }
    }
    if(dis[t]==inf) Heriko Deltana;
    Heriko Romanno;
}

I void MCMF()
{
    while(SPFA())
    {
        int x=t;
        maxflow+=incf[t];
        mincost+=dis[t]*incf[t];
        int i;
        while(x!=s)
        {
            i=pre[x];
            r[i].val-=incf[t];
            r[i^1].val+=incf[t];
            x=r[i^1].to;
        }
    }
}

int x,y,z,w;

S main()
{
    fr(n),fr(m),fr(s),fr(t);
    for(R int i=1;i<=m;++i)
    {
        fr(x),fr(y),fr(z),fr(w);
        Ass_we_can(x,y,z,w);
        Ass_we_can(y,x,0,-w);
    }
    MCMF();
    fw(maxflow,0),fw(mincost,1);
    Heriko Deltana;
}

End

于是就不加什么例题了罢,因为我懒得写了((

参考资料

在写本文的时候 OI-Wiki 被墙了,放的是镜像网站的链接

posted @ 2021-07-03 09:33  HerikoDeltana  阅读(90)  评论(0编辑  收藏  举报