网络流最大流(FF、Dinic)详解

网络流24题题解戳这里~

网络流基础概念

在这里插入图片描述
考虑这幅图,你可以看成从村庄s到村庄t有很多条物流道路,每个点都是个中转结点,每条路的权值即该条路最多能运送的货物

介绍一些基本概念:

网络:一个入度为0的点s,一个出度为0的点t,每条边有自己权值的有向图
容量:当前边上的权值,比如SA的容量即为3
源点:入度为0的点,通常s来表示
汇点:出度为0的点,通常t来表示
流:一个合法解称作一个流,也就是一条可以从源点到汇点的一条合法路径。
流量:每条边各自运送的包裹数称作其流量,最终收集的总数为整个流的流量。边上的流量:f(SA)=2,代表今天从s发了两个货物到A,最终整幅图每个流的流量和即为这张网络的总流量

显然我们会有两个定理
容量限制:每条边的流量不超过其容量(仓库塞不下这么多包裹)。
流量平衡:对于除源点和汇点以外的点来说,其流入量一定等于流出量。

割:一个边集,删掉这个边集即可使s与t不连通,比如说{C->T,S->A}即是一个割集,割集的大小即是边的容量和,这里为5
最小割:大小最小的割集
最大流:限制下能走到t的最大流量,即最终能送到t的最多包裹数

在这里插入图片描述

假设我们沿着S->A->C->T发包裹,因为容量限制的存在,我们最多发两个包裹,那么剩下的这幅容量图,即是我们的残量网络

增广路:即能增加运货量的s到t的路径
在这里插入图片描述
之前那幅图即有一条增广路:S->A->T

最大流-最小割

给出结论:最小割的容量等于最大流的流量
所以求最小割和最大流本质是一个问题,接下来说明最大流的求法

朴素想法

考虑这幅很小的简单图,我们显然可以用深搜的思想去暴力求解
假设增广路的流量为 Δ i \Delta{i} Δi ,那么显然最后的最大流f为 ∑ i = 1 n Δ i \sum_{i=1}^n{\Delta{i}} i=1nΔi
在这里插入图片描述
这种算法没有错,但显然只适用于很小的图,才可以在时间限制内去找到所有的增广情况

FF算法(Ford-Fulkerson)

FF算法是在原图上通过建立后悔边的方法来加快dfs,哪怕一开始我们选择的路径是错误的,我们也可以通过后悔边不断去修正这个错误,以此达到最后正确的结果(把所有错误的情况修正了,就是正确的结果之一)

后悔边
在这里插入图片描述
我们一开始会对所有的有向边建一条反向的容量为0的边,这就是后悔边
同时后悔边有一个性质
在任意一个残量网络中,残量网络边上的容量+后悔边上的容量=这条边初始的容量

现在我们来看看后悔边是怎么生效的
在这里插入图片描述
我们可以看见,当我们沿着后悔边去走了一段达到了终点后,我们再次修改,就相当于把原来不该走的错误修正了,最后网络会进行正确的增广,这就是后悔边的作用

对于一幅残量网络,新找到的需要通过后悔边到达终点的路,相当于将原来后悔边所在处的路从原来的路径删去

在这里插入图片描述
假设回头的路是我们的后悔边,那么我们沿着后悔走上的那几段,都应该从之前找到的路径删去并更新网络,最后我们的图会变成正确的模样(比如上图我们一开始只找到三条直达路,但我们新找到了最下面那条很长弯曲的路,其中经过两条后悔边,那么我们从原来的网络删去这两段,最后网络会更新成右边正确的样子,即有四条路(假设路多就是最大流大))

那么现在我们显然发现了后悔边是能维护结果的正确性的,那么我们来实现FF

先回顾整个过程,我们最少得有两个函数

ll dfs(int v,int t,ll flow)//去找增广路
{
	if(v==t)//到达汇点返回
		return flow;
    used[v]=true;
    for(int i=0;i<G[v].size();i++)//遍历整幅图
    {
        edge &e=G[v][i];
        if(!used[e.to]&&e.cap>0)//下个结点未被访问过,并且这条路容量大于0就走
        {
            int d=dfs(e.to,t,min(flow,e.cap));
            if(d>0)//找到增广路
            {
                e.cap-=d;//容量-d
                G[e.to][e.rev].cap+=d;//反向边增加d
                return d;
            }
        }
    }
	return 0;//没有找到
}

ll get_maxFlow(int s,int t)
{
	ll maxFlow = 0;
    for(;;)
    {
        memset(used,0,sizeof(used));
        ll f=dfs(s,t,INF);//不断去找增广路
        if(f==0)
            return maxFlow;//找不到就返回,可以证明当找不到增广路的时候一定是正确的解
        maxFlow+=f;
    }
}

接下来贴完整的FF模板,请确保上面两个关键函数已经看懂了
我们采用邻接表的方式存图,请注意加边函数,因为每次存边的时候是正向和反向一起当做一组存的,所以我们可以通过size来判定对应的正向边或反向边(建议自己模拟操作理解下)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define INF 0x3f3f3f3f
const int maxn=205;

struct edge{
    int to;//终点
    ll cap;//容量
    int rev;//反向边
};

int n,m,s,t;
vector<edge> G[maxn];
bool used[maxn];//dfs中用到的访问标记

void add_edge(int from,int to,ll cap){
    G[from].push_back((edge){to,cap,G[to].size()});
    G[to].push_back((edge){from,0,G[from].size()-1});
}

ll dfs(int v,int t,ll flow)//去找增广路
{
	if(v==t)//到达汇点返回
		return flow;
    used[v]=true;
    for(int i=0;i<G[v].size();i++)//遍历整幅图
    {
        edge &e=G[v][i];
        if(!used[e.to]&&e.cap>0)//下个结点未被访问过,并且这条路容量大于0就走
        {
            int d=dfs(e.to,t,min(flow,e.cap));
            if(d>0)//找到增广路
            {
                e.cap-=d;//容量-d
                G[e.to][e.rev].cap+=d;//反向边增加d
                return d;
            }
        }
    }
	return 0;//没有找到
}

ll get_maxFlow(int s,int t)
{
	ll maxFlow = 0;
    for(;;)
    {
        memset(used,0,sizeof(used));
        ll f=dfs(s,t,INF);
        if(f==0)
            return maxFlow;
        maxFlow+=f;
    }
}


int main()
{
    ios::sync_with_stdio(false);
    cin>>n>>m>>s>>t;
    int u,v;
    ll w;
    while(m--)
    {
        cin>>u>>v>>w;
        add_edge(u,v,w);
    }
    ll ans=get_maxFlow(s,t);
    cout<<ans<<endl;
    return 0;
}

可以在洛谷的网络流模板提交试试,会被卡掉两组数据,FF虽然简单易懂,但在顶点数或最大流量非常大师,这个算法就不够快了

Dinic算法

FF算法是通过深搜来找增广路,并沿着它进行增广。与之相对,dinic算法总数寻找最短的增广路(bfs来找),并沿着它增广。
因为最短增广路的长度在增广的过程中始终不会变短,所以我们不用每次都跑一遍bfs来找最短增广路。

这里引用分层图的概念

我们每一次跑一遍bfs,就可以构建一幅分层图,s为0,所有一步可达的点在层次1,两步可达的点在层次2,以此类推
我们在分层图上跑dfs,直到找不到新的增广路了,就说明最短增广路的长度确实变长了,我们就重新跑一遍bfs构造新的分层图

直到最后我们的汇点不在层次网络中,算法终止,我们就找到了最大流

PS:Dinic中有一个优化叫做当前弧优化
即每一次dfs增广时不从第一条边开始,而是用一个数组cur记录点u之前循环到了哪一条边,以此来加速

总代码如下,优化的地方已在代码中标出:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define INF 0x3f3f3f3f
const int maxn=250;

struct edge{
    int to;//终点
    ll cap;//容量
    int rev;//反向边
};

int n,m,s,t;
vector<edge> G[maxn];
int level[maxn];//距离标号
int iter[maxn];//iter就是记录当前点u循环到了哪一条边

//连边函数,edge中的rev记录的是反向边在数组中的位置
void add_edge(int from,int to,ll cap)
{
    G[from].push_back((edge){to,cap,G[to].size()});
    G[to].push_back((edge){from,0,G[from].size()-1});
}

void bfs(int s)//广搜构建分层图
{
    memset(level,-1,sizeof(level));//每跑一次bfs都要把之前的分层图清空
    queue<int> q;
    level[s]=0;//源点层次为0
    q.push(s);
    while(!q.empty())
    {
        int v=q.front();
        q.pop();
        for(int i=0;i<G[v].size();i++)
        {
            edge e=G[v][i];
            if(level[e.to]<0&&e.cap>0)//只要这个点还未访问过并且能走
            {
                level[e.to]=level[v]+1;//那这一点就是上一点能一步到达的点
                q.push(e.to);
            }
        }
    }
}

ll dfs(int v,int t,ll f)
{
    if(v==t)//到达汇点终止
        return f;
    for(int &i=iter[v];i<G[v].size();i++)//注意这里的&符号,这样i增加的同时也能改变iter[v]的值,达到记录当前弧的目的
    {
        edge &e=G[v][i];
        if(e.cap>0&&level[v]<level[e.to])//残量不为0并且满足分层图
        {
            ll d=dfs(e.to,t,min(f,e.cap));
            if(d>0)
            {
                e.cap-=d;//正向边减流量
                G[e.to][e.rev].cap+=d;//反向边加流量
                return d;
            }
        }
    }
    return 0;//否则没有增广路,返回0
}

ll dinic(int s,int t)
{
    ll flow=0;
    for(;;)
    {
        bfs(s);
        if(level[t]<0)//汇点不在分层图中了就返回,找到最大流了
            return flow;
        memset(iter,0,sizeof(iter));//每一次建立完分层图后都要把iter置为每一个点的第一条边
        ll f;
        while((f=dfs(s,t,INF))>0){//找增广路
            flow+=f;
        }
    }
}


int main()
{
    cin>>n>>m>>s>>t;
    int u,v;
    ll w;
    while(m--)
    {
        cin>>u>>v>>w;
        add_edge(u,v,w);
    }
    ll ans=dinic(s,t);
    cout<<ans<<endl;
    return 0;
}

洛谷中提交能AC的

我会在我另一篇blog中记录网络流24题每题的题解和代码

  • 8
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值