网络流基础算法模板

来源:互联网 发布:简历淘宝美工工作描述 编辑:程序博客网 时间:2024/05/19 12:12

注:这篇文章写得不咋好,仅供参考。

网络流是一种非常玄妙的算法,被广泛地用于各种有权值存在或一对多的匹配问题中。而网络流又有许多数学性质,比如最大流等于最小割等等。本篇主要介绍常用的Dinic最大流算法。

网络流的知识基础

网络流(network-flows)是一种类比水流的解决问题方法,与线性规划密切相关。网络流的理论和应用在不断发展,出现了具有增益的流、多终端流、多商品流以及网络流的分解与合成等新课题。网络流的应用已遍及通讯、运输、电力、工程规划、任务分派、设备更新以及计算机辅助设计等众多领域。——360百科

网络,就是一张有点有边图。其中有两个特殊的点——源点和汇点。网络流中的每一条边就好比一条水管,容量就好比是这个水管的粗细。我们要求的最大流,指的就是从源点开始往图里注水,水会沿着网络流动,然后从汇点流出。因为每一条边的容量一般都有限,而又因为网络流中每个点,流入它的的“水量”等于流出它的的“水量”,所以以这个从源点注入的水流的最大流量一定是有限的,否则这个网络是承载不了的。这个最大的流量就叫“最大流”。

再说说什么是割。割就是堵死网络流中的一些水管,使得这个图中的源点和汇点不再连通。水管可以被完全堵死,也可以被堵死一部分(或者你也可以把它理解为,割掉图中的一些边)。当然了,割掉一些边也是要花费一定代价的。割掉一条容量为x的边,花费的代价就为x。在所有的“割”中,花费代价最少的一种“割法”所花费的代价就叫做“最小割”。

可以用数学方法证明最大流等于最小割,感兴趣的同学可以上网查一下。在这里我只做出一个“感性”的证明:当你割掉了一条边权为x的边,如果你点子很正,那么当前残量网络(已经被处理了一部分边的网络被称为残量网络)中的最大流最多比原图中最大流减少x,而不可能减少一个大于x的数。这就证明了“任意割”大于等于“最大流”。用最小割割掉图中的一些边与用最大流的流水占据图中的一些边的原理实际上是等价的,所以最大流等于最小割。(这个证明相当不严谨,请渴望严谨的同学自行百度)

Dinic算法

Dinic算法被用来求最小割。首先,要说求最小割,我的第一种思路就是去从起点“阻塞”当前的残量网络,一直阻塞到不能阻塞为止。这种方法有一定概率得到正确答案,但是由于“贪心不总是对的”,所以有的时候这种方法会“割多”。怎么处理这种情况呢?Dinic算法利用了一种“反向边”的思想解决了这个问题。

首先我们先明确一下边的概念:

struct Edge{    int from,to,cap,flow;    //分别表示这条边的入结点,出结点,容量和流量    Edge(int FROM=0,int TO=0,int CAP=0,int FLOW=0)//初始化函数,可以没有    {        from=FROM;to=TO;cap=CAP;flow=FLOW;    }};

这里的边都是有向边,这个结构体表示一条从from流向to,容量为cap,当前已经被占用的流量为flow的边。(即残量为cap-flow,残量大于0说明这条边仍在网络流中。如果cap-flow=0,说明这条边已经被完全割掉)

Dinic算法在把正向边加入图中的同时又向图中加入了它的反向边。反向边与正向边方向相反(即从to到from),而且反向边的容量cap为0。每当我要在一条边e上割掉f的流量时,我们要做两个操作,一是把e.flow+=f,而是把e的反向边nege的流量nege.flow-=f。为什么要这样呢?

因为nege为反向边,nege.cap一定等于零,如果nege.flow=-f(f>0)。那么nege.cap-nege.flow=f>0。这条“反边”就会像正边一样被看成是残量网络中的边。假设我下一次又走了这条“反边”,那么就会让nege.flow+=f,e.flow-=f(nege和e互为反向边)。就相当于是撤销了上一次对边e的割。这就使得如果一个割不是最优的,那么它中的一些被割的边一定是可被恢复的,这就弥补了贪心的不足。

举个例子(仅供参考):

走红边

假如我在这个图中走了上图中的红边,这是三条红边的反向边也会被加入到图中。

继续走红边

继续沿着红边走,因为中间那条绿边的反向边被加入到了图中,所以可以走。走完之后绿边被还原。

走蓝边

而实际上,上面走的两条红边的效果相当于走了这两条蓝边(因为,中间的那条边被还原了)。所以“还原”一条边的实质是选择了其他的割的方案。

当然图里边也不是想怎么走怎么走的,水往低处流,而不流回头路。你可以把这理解为,把这个网络流中的点分成一些层次,每次都只能从一个层次的点走到下一个层次的点。这个层次就是从起点走到某一个点最少要经过的边数(这里的边数指的是残量网络中的边数,所以每一次増广都要重新分层)。这个分层的工作可以用一个简单的BFS实现。

然后我们给出这个模板:

#include<cstdio>#include<cstring>#include<vector>#include<queue>using namespace std;const int maxn=10001,INF=0x7fffffff;struct Edge//表示一条边{    int from,to,cap,flow;    Edge(int FROM=0,int TO=0,int CAP=0,int FLOW=0)    {        from=FROM;to=TO;cap=CAP;flow=FLOW;    }};int min(int a,int b){return a<b?a:b;}struct Dinic{    int n,m,s,t;    vector<Edge>edges;//表示图中的所有边     vector<int>G[maxn];//表示每个结点的出边    //G[i][j]表示结点i的第j条出边    bool vis[maxn];//BFS的标记数组    int d[maxn];//从起点到i的距离(分层编号)    int cur[maxn];//当前弧下标    void AddEdge(int from,int to,int cap)//添加一条边     {        edges.push_back(Edge(from,to,cap,0));//添加一条正向边         edges.push_back(Edge(to,from,0,0));//添加一条反向边         m=edges.size();        G[from].push_back(m-2);//更新出边表         G[to].push_back(m-1);     }    bool BFS()//分层BFS     {        memset(vis,0,sizeof(vis));//初始化         queue<int>Q;        Q.push(s);//起点入队        d[s]=0;vis[s]=1;//起点的编号为0        while(!Q.empty())        {            int x=Q.front();Q.pop();//取队首             for(int i=0;i<G[x].size();i++)            {                Edge& e=edges[G[x][i]];//对于x的每条出边i                if(!vis[e.to] && e.cap>e.flow)//如果当前边还存在于网络中                 {                    vis[e.to]=1;//把边的抵达结点记为已访问                     d[e.to]=d[x]+1;                    Q.push(e.to);                }            }        }        return vis[t];//如果汇点没被访问        //说明从原点到汇点之间已不存在增广路,不可増广        /上文中的d[i]表示从s到i最少经过的边数    }    int DFS(int x,int a)//x表示当前点,a表示当前弧上的最小残量     {        if(x==t || a==0)return a;//走到终点或发现当前路不可増广        int flow=0,f;        for(int& i=cur[x];i<G[x].size();i++)//从上次考虑到的弧开始         {            //我们每次都会从0开始考虑某一个结点的每一条出边            //但一次分层处理中某个点会被DFS多次            //cur[x]表示点x的前cur[x]-1条边已经在之前的DFS中被考虑过了            //如果现在再继续考虑,得到的结果也只能是0            //所以直接跳过对它们的考虑            Edge& e=edges[G[x][i]];            if(d[x]+1==d[e.to] && (f=DFS(e.to,min(a,e.cap-e.flow)))>0)            {                e.flow+=f;//正边的流加f                 edges[G[x][i]^1].flow-=f;//反边的流减f                 flow+=f;//弧的流量+=f                 a-=f;//剩余残量-=f                 if(a==0)break;//残量一点不剩,直接退出             }        }        return flow;    }    int Maxflow(int s,int t)    {        this->s=s;this->t=t;        int flow=0;        while(BFS())        {            memset(cur,0,sizeof(cur));            flow+=DFS(s,INF);        }        return flow;    }}dinic;

这个DFS函数是网络流的精髓,它表示我当前已经走到了结点x,当前经过的最小的一个边的容量为a,所能产生的最大的流量。令e为x的所有出边,就有DFS(x,a)=∑DFS(e.to,min(m,e.cap-e.flow)),因为点x流入流量等于a,所以流出的流量和也就是∑m=a。用贪心的想法,每次在a的身上减去DFS(e.to,min(a,e.cap-e.flow)),这样剩余流出量就会越来越少,直到等于0就break(这是用来提速的)。如果贪心不正确,那么它在接下来的贪心之中还会被“贪回来”。

BFS函数用来分层,返回的值是汇点t是否被访问过。如果汇点t没有被访问过就说明从源点到汇点已经没有任何一条増广路径了,算法就可以终止。起点没有前驱结点所以它的前驱路径上的最小边权就为正无穷,所以我们每次可以对起点进行a=+∞的DFS,然后把这些得数累加起来,直到BFS=false为止。

赶稿匆忙,如有谬误,望同学们理解。

声明:之前版本的Dinic代码有BUG,望谅解,现已刷新。——2017.8.8

原创粉丝点击