网络流初步

一、简介:
   网络流是图论中一个博大精深的分支。若要详细的学习网络流的各种模型与应用,需要不少时间。本篇文章的学习希望我自己可以建立对网络流的初步认识,内容比较浅显,少部分算法和证明的细节会被省略。
   
   
   一个网络G=(V , E)是一张有向图,图中每条有向边(x,y)∈E都有一个给定的权值c(x,y),称为边的容量。特别的,若(x,y)不属于E,则c(x,y)=0。图中还有两个指定的特殊节点S∈V和T∈V(S≠T),分别称为源点和汇点。
   
   设f(x,y)是定义在节点二元组(x∈V,y∈V)上的实数函数,且满足
   (1) f(x,y) ≤ c(x,y)
   (2)f(x,y) = -f(y,x)
   (3)对于所有的 x ≠ S 且 x ≠ T ,f(u,x)的和,其中(u,x)∈E等于f(x,v)的和,其中(x,v)∈E。

    f 称为网络的流函数。对于(x,y)∈E,f(x,y)称为边的流量,c(x,y)-f(x,y)称为边的剩余容量。

   f(S,v)的和,其中(S,v)∈E称为整个网络的流量(其中S表示源点)。

   上文给出的流函数f(x,y)的三条性质分别称为容量限制、斜对称和流量守恒。尤其是流量守恒定律,它告诉我们网络中除源点、汇点以外,任何节点不存储流,其流入总量等于流出总量。网络流模型可以形象地描述为:在不超过容量限制的前提下,流 从源点源源不断地产生,流经整个网络,最终全部归于汇点。
   
   
   
   
二、最大流:
   对于一个给定的网络,合法的流函数 f 有很多。其中使得整个网络的流量f(S,v)的和,其中(S,v)∈E最大(其中S表示源点)的流函数被称为网络最大流,此时的流量被称为网络最大流量。
   
   最大流能解决许多实际的问题。就拿我们上两节讨论的二分图为例。对于一张n个顶点m条边的二分图,我们可以新增一个源点S和一个汇点T,从S到每个左部点连有向边,从每个右部点到T连有向边,把原二分图的每条边看作从左部点到右部点的有向边,形成一张n+2个点n+m条边的网络,网络中每条边的容量都为1。容易发现,二分图的最大匹配数就等于网络的最大流量,求出最大流后,所有有 流 经过的点、边,就是匹配点,匹配边。

   进一步地,若要求二分图多重匹配,则只需把S到左部点的有向边容量设为左部点的匹配数量上线 kli,右部点到T的有向边容量设为右部点的匹配数量上限 krj,原二分图每条边的容量设为1,再求最大流即可。
   
   计算最大流的算法有很多,本节简单涉及Edmonds–Karp增广路算法和Dinic算法。

网络流图里,源点流出的量等于汇点流入的量,除源点、汇点之外的任何点,其流入量之和等于流出量之和

   
   
三、Edmonds–Karp增广路算法:
   
   若一条从源点S到汇点T的路径上各条边的剩余容量都大于0,则称这条路径为一条增广路。显然,可以让一股流沿着增广路从S到T,使网络的流量增大。Edmonds-Karp算法的思想就是不断用BFS寻找增广路,直到网路上不存在增广路为止。

   在每轮寻找增广路的过程中,Edmonds-Karp算法只考虑图中所有f(x,y)<c(x,y)的边,用BFS找到任意一条从S到T的路径,同时计算出路径上各边的剩余容量的最小值minf,则网络的流量就可以增加minf。
   
   需要注意的是,当一条边的流量f(x,y)>0时,根据斜对称性质,它的反向边流量f(y,x)<0,此时必定有f(y,x)<c(y,x)。故Edmonds–Karp算法在BFS时除了原图的边集E外,还应该考虑遍历E中每条边的反向边。

   在具体实现中,我们可以按照邻接表成对存储的技巧,把网络的每条有向边及其反向边存在邻接表的下表为 2 和3 , 4 和5 ,6 和 7的位置上,每条边只记录剩余容量c—f即可。当一条边(x,y)流过大小为e的流时,令(x,y)的剩余容量减小e,(y,x)的剩余容量增大e。

   Edmonds–Karp增广路算法的时间复杂度为O(n * m * m)。然而在实际运用中则远远达不到这个上界,效率较高,一般能够处理 1000----10000规模的网络。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<string>
#include<queue>
#define ll long long
using namespace std;
const int _max=2010;
const int maxn=20010;
const int inf=1<<29;
int head[_max],ver[maxn],edge[maxn],nt[maxn];
int v[_max],incf[_max],pre[_max];
int n,m,s,t,tot,maxflow;

void add(int x,int y,int z)
{
    ver[++tot]=y,edge[tot]=z,nt[tot]=head[x],head[x]=tot;
    ver[++tot]=x,edge[tot]=0,nt[tot]=head[y],head[y]=tot;
}

bool bfs(void)
{
    memset(v,0,sizeof(v));
    queue<int>q;
    q.push(s);
    v[s]=1;
    incf[s]=inf;//增广路上各边的最小剩余容量

    while(q.size())
    {
        int x=q.front();
        q.pop();

        for(int i=head[x];i;i=nt[i])
        {
            if(edge[i])
            {
                int y=ver[i];
                if(v[y]) continue;
                incf[y]=min(incf[x],edge[i]);
                pre[y]=i;//记录前驱,便于找到最长路的实际方案
                q.push(y);
                v[y]=1;

                if(y==t) return true;
            }
        }
    }
    return false;
}

void update(void)//更新增广路及其反向边的剩余容量
{
    int x=t;
    while(x != s)
    {
        int i=pre[x];
        edge[i] -= incf[t];
        edge[i^1] += incf[t];
        x=ver[i^1];
    }
    maxflow+=incf[t];
}

int main(void)
{
    while(cin>>n>>m)
    {
        memset(head,0,sizeof(head));
        s=1,t=n;
        tot=1;
        maxflow=0;
        for(int i=1;i<=m;i++)
        {
            int x,y,c;
            scanf("%d%d%d",&x,&y,&c);
            add(x,y,c);
        }
        while(bfs())
            update();

        cout<<maxflow<<endl;
    }
    return 0;
}

   
   
   
四、Dinic算法:
   
   在任意时刻,网络中所有节点以及剩余容量大于0的边构成的子图被称为残量网络。Edmonds–Karp每轮可能会遍历整个残量网络,但只找出 1 条增广路,还有进一步优化的空间。
   
   宽度优先遍历时,我们就提到了节点的层次d(x),它表示S到x最少需要经过的边数。在残量网络中,满足d(y)=d(x)+ 1的边(x,y)构成的子图被称为分层图。分层图显然是一张有向无环图。
   
   Dinic算法不断重复以下步骤,直到在残量网络中S不能到达T:
   (1)在残量网络上BFS求出节点的层次,构造分层图。
   (2)在分层图上DFS寻找增广路,在回溯时实时更新剩余容量。另外,每个点可以流向多条出边,同时还加入了若干剪枝。

   
   Dinic算法的时间复杂度为O(n * n * m)。实际运用总远远达不到这个上界,可以说是比较容易实现的效率最高的网络流算法之一,一般能处理10000----100000规模的网络。特别的,Dinic算法求解二分图的最大匹配的时间复杂度为O(m sqrt(n)),实际则表现更快。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<string>
#include<queue>
#define ll long long
using namespace std;
const int _max=50010;
const int maxn=300010;
const int inf=1<<29;
int head[_max],ver[maxn],edge[maxn],nt[maxn];
int d[_max];
int n,m,s,t,tot,maxflow;
queue<int>q;


void add(int x,int y,int z)
{
    ver[++tot]=y,edge[tot]=z,nt[tot]=head[x],head[x]=tot;
    ver[++tot]=x,edge[tot]=0,nt[tot]=head[y],head[y]=tot;
}

bool bfs(void) //在残量网络上构造分层图
{
    memset(d,0,sizeof(d));
    while(q.size()) q.pop();

    q.push(s);
    d[s]=1;

    while(q.size())
    {
        int x=q.front();
        q.pop();

        for(int i=head[x];i;i=nt[i])
        {
            int y=ver[i];
            if(edge[i]&&!d[y])
            {
                q.push(y);
                d[y]=d[x]+1;
                if(y==t) return true;
            }
        }
    }
    return false;
}

int dinic(int x,int flow)//在当前分层图上增广
{
    if(x == t) return flow;
    int rest =flow ,k;

    for(int i=head[x];i&&rest;i=nt[i])
    {
        int y=ver[i];
        if(edge[i]&&d[y]==d[x]+1)
        {
            k=dinic(y,min(rest,edge[i]));
            if(!k) d[y]=0;//剪枝,去掉增广完毕的点

            edge[i] -= k;
            edge[i^1] += k;
            rest -= k;
        }
    }
    return flow - rest;
}

int main(void)
{
    cin>>n>>m;
    cin>>s>>t;
    tot=1;
    for(int i=1;i<=m;i++)
    {
        int x,y,c;
        scanf("%d%d%d",&x,&y,&c);
        add(x,y,c);
    }

    int flow=0;
    while(bfs())
        while(flow = dinic(s,inf))
            maxflow += flow;

    cout<<maxflow<<endl;

    return 0;
}

   
   
   
后来(现在)经lc大佬教导,又学了个当前弧优化的写法

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<string>
#include<queue>
#define ll long long
using namespace std;
const int maxx=50010;
const int maxn=300010;
const int inf=1<<29;
int head[maxx],ver[maxn],edge[maxn],nt[maxn],now[maxn];
int d[maxx];
int n,m,s,t,tot,maxflow;
queue<int>q;


void add(int x,int y,int z)
{
    ver[++tot]=y,edge[tot]=z,nt[tot]=head[x],head[x]=tot;
    ver[++tot]=x,edge[tot]=0,nt[tot]=head[y],head[y]=tot;
}

bool bfs(void)
{
    memset(d,0,sizeof(d));
    while(q.size()) q.pop();

    q.push(s);
    d[s]=1;
    now[s]=head[s];

    while(q.size())
    {
        int x=q.front();
        q.pop();

        for(int i=head[x];i;i=nt[i])
        {
            int y=ver[i];
            if(edge[i]&&!d[y])
            {
                q.push(y);
                now[y]=head[y];
                d[y]=d[x]+1;
                if(y==t) return true;
            }
        }
    }
    return false;
}

int dinic(int x,int flow)
{
    if(x==t) return flow;
    int rest=flow,k,i;

    for(i=now[x];i&&rest;i=nt[i])
    {
        int y=ver[i];
        if(edge[i]&&d[y]==d[x]+1)
        {
            k=dinic(y,min(rest,edge[i]));
            if(!k) d[y]=0;

            edge[i] -= k;
            edge[i^1] += k;
            rest -= k;
        }
    }
    now[x]=i;
    return flow - rest;
}

int get_maxx(void)
{
    int flow=0;
    while(bfs())
        while(flow = dinic(s,inf))
            maxflow += flow;
    return maxflow;
}

int main(void)
{
    cin>>n>>m;
    cin>>s>>t;
    tot=1;
    for(int i=1;i<=m;i++)
    {
        int x,y,c;
        scanf("%d%d%d",&x,&y,&c);
        add(x,y,c);
    }

    cout<<get_maxx()<<endl;

    return 0;
}

五、二分图最大匹配的必须边与可行边:

   给定一张二分图,其最大匹配的方案不一定是唯一的。若任何一个最大匹配方案的匹配边都包括(x,y),则称(x,y)为二分图最大匹配的必须边。若(x,y)至少属于一个最大匹配方案,则称(x,y)为二分图最大匹配的可行边。

   先来考虑一种简化情况——二分图的最大匹配是一组完备匹配。我们先任意求出一组完备匹配的方案,此时左、右部所有的节点都是匹配点。
   
   根据定义,(x,y)是必须边,当且仅当以下两个条件都满足:
   (1)(x,y)当前是匹配边。
   (2)删除边(x,y)后,不能找到另一条从x到y的增广路。

   (x,y)是可行边,当且仅当满足以下两个条件之一:
   (1)(x,y)当前是匹配边。
   (2)(x,y)是非匹配边,设当前x与v匹配,y与u匹配,连接边(x,y)后,节点u,v失去匹配。必须能找到一条从u到v的增广路。

   
   如果我们把二分图G中的非匹配边看作从左部到右部的有向边,把匹配边看作从右部到左部的有向边,构成一张新的有向图,记为GG,那么G中从x到y有增广路,等价于GG中存在x到y的路径。
   
   考虑必须边的条件,(x,y)是匹配边,对应GG中有一条从y到x的有向边。在此前提下,若x到y还存在一条路径,则x和y互相可达,必处于同一个强连通分量中。x和y在GG中相互可达,构成一个环,把环上的边的匹配状态取反,得到另一组完备匹配。

   因此,必须边的判定条件可改写为:
   ——》(x,y)当前是二分图G的匹配边,并且x,y两点在有向图GG中属于不同的强连通分量。
   类似地,可行边的判定条件可以改写为:
   ——》(x,y)当前是二分图G的匹配边,或者x,y两点在有向图GG中属于同一个强连通分量。
   
   
   
   若二分图的最大匹配不是一组完备匹配:
   
   上述完备匹配的第二个条件会出问题,设(x,y)是非匹配边:
   (1)x,y二者之一可能本来就是非匹配点。不妨设y是非匹配点,此时直接断开x原来的匹配边,连接(x,y),得到的匹配集仍然是最大匹配
   (2)即使当前x与v匹配,y与u匹配,连接边(x,y)后,节点u,v失去匹配,我们也不一定非要找到从u到v的增广路。因为从u出发找到到达另一个非匹配点z的增广路,同样可以得到一组包含(x,y)的最大匹配。

   所以我们不能像完备匹配中一样,直接用强连通分量进行判定。然而,我们可以借助网络流的源点和汇点。回想我们根据二分图G建立有向图GG的方法,GG其实就是用网络流计算二分图最大匹配时,残量网络中除去源点,汇点之外的部分——非匹配边(左部到右部)的剩余容量为1,匹配边的反向边(右部到左部)的剩余容量为1。
   
   若z当前是非匹配点,则(z,T)的剩余容量必定为1。若v当前是匹配点,则(v,T)的剩余容量必定为0,(T,v)的剩余容量必定为1。换言之,残量网络中存在路径z——T——v。若二分图中u到z有增广路,则残量网络上u能到达z,进而到达v。故(u,v)仍在同一个强连通分量中,他们借助汇点T实现了连通。
   
   综上所述,在一般的二分图中(最大匹配不一定是完备匹配),可以用最大流计算出任意一组最大匹配。此时
   必须边的判定条件:
   (x,y)流量为1,并且在残量网络上属于不同的强连通分量。
   可行边的判定条件:
   (x,y)流量为1,或者在残量网络上属于同一个强连通分量。

使用Dinic算法求最大流,Tarjan算法求强连通分量,最后逐一对边进行判定,整个算法的时间复杂度为O(E * sqrt(N + M))。
   
   
   
   
六、最小割:
   给定一个网络G=(V,E),源点为S,汇点为T。若一个边集EE含于E被删去之后,源点S和汇点T不再连通,则称该边集为网络的割。边的容量之和最小的割称为网络的最小割。

   最大流最小割定理:
   任何一个网络的最大流量等于最小割中边的容量之和,简记为最大流=最小割。
   假设最小割<最大流,那么割去这些边之后,因为网络流量尚未最大化,所以仍然可以找到一条S到T的增广路,与S,T不连通矛盾,故最小割≥最大流。如果我们能给出一个最小割=最大流的构造方案,即可得到上述定理。
   求出最大流后,从源点开始沿残量网络BFS,标记能够到达的点。E中所有连接 已标记点x 和 未标记点y 的边(x,y)构成该网络的最小割。

   
   
   
七、费用流:
   
   给定一个网络G=(V,E),每条边(x,y)除了有容量限制c(x,y),还有一个给定的单位费用w(x,y)。当边(x,y)的流量为f(x,y)时,就要花费f(x,y) * w(x,y)。该网络中总花费最小的最大流被称为 最小费用最大流,总花费最大的最大流被称为 最大费用最大流,二者合称为费用流模型。注意:费用流的前提是最大流,然后才考虑费用的最值。
   
   类似于二分图最大匹配与最大流的关系,二分图带权最大匹配可直接用最大费用最大流求解,每条边的权值就是它的单位费用。

   Edmonds—Karp增广路算法:
   在Edmonds–Karp求解最大流的基础上,把 用BFS寻找任意一条增广路 改为 用SPFA寻找一条单位费用之和最小的增广路 (也就是把w(x,y)当作边权,在残量网络上求最短路)即可求出最小费用最大流。注意:一条反向边(y,x)的费用应设为-w(x,y)。
   参考程序以下面的例题为背景给出:

   POJ 3422 K取方格数:

On an N × N chessboard with a non-negative number in each grid, Kaka starts his matrix travels with SUM = 0. For each travel, Kaka moves one rook from the left-upper grid to the right-bottom one, taking care that the rook moves only to the right or down. Kaka adds the number to SUM in each grid the rook visited, and replaces it with zero. It is not difficult to know the maximum SUM Kaka can obtain for his first travel. Now Kaka is wondering what is the maximum SUM he can obtain after his Kth travel. Note the SUM is accumulative during the K travels.

Input
The first line contains two integers N and K (1 ≤ N ≤ 50, 0 ≤ K ≤ 10) described above. The following N lines represents the matrix. You can assume the numbers in the matrix are no more than 1000.

Output
The maximum SUM Kaka can obtain after his Kth travel.

Sample Input
3 2
1 2 3
0 2 1
1 4 2
Sample Output
15

   在一个N * N的矩形网格中,每个格子里都写着一个整数。可以从左上角到右下角安排K条路线,每一步只能往下或往右,沿途经过的格子中整数会被取走。若多条路线重复经过一个格子,只取一次。求能取得的整数的和最大是多少。

   当k=1时,若把第 i 行,第 j 列的格子看作一个节点,从格子(i,j)对应的节点向格子(i,j+1),(i+1,j)对应的节点连有向边,则能取得的整数之和的最大值就相当于从(1,1)到(N,N)的最长路(只不过权值在节点上)。

   当K>1时,我们需要限制每个节点的权值只能被算一次。这种点(或边)同时带有权值和上界限制的最优化问题,恰好与费用流的模型非常相似。我们可以按照如下规则构造一个网络:
   (1)应用点边转化的技巧,把每个格子(i,j)对应的节点拆成一个入点和一个出点,整个网络共有2 * N *N个点。
   (2)从每个(i,j)入点向出点连两条有向边。第一条有向边容量为1,费用为格子(i,j)中的数。第二条有向边容量为k-1,费用为0。这表示第一次经过该点时,可以把数取走,之后再经过时就不再计算。
   (3)从(i,j)的出点到(i,j+1)和(i+1,j) 的入点连有向边,容量为K,费用为0.

   以(1,1)的入点为源点,(N,N)的出点为汇点,求最大费用流。
因为(1,1)的入点和出点之间的两条有向边一共只有K的容量,并且费用流先保证最大流,再最优化费用,所以最终会恰好找到K条路线。求出的总费用就是本题的答案。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<string>
#include<queue>
#define ll long long
using namespace std;
const int _max=5010;
const int maxn=200010;
int head[_max],ver[maxn],edge[maxn],cost[maxn],nt[maxn];
int d[_max],incf[_max],pre[_max],v[_max];

int n,k,tot,s,t,maxflow,ans;

void add(int x,int y,int z,int c)
{
    //正向边,初始容量为z,单位费用为c
    ver[++tot]=y,edge[tot]=z,cost[tot]=c;
    nt[tot]=head[x],head[x]=tot;

    //反向边,初始容量0,单位费用-c,与正向边成对存储
    ver[++tot]=x,edge[tot]=0,cost[tot]=-c;
    nt[tot]=head[y],head[y]=tot;
}

int num(int i,int j,int k)
{
    return (i-1)*n+j+k*n*n;
}

bool spfa(void)
{
    queue<int>q;
    memset(d,0xcf,sizeof(d));//-INF
    memset(v,0,sizeof(v));

    q.push(s);//SPFA求最长路
    d[s]=0;
    v[s]=1;

    incf[s]=1<<30;//增广路各边最小容量
    while(q.size())
    {
        int x=q.front();
        v[x]=0;
        q.pop();
        for(int i=head[x];i;i=nt[i])
        {
            if(!edge[i]) continue;//剩余容量为0,不在残量网络中,不遍历

            int y=ver[i];//记录前驱,便于找到最长路的实际方案
            if(d[y]<d[x]+cost[i])
            {
                d[y]=d[x]+cost[i];
                incf[y]=min(incf[x],edge[i]);
                pre[y]=i;
                if(!v[y]) v[y]=1,q.push(y);
            }
        }
    }
    if(d[t]==0xcfcfcfcf) return false;//汇点不可达,已求出最大流
    return true;
}


void update(void)
{
    int x=t;
    while(x!=s)
    {
        int i=pre[x];
        edge[i] -= incf[t];
        edge[i^1] += incf[t];
        x=ver[i^1];
    }
    maxflow+=incf[t];
    ans+=d[t]*incf[t];
}

int main(void)
{

    while(scanf("%d%d",&n,&k)!=EOF)
    {
        s=1;
        t=2*n*n;
        tot=1;
        memset(head,0,sizeof(head));

        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            {
                int c;
                scanf("%d",&c);
                add(num(i,j,0),num(i,j,1),1,c);
                add(num(i,j,0),num(i,j,1),k-1,0);
                if(j<n) add(num(i,j,1),num(i,j+1,0),k,0);
                if(i<n) add(num(i,j,1),num(i+1,j,0),k,0);

            }
        }
        while(spfa()) update();
        printf("%d\n",ans);

    }

    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值