网络流——最大流与最小割

网络流——最大流与最小割

最大流

先看一道题(选自usaco题库4.2.1):

【USACO题库】4.2.1 Drainage Ditches草地排水

时间限制: 1000 ms 空间限制: 128000 KB 具体限制

题目描述
在农夫约翰的农场上,每逢下雨,贝茜最喜欢的三叶草地就积聚了一潭水。这意味着草地被水淹没了,并且小草要继续生长还要花相当长一段时间。因此,农夫约翰修建了一套排水系统来使贝茜的草地免除被大水淹没的烦恼(不用担心,雨水会流向附近的一条小溪)。作为一名一流的技师,农夫约翰已经在每条排水沟的一端安上了控制器,这样他可以控制流入排水沟的水流量。
农夫约翰知道每一条排水沟每分钟可以流过的水量,和排水系统的准确布局(起点为水潭而终点为小溪的一张网)。需要注意的是,有些时候从一处到另一处不只有一条排水沟。
根据这些信息,计算从水潭排水到小溪的最大流量。对于给出的每条排水沟,雨水只能沿着一个方向流动,注意可能会出现雨水环形流动的情形。

输入
第1行: 两个用空格分开的整数N (0 <= N <= 500000) 和 M (2 <= M <= 1000)。N是农夫约翰已经挖好的排水沟的数量,M是排水沟交叉点的数量。交点1是水潭,交点M是小溪。
第二行到第N+1行: 每行有三个整数,Si, Ei, 和 Ci。Si 和 Ei (1 <= Si, Ei <= M) 指明排水沟两端的交点,雨水从Si 流向Ei。Ci (0 <= Ci <= 10,000,000)是这条排水沟的最大容量。

输出
输出一个整数,即排水的最大流量。

样例输入

5 4
1 2 40
1 4 20
2 4 20
2 3 30
3 4 10

样例输出

50

数据范围限制
N (0 <= N <= 500000)
M (2 <= M <= 1000)

这道题看了看,发现用之前所学的算法都没有什么思路,那么,我们现在就引进一种新的算法:网络流。而网络流又分为很多种:最大流,最小割,费用流等,这一道题用的是最大流。

何为最大流?
抽象地讲是给你一个网络,有一个源点和汇点,现在要从源点放水,水从一条边流到另一个点,但最多只能流一个值的水,下一个点又这样流向另一个点,问汇点最多会流入多少水?

知道了这些,我们就了解一些概念:

概念

    • 剩余图
    • 给定一个流量网络 G1=(E1,V1) 源点s汇点t、容量函数为c(u,v)以及其上的流量函数f(u,v)。那么定义剩余图 G2=(E2,V2) :剩余图中的点集与流量网络中的点集相同,即 V1=V2 ,对于流量网络中的任一条边 (u,v)E1 f(u,v)<c(u,v) (u,v)E2 这条边在剩余图中的权值为 g(u,v)=c(u,v)f(u,v) ,同时,若 f(u,v)>0 (v,u)E2 g(v,u)=f(u,v)
    • 我们可以发现,流量网络中的每条边在剩余图中都化作一条或二条边。剩余图中的每条边都表示在原流量网络中能沿其方向增广。剩余图的权值函数 g(u,v) 表示在流量网络中能够沿着u到v 的方向增广大小为 g(u,v) 的流量。所以在剩余图中,从源点到汇点的任意一条简单路径都对应着一条增广路,路径上每条边的权值的最小值即为能够一次增广的最大流量。
    • 顶点的层次
    • 在剩余图中,我们把从源点到点u的最短路径长度称作点u的层次,记为 level(u) 。源点的层次为0。在下面这张剩余图中:
    • 网络流——最大流与最小割_第1张图片
    • 每个点旁边的数字即表示该点在图中的层次。
    • 层次图的概念
    • 我们这样定义层次图 G3=(E3,V3) :对于剩余图 G2=(E2,V2) 中的一条边 (u,v) ,当且仅当 level(u)+1=level(v) 时,边 (u,v)E3 V3|E3
    • 直观地讲,层次图是建立在剩余图基础之上的一张“最短路图”。从源点开始,在层次图中沿着边不管怎么走,经过的路径一定是终点在剩余图中的最短路。
    • 阻塞流的概念
    • 在流量网络中存在一可行流 f ,当该网络的层次图 G3 中不存在增广路时,我们称流函数 f 为层次图 G3 的阻塞流。

算法

1.最短路径增值算法(MPLA)

主要步骤:
  1. 初始化流量,计算出剩余图
  2. 根据剩余图计算层次图。若汇点不在层次图内,则算法结束
  3. 在层次图内不断用bfs增广,直到层次图内没有增广路为止
  4. 转步骤2

-

  • 算法中,2、3步被循环执行,我们将执行2、3步的一次循环称为一个阶段
  • 每个阶段中,我们首先根据剩余图建立层次图,然后不断用bfs在层次图内增广,寻找阻塞流。增广完毕后,进入下一个阶段。这样不断重复,直到汇点不在层次图内出现为止。汇点不在层次图内意味着在剩余图中不存在一条从源点到汇点的路径,即没有增广路。
  • 在程序实现的时候,层次图并不用被“建”出来,我们只需对每个顶点标记层次,增广的时候,判断边是否满足 level(u)+1=level(v) 这一约束即可。
定理的证明

定理:对于有n个点的流量网络,在最短路径增值算法中,最多有n个阶段。
也就是说,在算法中层次图最多被建立n次。证明这个定理有助于我们进行算法复杂度分析。

证明:
在建立完层次图以后,假设从源点到汇点的最短路径长度为k,我们将层次图中所有的点分到k+1个集合中,第i个集合为 {u|level(u)=i1} ,如下图所示:
网络流——最大流与最小割_第2张图片
在剩余图中,存在着2类边。

  • 第一类:从第 个集合中的顶点连到第i+1(1≤i≤k)个集合中的顶点
  • 第二类:从第i(1≤i≤k+1)个集合中的顶点连到第j(1≤j≤i)个集合中的顶点

在层次图中,只存在第一类边,这是由层次图的性质决定的。我们所要找的增广路中的边也必定是第一类边。

当我们对一条增广路径增广后,会删除一条或多条增广路中的饱和边,也就是第一类边;而同时会在剩余图中加入一些与增广路径中的边反向的边。这些新加入的边一定是第二类边。如下图所示,在剩余图(a)中,找到一条从左向右的增广路径,能够增广的流量大小为2。增广后的结果是剩余图(b)。可以发现,在剩余图(a)里面,中间一条红色第一类边在增广后饱和而被删除了,同时,在剩余图(b)中,新增了2条绿色的第二类边。
网络流——最大流与最小割_第3张图片

当我们在层次图中找到阻塞流之后,层次图中就不存在从第一个集合一步一步往下走,最后达到第k+1个集合的长为k的路径了。而此时不在层次图中的边都是第二类边。我们可以发现,这个时候在剩余图中的最短路径一定是这样:从源点开始,往下一步一步走,走到某个集合后沿着第二类边向上退至某个集合,再继续一步一步向下走,到某个集合又向上退…………直到走到汇点。

因为必然会经过第二类边,而经过的第一类边的数量>=k,所以路径总长度一定大于k。这即是下一个阶段的最短路径长度。
由此,我们得出了一个结论:

结论:层次图中增广路径长度随阶段而严格递增。
因为增广路径长度最短是1,最长是n-1 ,再算上汇点不在层次图内的最后一次,层次图最多被建造n次,所以最短路径增值算法最多有n个阶段。
证毕。

增广复杂度分析

我们首先分析在每一阶段中找增广路的复杂度。

注意到每增广一次,层次图中必定有一条边会被删除。层次图中最多有m条边,所以可以认为最多增广m次。在最短路径增广中,我们用bfs来增广。一次增广的复杂度为O(n+m),其中O(m)为bfs的花费,O(n)为修改流量的话费。所以在每一阶段的复杂度为 O(m(n+m))=O(m2)

这样,得到找增广路总的复杂度为 O(nm2)

最短路径增值算法的总复杂度即为建层次图的总复杂度与找增广路的总复杂度之和,为 O(nm2)

2.Dinic算法

算法步骤

Dinic算法的思想也是分阶段地在层次图中增广。它与最短路径增值算法不同之处是:在Dinic算法中,我们用一个dfs过程代替多次bfs来寻找阻塞流。下面给出其算法步骤:
1. 初始化流量,计算出剩余图
2. 根据剩余图计算层次图。若汇点不在层次图内,则算法结束
3. 在层次图内用一次dfs过程增广
4. 转步骤2

-
在Dinic的算法步骤中,只有第三步与最短路径增值算法不同。之后我们会发现,dfs过程将会使算法的效率较之MPLA有非常大的提高。
下面是dfs过程

p←s;
While outdegree(s)>0
    u←p.top;
    if u<>t
    if outdegree(u)>0
        设(u,v)为层次图中的一条边;
        p←p,v;   
    else
        从p和层次图中删除点u,
        以及和u连接的所有边;
else
    增广p(删除了p中的饱和边);
    令p.top为p中从s可到达的最后顶点;
end while
  • 在程序里,p表示找到的增广路径,p.top为路径中的最后一个顶点。一开始,p中只有源点。
  • 整个While循环分为2个操作。如果p的最后一个顶点为汇点,也就是说找到了增广路,那么对p增广,注意到增广后一定有一条或多条p中的边被删除了。这时,我们使增广路径后退至p中从源点可到达的最后一个顶点
  • 如果p的最后一个顶点不为汇点,那么观察最后那个的顶点u 。若在层次图中存在从u连出的一条边,比如(u,v),我们就将顶点v放入路径p中,继续dfs遍历;否则,点u对之后的dfs遍历就没有用了,我们将点u以及层次图中连到u的所有边删除,并且在p中后退一个点。
  • Dfs过程将会不断重复这2个操作,直到从源点连出的边全部被删除为止。
  • 下面给出一个dfs的图例,图中,红边代表找到的增广路p中的边。

网络流——最大流与最小割_第4张图片网络流——最大流与最小割_第5张图片

复杂度分析

和在最短路径增值算法中的证明一样,Dinic算法最多被分为n个阶段。
这样首先可以得到Dinic算法中建立层次图的总复杂度仍是O(nm)。

我们再来分析dfs过程的总复杂度。在每一阶段,将dfs分成2部分分析:

p←s;
While outdegree(s)>0
    u←p.top;
//1{
    if u<>t
        if outdegree(u)>0
            设(u,v)为层次图中的一条边;
            p←p,v;   
        else
            从p和层次图中删除点u,
            以及和u连接的所有边;  
//}

//2{
    else
        增广p(删除了p中的饱和边);
        令p.top为p中从s可到达的最后顶点;

//}
end while
    • (1) 修改增广路的流量并后退的花费:(即为代码中框{2}对应的部分)
    • 在3.3.2小节中我们讲到过,在每一阶段,最多增广m次,每次修改流量的费用为O(n)。而一次增广后在增广路中后退的费用也为O(n)。所以在每一阶段,修改增广路以及后退的复杂度为 O(m(n+m))=O(m2)
    • (2) Dfs遍历时的前进与后退:(即为代码中框{1}对应的部分)
    • 在dfs遍历时,如果当前路径的最后一个顶点能够继续扩展,则一定是沿着第一类边向汇点前进了一步。因为增广路径长度最长为n,所以最多连续前进n步后就会遇到汇点。在前进的过程中,可能会遇到没有边能够沿着继续前进的情况,这时,我们将路径中的最后一个点在层次图中删除并出栈 。
    • 注意到每后退一次必定会删除一个点,所以后退的次数最多为n次。在每一阶段中,后退的复杂度为O(n)。
    • 假设在最坏情况下,所有的点最后均被删除,一共后退了n次,这也就意味着,有n次的前进被“无情”地退了回来,这n次前进操作都打了水漂。除去这n次前进和n次后退,其余的前进都对最后找到增广路作了贡献。增广路最多找m次,每次最多前进n个点。所以所有前进操作最多为n+nm次,因此复杂度为 O(nm)
    • 于是我们得到:在每一阶段中,dfs遍历时前进与后退的花费为 O(n)+O(nm)=O(nm)

综合以上二点,一次dfs的复杂度为 O(nm) ,因为最多进行n次dfs,所以在Dinic算法中找增广路的总复杂度为 O(n2m)

#include 
#include 
#include 

using namespace std;

const int maxn = 100005,maxm = 100005;

struct node
{
    int to,next,flow;//目标点,下一条边,剩余流量

    node(void){}
    node(int a,int b,int c) : to(a),next(b),flow(c){}
}e[maxm * 2];//正向+反向弧

int d[maxn];//距离标号,距离原点的最短距离
int final[maxn],cur[maxn];
int n,m,tot,s,t;

void link(int u,int v,int c)
{
    e[++ tot] = node(v,final[u],c),final[u] = tot;
    e[++ tot] = node(u,final[v],0),final[v] = tot;
// 2 3 
    //这里保证了正,反向弧的标号连续,那么对于第i条边,其反向弧就是i^1
}

bool bfs()
{
    //用bfs找到每个点的距离标号
    static int que[maxn];
    for(int i = s;i <= t;i ++) d[i] = -1,cur[i] = final[i];
    d[s] = 0;
    que[1] = s;
    for(int fi = 1,en = 1;fi <= en;fi ++)
    {
        int u = que[fi];
        for(int i = final[u];i;i = e[i].next)
            if (e[i].flow > 0 && d[e[i].to] == -1)
            {
                d[e[i].to] = d[u] + 1;
                que[++ en] = e[i].to;
            }
    }
    return d[t] != -1;
}

int dfs(int now,int flow)
{
    if (now == t) return flow;//流完,退出
    int use = 0;
    for(int i = cur[now];i;i = e[i].next)
    {
                cur[now] = i;
        if (e[i].flow > 0 && d[e[i].to] == d[now] + 1/*只能沿着最短路走*/)
        {
            int tmp = dfs(e[i].to,min(e[i].flow,flow - use));
            use += tmp,e[i].flow -= tmp,e[i ^ 1].flow += tmp;
            if (flow == use) return use;
        }
    }
    return use;
}

int main()
{
    tot = 1;//计算反向弧时更方便
    scanf("%d%d", &m, &n);//n为点数,m为边数
    s = 1,t = n;//假定原点为s,汇点为t
    for(int i = 1;i <= m;i ++)
    {
        int u,v,c;
        scanf("%d%d%d", &u, &v, &c);//一条从u到v,流量为c的边
        link(u,v,c);
    }
    int ans = 0;
    for(;bfs()/*假如找不到证明无法增广*/;)
        ans += dfs(s,1 << 30);
    printf("%d\n", ans);
    return 0;
}

SAP算法

SAP算法,其实就是在找增广路的时候给每个点记录一个高度标号,每次增广只走两边的点的高度相差为1的边(即满足条件 level(v)=level(u)+1 的边),且每次流完都用该点能走到的点的高度的最大值来更新该点的高度标号。该算法的理论时间复杂度是 O(n2m) 的,但在实践中,加了优化后,时间复杂度远远低于理论值,而且代码很短,是最实用的最大流算法之一。

SAP+GAP优化

根据SAP算法的特点,我们注意到,当某次增广时,最大流可能已经求出,因此算法做了很多无用功。这启示我们若高度标号之间存在间隙,就说明不可能再有增广路,算法就可以提前终止。这就是GAP优化。事实证明,GAP优化使程序的效率提高了不少。

这是一个十分优秀的算法。

SAP+GAP优化+当前弧优化

当前弧优化,就是指在增广时给每个点记录一条当前弧。然后,每次从当前弧开始增广,每找到一条可行弧就把它设为该点的当前弧。当前弧优化的作用也是十分显著的(尤其是稠密图)

现在给出SAP+GAP+当前弧优化的标程(以usaco草地排水为例):

#include
#include
#include

using namespace std;

const int N=1010,M=500010;

int to[2*M],next[2*M],fir[N],las[N],c[M*2],n,m,top=1,ans=0,h[N],vh[N];

void Con(int,int);
int dg(int,int);

int main()
{
    scanf("%d%d",&m,&n);
    for(int i=1;i<=m;i++)
    {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        Con(x,y);
        c[top]+=z;
        Con(y,x);
    }
    vh[0]=n;
    while(h[1]<=n)ans+=dg(1,int(1e9));
    printf("%d",ans);
}

int dg(int w,int v)
{
    if(w==n)return v;
    int Get=n+1;
    for(int i=fir[w],j=las[w];i;j=i,i=next[i])
    {
        int x=to[i];
        if(c[i])
        {
            if(h[w]==h[x]+1)
            {
                int f=dg(x,min(v,c[i]));
                if(f>0)
                {
                    c[i]-=f;
                    c[i^1]+=f;
                    //当前弧
                    next[las[w]]=fir[w];
                    next[j]=0;
                    fir[w]=i,las[w]=j;
                    //
                    return f;
                }
                if(h[1]>n)return 0;
            }
            Get=min(Get,h[x]+1);
        }
    }
    if(--vh[h[w]]==0)h[1]=n+1;
    h[w]=Get;
    vh[h[w]]++;
    return 0;
}

void Con(int x,int y)
{
    to[++top]=y;
    next[top]=0;
    if(fir[x])next[las[x]]=top;else fir[x]=top;
    las[x]=top;
}

还有,我们将边用前向星存起来,这个边的位置用另一个数组表示能过的流量,也就是说,u[i]到v[i]这条边的流量存入c[i],而不是c[u[i]][v[i]],它的反向边的流量为c[i^1],

由于我们讨论的时间复杂度属于最坏情况,所以实际所需时间远远不及 O(mn2) 甚至1000个点的图在1s内能跑过去

最小割

概念

首先,我们定义,对于一个图,我们将一些边删掉,使得原图( G(E,V) )的点( E )分成两个集合 STST=EST=sS,tT ,那么称(S,T)构成网络G(E,V)的割,定义割边
CUT(S,T){l(x,y)VxS,yTxT,yS)}
正向割边 CUT+(S,T){l(x,y)VxS,yT}
反向割边 CUT(S,T){l(x,y)VxT,yS}
割的容量,即为正向割边的容量和 cut(S,T)=xS,yTc(x,y)
正向流量 flow+(S,T)=l(x,y)CUT+(S,T)f(x,y)
反向流量 flow(S,T)=l(x,y)CUT(S,T)f(x,y)
网络流——最大流与最小割_第6张图片

那么我们定义最小割

minCUT(S,T)cut(S,T)=minCUT(S,T) xS,yTc(x,y)

最大流与最小割

定理:最大流=最小割

由于本蒟蒻学识并不高,所以只能给出一个比较感性的证明:

证明1:
=     =

证明2:
我们要使由原点至汇点的路径断开,那么最小为此路径的流;
此时再找另一条路径的割边,有两种情况:
1. 割边不在两条路径的相同部分,即为流
2. 割边在两条路径的相同部分,那么两条路径割边相同,即为两条路径最大流

所以最小割=最大流

好,我们现在就先上几道题:
①最大权闭合子图
首先对于一个图G(E,V), Ci 表示编号为i的点的权
我们定义一个集合 S{i|iE,jE,(i,j)VjS} 为图G的一个闭合子图
闭合子图的权 f(S)=iSCi
网络流——最大流与最小割_第7张图片
那么要你求一个图的最大权闭合子图的权

这道题可以用最小割模型:
1. 我们把源点连向权为正数的点,边权为这个点的点权,割掉表示选的闭合子图没有这个点
2. 我们把权为负数的点连向汇点,边权为点权的相反数,割掉表示选的闭合子图有这个点
3. 把原图的边权赋为Max,表示这些边不能割
跑一边最大流,将总正点权减去答案即可
网络流——最大流与最小割_第8张图片
为什么是正确的:
1. 定义的正确性:首先我们想到最大的权,如果不要这个正数点或要一个负数点就相当于割掉一条边,要使权最大那么久要使割最小,再用所有正权点权和减它,符合最小割定义
2. 答案的正确性:我们规定割掉连向源点的边表示不要这个点,那么割的流量会加上这个值,答案相应减少这个值;由于答案之前没有计入负点权,所以我们当选择一个负点时,答案应在原来基础上减去它的相反数,正好割的流量多了它的相反数。
3. 选取子集的正确性:我们求了答案,那么怎么保证符合闭合子图的定义呢?根据构图来说,只要源点和汇点连通,那么所选取的子集一定不是闭合子图;只要源点汇点不连通,那么选取的一定是闭合子图。感性证明:如果从一个正数X,走到另一个正数Y,如果源点连向X但不连向Y,很显然取X和Y会更优,自然排除只选X不选Y;若存在一条从源到汇的路径,由源点走向X,Y走向汇点,那么所选的子集中含X但不含有Y,也就说明闭合子图肯定有一节点连向非集合的节点,不符;若有X连向汇点但Y不连汇点,Y连向X也就是说闭合子图存在Y不存在X,如果Y与源点连通,则源点与汇点连通,不符。证毕。

②狼和羊的故事

你可能感兴趣的:(网络流,DFS,BFS)