2016.8.9最大流

来源:互联网 发布:ubuntu 压缩 编辑:程序博客网 时间:2024/06/06 10:55

  现在有一道题目,大概是这样的,有N个城市和M条将它们相连的公路,每条公路都有单位时间内的车流量限制,求单位时间内从城市1到N的车流量最大为多少。
  看这一题可以转换成在一幅图找1到N的最大流量,也就是最大流问题。1是源点,N是汇点,每条公路的车流限制就是每条边的容量。首先想到的就是用搜索,每次选流量最大的一条路径流,直到整幅图再也无法找到源点流向汇点的路径。但是否是正确的呢?
  看一个例子:
  这里写图片描述
  用上面的方法,先找到S->1->2->T这条路径,然后就没有路径可以从S->T,所以计算出的最大流为3。
  但细心分析,肯定会发现这种方法是不对的,因为可以先S->1->T,流量为2,然后S->2->T,流量为2,最后S->1->2->T,流量为1(前面已经用了,S->1剩下1的流量,2->T剩下1的流量,所以这条路径最多流1),计算出的最大流为5。说明上面的方法是不对的,那要怎么做呢。

增广路算法

  介绍这个算法之前,先介绍几个基本概念:
  残余网络:也就是每条边余剩容量和点组成的网络(原网络每条边减掉已用容量(减到0就删去))。
  层次网络:就是将残余网络按到源点的最短距离分层(或标记)(与边的容量无关)。
  增广路算法的总体思想就是在原有边的基础上,增加一条反向弧(不能与跟原有边方向相反的边一起储存,应该分开),它的初始容量为0,如果原有边的容量减少k,则这条反向弧的容量增加k,如果反向弧的的容量减少k,那么原有边的容量也增加k,也就是说,反向弧余剩容量+原有边余剩容量=原有边原有容量,反向弧的容量就是这条边流出的流量,这样做,就可以使流出去的流量流回来,即“反悔”。加了反向弧后,将它看成一条与其他边一样的边,再用上面的方法进行搜索。
  具体实现及证明看下图:
  这里写图片描述
  这还是刚刚那幅图,加上了反向弧,A/B,A指最大容量,B指余剩容量。与刚刚的方法类似,先找出一条路径,处理后的情况是这样的:
  这里写图片描述
  这次的流量是3,路径是S->1->2->T,由图可以看出,每条反向弧的容量都增加了3。
  如果没有反向弧,那么算法到这里就已经结束了,当然答案也是错误的。如果有反向弧,那么从源点到汇点仍然是有路径的,如下图:
  这里写图片描述
  走反向弧的意思就是:本来1->2流过来3个流量,现在我将两个流量退回1点,再从1->T,使从S->2来的流量可以通过2->T流到汇点。但如何证明可以向回流呢?
  其实上面的做法可以等价于S->1->2->T只流一个流量,剩下本来要从这条路径流到汇点的2个流量,通过S->1->T这条路流到汇点。S->2->T则流两个,这是一种通俗的证明方法,可能不太准确,详细的可以在网上搜。
  按照上面的思想,可以很容易想出一种方法,就是不断增广,直到再也无法增广(没有路径从源点到达汇点),则求出了最大流。
代码:

#include <iostream>#include <fstream>#include <algorithm>#include <string.h>#include <cstring>#include <math.h>#include <cmath>using namespace std;const int MAXV = 10000006,          MAXN = 406;int head[MAXN],v[MAXN],node[MAXN],next[MAXN];bool f[MAXN];int n,m,x,y,z,Min,ans=0,fp=0;bool dfs(int k) //使用dfs增广 {    if (k==n) {ans+=Min;return 1;} //找到一条路径就结束搜索     f[k]=1;    for (int i=head[k];i!=-1;i=next[i])    {        if (v[i]==0 || f[node[i]]) continue;        int mx=Min;        Min=min(Min,v[i]); //记录路径上的边的最小容量(即最后能够到达汇点的最大流量)         if (dfs(node[i]))        {            v[i]-=Min;            v[i^1]+=Min; //将自己的对应边(反向弧对原有边,连续下标储存,可以直接^1)             return 1;//找到一条路径就结束搜索         }        Min=mx;    }    return 0;}void _insert(int x,int y,int z) //邻接链表 {    node[fp]=y;    next[fp]=head[x];    head[x]=fp;    v[fp]=z;    fp++;}int main(){    while (~scanf("%d%d",&m,&n))    {        ans=0;fp=0;        memset(head,-1,sizeof head);        for (int i=0;i<m;i++)        {            scanf("%d%d%d",&x,&y,&z);            _insert(x,y,z); //添加原有边入图             _insert(y,x,0); //添加反向弧入图        }        do        {            memset(f,0,sizeof f);            Min=MAXV;        }while (dfs(1)); //增广至无法增广         printf("%d\n",ans);    }    return 0;}

最短增广路算法

  其实最短增广路算法只是比上一种多了一个“最短”,也就是保证,每次走的路径都要保证是当前最短的,要保证最短,可以用最短路建立一个层次网络(见上面定义),每次保证只从第k层走到第k+1层,由于源点一定是在第0层,汇点的层数就是源点到汇点的最短距离,所以保证走的是最短路,当在这一阶段的分层图中再也找不到路径,那么就需要再次分层(可能增加了许多反向弧,减少了原有边),最后当发现图中再也没有路径可以从源点到达汇点(不一定是最短路,在最短路时可以判断),那么最短增广路算法就结束了。

  在最短增广路中,最多建立n个层次网络,每个层次网络用BFS一次遍历即可得到。一次BFS的复杂度为O(m),所以建层次图的总复杂度为O(n*m)。
  每增广一次,层次网络中必定有一条边会被删除。层次网络中最多有m条边,所以认为最多可以增广m次。在最短增广路算法中,用DFS来增广,一次增广的复杂度为O(n+m),其中O(m)为DFS的花费,O(n)为修改流量的花费。所以在每一阶段寻找增广路的复杂度为O(m*(m+n)) = O(m*m)。因此n个阶段寻找增广路的算法总复杂度为O(n*m*m)。
  两者之和为O(n*m*m)。

Dinic算法

  dinic算法其实也是增广路算法的优化,它的思想就是,将本来每次只能增广一次DFS改进,使得它可以实现多次增广。
  在上面的算法中,DFS只要找到一条路径,就会结束搜索,这很明显没有利用好时间,下一次又要从头开始搜索,而dinic就是改进了这一方面。
  对于每一个点,本来是只找一条边流出,现在是找多条边,尽量将前面的边能流过来的流完,每个点都如此,这样就能一次DFS就完成这一阶段层次网络增广,再用BFS进行分层,直到图中再也没有路径可以从源点到达汇点,算法结束。
  dinic算法总是寻找最短的増广路并沿着它增广,増广路的长度不会在增广过程中改变,则当无法增广时,说明分层图上没有可以增广的路线了,这有两种情况,第一,已经求出了最大流,第二,可能存在长一些的増广路可以继续增广,因此,继续bfs构造分层网络。每次完成后最短増广路长度+1,由于最短路﹤n,则最对重复n-1次bfs就可完成了。
  在每一阶段,最多增广m次,每次修改流量的费用为O(n)。而一次增广后在增广路中后退的费用也为O(n)。所以在每一阶段中,修改增广路以及后退的复杂度为O(m*n)。
  所以总复杂度就是O(m*n^2),由于一般情况下,边的数量要比点的数量大的多,所以时间复杂度是大大降低了的。
代码:

#include <iostream>#include <fstream>#include <stdio.h>#include <algorithm>#include <string.h>#include <iomanip>using namespace std;struct Tline{    int x,y,v;    Tline(){v=0;}};const int MAXV = 10000006,          MAXN = 206,          MAXM = 406,          INF = 2000000000;int n,m,a,b,v,Min,ans=0,fp=0;Tline connect[MAXM];int line[MAXN];int next[MAXM],head[MAXN],dis[MAXN];bool f[MAXN];void _add(int a,int b,int v){    connect[fp].x=a;    connect[fp].y=b;    connect[fp].v=v;    next[fp]=head[a];    head[a]=fp;    fp++;}bool BFS() //最短路分层 {    int h=0,t=0;    memset(f,0,sizeof f);    line[t++]=1;f[1]=1;    dis[1]=0;    while (h<t)    {        for (int i=head[line[h]];i!=-1;i=next[i])            if (f[connect[i].y]==0 && connect[i].v>0)            {                f[connect[i].y]=1;                dis[connect[i].y]=dis[line[h]]+1; //记录源点到这个点的最短距离                 line[t++]=connect[i].y;            }        h++;    }    return f[n];}int DFS(int root,int Min){    int ans=0,tmp;    if (root==n) {return Min;}    for (int i=head[root];i!=-1;i=next[i])    {        if (connect[i].v==0 || dis[connect[i].y]!=dis[root]+1) continue; //一定是从dis[root]层到dis[root]+1层         tmp=DFS(connect[i].y,min(Min-ans,connect[i].v));        connect[i].v-=tmp;        connect[i^1].v+=tmp;        ans+=tmp;        if (ans==Min) return ans; //尽量多流     }    return ans;}int main(){    while (~scanf("%d%d",&m,&n))    {        memset(head,-1,sizeof head);        memset(next,-1,sizeof next);        fp=0;ans=0;        for (int i=0;i<m;i++)        {            scanf("%d%d%d",&a,&b,&v);            _add(a,b,v); //添加原有边             _add(b,a,0); //添加反向弧         }        while (BFS())        {        //  for (int i=1;i<=n;i++) cout << dis[i] << ' ';cout << endl;            Min=MAXV;            ans+=DFS(1,INF);        }        printf("%d\n",ans);    }}
1 0
原创粉丝点击