网络流基础

网络流基础

网络流问题

相关概念:node

  • 源点:有n个点,有m条有向边,有一个点很特殊,只出不进,叫作源点。
  • 汇点:另外一个点也很特殊,只进不出,叫作汇点。
  • 容量和流量:每条有向边上有两个量,容量和流量,从i到j的容量一般用c[i,j]表示,流量则一般是f[i,j]。
  • 最大流:通俗点解释,就比如你有不少货物要从源点点运到汇点点,有向图中的一条边表明一条公路,每条公路有固定的货物装载限制(容量),对每条公路你只能运输必定数量的货物,问你每一次运输最多运到汇点点多少货物。

给定指定的一个有向图,其中有两个特殊的源点S和汇点T,每条边有指定的容量,求知足条件的从S到T的最大流。ios

网络流的性质

  • 容量限制:f[u,v]<=c[u,v]
  • 反对称性:f[u,v] = - f[v,u]
  • 流量平衡:对于不是源点也不是汇点的任意结点,流入该结点的流量和等于流出该结点的流量和。

残量网络,容量网络,流量网络

残量网络=容量网络-流量网络
概念就不讲了吧,顾名思义。算法

增广路

增广路: 设 f 是一个容量网络 G 中的一个可行流, P 是从 Vs 到 Vt 的一条链, 若 P 知足下列条件:网络

  • 在 P 的全部前向弧 <u, v> 上, , 即 P+ 中每一条弧都是非饱和弧;
  • 在 P 的全部后向弧 <u, v> 上, , 即 P– 中每一条弧是非零流弧。

则称 P 为关于可行流 f 的一条增广路, 简称为 增广路(或称为增广链、可改进路)。沿着增广路改进可行流的操做称为增广优化

最小割最大流定理

割,割集

对于一张流量图G,断开一些边后,源点s和汇点t就不在连通,咱们将这样的k条边的权值(即最大容量)和求和,求和后的值称为割。显然,对于一张流量图G,割有不少个且不尽相同。咱们要求的就是全部割中权值最小的那一个(可能不惟一),即花最小的代价使s和t不在同一集合中。spa

最小割最大流定理

  • 任意一个流都小于等于任意一个割
  • 构造出一个流等于一个割
  • 在一张流量图G中,最大流=最小割。

网络流问题解决方法

FF方法(Ford-Fulkerson)code

基本思想

根据增广路定理, 为了获得最大流, 能够从任何一个可行流开始, 沿着增广路对网络流进行增广, 直到网络中不存在增广路为止,这样的算法称为增广路算法。问题的关键在于如何有效地找到增广路, 并保证算法在有限次增广后必定终止。
FF方法的基本流程是 :blog

  • (1) 取一个可行流 f 做为初始流(若是没有给定初始流,则取零流 f= { 0 }做为初始流);
  • (2) 寻找关于 f 的增广路 P,若是找到,则沿着这条增广路 P 将 f 改进成一个更大的流, 并创建相应的反向弧;
  • (3) 重复第(2)步直到 f 不存在增广路为止。



反向弧创建的意义:为程序提供反悔的机会

很明显,上图最大流应该是2,但咱们找到了一条错误的路径,因而咱们就应该有返回的机会,即创建反向边,这样再次从反向边流过就至关于抵消了。队列

算法一:EK算法(EdmondsKarp)

算法思路

在EK算法中, 程序的实现过程与增广路求最大流的过程基本一致. 即每一次更新都进行一次找增广路而后更新路径上的流量的过程。可是咱们能够从上图中发现一个问题, 就是每次找到的增广路曲曲折折很是长, 此时咱们每每走了冤枉路(即:明明咱们能够从源点离汇点越走越近的,但是中间的几条边却向离汇点远的方向走了), 此时更新增广路的复杂度就会增长。EK 算法为了规避这个问题使用了 bfs 来寻找增广路, 而后在寻找增广路的时候老是向离汇点愈来愈近的方向去寻找下一个结点。博客

复杂度$\varTheta(m^{2}n)$

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define INF 0x7fffffff
#define N 10010
#define M 100010
using namespace std;
int n,m,ss,tt;
struct Edge{int to;int next;int value;}e[M<<1];
struct Pre{int node;int id;}pre[M<<1];//pre[i].node表示编号为i的点最短路的上一个点,pre[i].id表示最短路上链接i点的边的编号
int head[N],cnt=-1;//编号从0开始,缘由见下
bool vis[N];
queue<int> q;
void add(int from,int to,int value)
{
    cnt++;
    e[cnt].to=to;
    e[cnt].value=value;
    e[cnt].next=head[from];
    head[from]=cnt;
}
bool bfs(int s,int t)//用来寻找s,t的最短路并记录,若是s,t不连通则返回0
{
    q=queue<int>();//清空队列
    memset(vis,0,sizeof(vis));
    memset(pre,-1,sizeof(pre));
    pre[s].node=s;
    vis[s]=1;
    q.push(s);
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(int i=head[x];i>-1;i=e[i].next)
        {
            int now=e[i].to;
            if(!vis[now]&&e[i].value)//忽略流量为0的边
            {
                pre[now].node=x;//用pre记录最短路
                pre[now].id=i;
                vis[now]=1;
                if(now==t)return 1;//找到
                q.push(now);
            }
        }
    }
    return 0;
}

int EK(int s,int t)
{
    int ans=0;
    while(bfs(s,t))
    {
        int minv=INF;
        for(int i=t;i!=s;i=pre[i].node)
            minv=min(minv,e[pre[i].id].value);
        for(int i=t;i!=s;i=pre[i].node)
        {
            e[pre[i].id].value-=minv;
            e[pre[i].id^1].value+=minv;//x^1表示x边的反向边,此方法仅在边的编号从0开始时有效
        }
        ans+=minv;
    }
    return ans;
}
int main()
{
    memset(head,-1,sizeof(head));
    scanf("%d%d%d%d",&n,&m,&ss,&tt);
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
        add(b,a,0);//创建反向边
    }
    printf("%d\n",EK(ss,tt));
    return 0;
}

算法二:Dinic算法

其实Dinic算法是EK算法的改进

算法思路

发如今EK算法中,每增广一次都要先进行bfs寻找最短增广路,然而bfs后,极可能不止一条路径能够增广,若是仍是按照EK算法的bfs一次增广一条路,很显然浪费了不少时间,这样,咱们让bfs负责寻找增广路径,dfs计算可行的最大流。

下图1点为s点,6点为t点,红线表明寻找的路径,蓝线表明回溯的路径:

  • 图1,bfs计算dis
    • 图2,dfs按最短路找到t点,累加路径上的最小容量
    • 图3,回溯,顺便更新正边和反向边的边权
    • 无其余路径,回溯到源点
  • 图4,再次bfs更新dis
    • 图5,dfs按最短路找到t点,累加路径上的最小容量
    • 图6,回溯,顺便更新正边和反向边的边权
    • 无符合要求的其余路径,回溯到源点
  • 再次bfs,发现s和t不连通,结束算法

复杂度:

在普通图中:$\varTheta(n^{2}m)$
在二分图中:$\varTheta(m\sqrt{n})$

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define N 10010
#define M 100010
#define INF 0x7fffffff
using namespace std;
int n,m,ss,tt;
int dis[N];
queue<int> q;

struct Edge{int to;int value;int next;}e[M<<1];
int head[N],cnt=-1;
void add(int from,int to,int value)
{
    cnt++;
    e[cnt].to=to;
    e[cnt].value=value;
    e[cnt].next=head[from];
    head[from]=cnt;
}

bool bfs(int s,int t)//bfs功能和EK算法的类似,不一样的是Dinic中的bfs要求出全部点到源点s的最短路dis[i]
{
    q=queue<int>();//清空队列
    memset(dis,-1,sizeof(dis));
    dis[s]=0;
    q.push(s);
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(int i=head[x];i>-1;i=e[i].next)
        {
            int now=e[i].to;
            if(dis[now]==-1&&e[i].value!=0)
            {
                dis[now]=dis[x]+1;
                q.push(now);
            }
        }
    }
    return dis[t]!=-1;
}
int dfs(int x,int t,int maxflow)//表示从x出发寻找到汇点T的增广路,寻找到maxflow流量为止,并相应的增广。返回值为实际增广了多少(由于有可能找不到maxflow流量的增广路)
{
    if(x==t)return maxflow;
    int ans=0;
    for(int i=head[x];i>-1;i=e[i].next)
    {
        int now=e[i].to;
        if(dis[now]!=dis[x]+1||e[i].value==0||ans>=maxflow)continue;
        int f=dfs(now,t,min(e[i].value,maxflow-ans));
        e[i].value-=f;
        e[i^1].value+=f;
        ans+=f;
    }
    return ans;
}
int Dinic(int s,int t)
{
    int ans=0;
    while(bfs(s,t))
        ans+=dfs(s,t,INF);
    return ans;
}
int main()
{
    memset(head,-1,sizeof(head));
    scanf("%d%d%d%d",&n,&m,&ss,&tt);
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
        add(b,a,0);
    }
    printf("%d\n",Dinic(ss,tt));
    return 0;
}

当前弧优化

咱们知道Dinic算法中的dfs是为了在可行增广路中找到最小容量并进行增广。而找增广路须要遍历每一个点所链接的边,直至找到一条可到达终点的路。若是这一次找到了增广路,下一次在访问到这个点时,上一次已经检查过的边就不用再走一遍了,由于遍历一个点链接的边都是有必定顺序的,上一次访问到这个点已经肯定那几条边是不可行的。因而,咱们用cur[i]来表示下一次遍历边时应该从那一条开始。

虽然渐进时间复杂度没有发生变化,但实际应用中的确大大下降了Dinic的常数

优化代码(其余代码不发生变化)

int cur[N];

int dfs(int x,int t,int maxflow)
{
    if(x==t)return maxflow;
    int ans=0;
    for(int i=cur[x];i>-1;i=e[i].next)
    {
        int now=e[i].to;
        if(dis[now]!=dis[x]+1||e[i].value==0||ans>=maxflow)continue;
        cur[x]=i;//此路可行,记录此路
        int f=dfs(now,t,min(e[i].value,maxflow-ans));
        e[i].value-=f;
        e[i^1].value+=f;
        ans+=f;
    }
    return ans;
}
int Dinic(int s,int t)
{
    int ans=0;
    while(bfs(s,t))
    {
        memcpy(cur,head,sizeof(head));//初始化
        ans+=dfs(s,t,INF);
    }
    return ans;
}

网络流的优化算法还有ISAP(Improved Shortest Augumenting Path),最高标号预流推动(HLPP)等等,Dinic在通常状况下已经够用了,其余算法自学请移步其余大佬博客喽。

相关文章
相关标签/搜索