网络流(1)-------最大流

一.网络流最大流问题和基本概念

1.网络流基本概念

(1)名词解释

源点:流量的源头,只有流出去的点
汇点:流量的汇聚点,只有流进来的点
流量:一条边上流过的实际流量
容量:一条边上可供流过的最大流量
残量:一条边上的容量-当前流量,剩下可流的最大流

(2)网络流概念

在一个有向图上选择一个源点,一个汇点,每一条边上都有一个流量上限(以下称为容量),即经过这条边的流量不能超过这个上界,同时,除源点和汇点外,所有点的入流和出流都相等,而源点只有流出的流,汇点只有汇入的流。这样的图叫做网络流

(3)网络流最大流问题

形象一点 : 现在有一个自来水厂要往你家通水,自来水厂到你们家连接了很多水管,并且中途经过很多转接点。每根水管都有粗细,通过管子的水流肯定不能太大,不然会撑爆管子。现在问你,从自来水厂放水最多到你家能汇聚有多少水。这就是网络流的最大流问题。给出你源点与汇点,给你m条有向路和每条路的容量,求最大流。

2.网络流的性质和定理

(1)网络流的三大性质

对于任意一个时刻,设f(u,v)为实际流量,c(u,v)为容量,则任意时刻流网络满足三个性质:

<1>容量限制:对于任意u,v,都有f(u,v) <= c(u,v)。即任意时刻流量都不超过容量。

<2>流守恒性:除了源点与汇点之外,其余任意一点,都满足流入该点总量 = 流出该点总量,节点自身不会产生或者消耗流量,只是中转媒介。

<3>反对称性:对于任意的u,v ,一定有f(u,v) = -f(v,u) 。 从u到v流了 l 流量就相当于从v到u流了-l 的流量

(2)最大流最小割定理

这里的割定义为边割集中所有边的容量之和(什么是边割集请自行百度QAQ)

<1>任意一个流都小于任意一个割

这是肯定的,任意一个边割集内的边被删掉以后,都会使图不联通,使流断掉。所以任意一个流肯定都小于任意一个割。

<2>构造出的一个流等于一个割

<3>最大流等于最小割

二.基于增广路定理思想的算法

1.概念

(1)增广路定理:网络达到最大流当且仅当残留网络中没有增广路。

(2)增广路算法思想(Ford-Fulkerson)

沿着图中的一个可行流开始,从源点往汇点不断寻找增广路,每找到一条就处理一次。不断重复寻找直到图中不存在增广路,这就是增广路算法。

(3)算法流程

 <1>我们找一条从源点可以通向汇点的通路,通路上的最小可行流就是路上所有水管都能承受的流量,我们就可以通上这些水。并且把所有路的流量加上这些,表示该水管已经通了这些水。

<2>不断继续寻找下一个可行的增广路,直到图中找不到从源点到汇点的可行路。但是这时不一定是最大流,因为我们刚开始选择很单一。所以我们这里就增加反向弧,从u到v通l 就 从v到u通l , 为什么这样做呢?

给流量一个反悔的机会,比如我本来从u到v,但是我发现从u分流一部分到v,另一部分走另一条路时最大,这里就有了反悔的选择。

2.朴素算法  - 一般增广路算法(EdmondsKarp)

(1)分析:由BFS不断寻找增广路,m条边,n个点

(2)代码:时间复杂度 O(m^2n)

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
const int maxn = 10000 + 7;
int head[maxn],n,m,tot,s,t,cost[maxn],pre[maxn];//cost记录到当前点的最小流量,pre记录前导边
struct Edge{
    int from,to,next,cap,flow;//cap容量,flow当前流量
}edge[maxn];
void addEdge(int a,int b,int c){
     edge[tot].from = a;edge[tot].to = b;edge[tot].next = head[a];edge[tot].cap = c;edge[tot].flow = 0;head[a] = tot++;
}
int EK(int st,int et){
    int maxflow = 0;
    for(;;){//不断寻找增广路
        memset(cost,0,sizeof(cost));
        queue<int> que;
        que.push(st);//从源点开始
        cost[st] = INF;
        while(!que.empty()){
            int u = que.front();
            que.pop();
            for(int i = head[u];~i;i = edge[i].next){
                int p = edge[i].to;
                if(!cost[p]&&edge[i].cap > edge[i].flow){//该点还未被走过 + 该水管残量不为0
                    cost[p] = min(cost[u],edge[i].cap - edge[i].flow);//取最小
                    pre[p] = i;//记录前导边
                    que.push(p);
                }
            }
            if(cost[et])break;//如果到达了终点,结束
        }
        if(!cost[et])break;//如果循环完发现没有到达汇点,说明不存在增广路了达到最大流
        for(int i = et;i!=st;i = edge[pre[i]].from){//从汇点往前更新所有边
            edge[pre[i]].flow+=cost[et];
            edge[pre[i]^1].flow-=cost[et];//反向弧
        }
        maxflow+=cost[et];//更新最大流
    }
    return maxflow;
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--){
        tot = 0;
        memset(head,-1,sizeof(head));
        scanf("%d%d%d%d",&n,&m,&s,&t);//n个点,m条有向边,源点,汇点
        for(int i = 0;i<m;i++){
            int a,b,v;
            scanf("%d%d%d",&a,&b,&v);
            addEdge(a,b,v);//正向建边
            addEdge(b,a,0);//反向弧初始容量为0
        }
        int ans = EK(s,t);//求解最大流
        printf("%d\n",ans);
    }
    return 0;
}

(3)EK算法的劣势

   EK算法的低效过程

3.分层图思想---Dinic算法

(1)为了解决上面的低效过程,我们给每个点标上一个记号来分层,而这个记号就是该点到源点的距离。我们规定每次前进必须走记号比当前点大一的点。这样就保证了每次移动必定前进,是必定距离汇点越来越近的。

(2)流程

<1> 利用 BFS 对原来的图进行分层,即对每个结点进行标号, 这个标号的含义是当前结点距离源点的最短距离(假设每条边的距离都为1),注意:构建层次图的时候所走的边的残余流量必须大于0
<2>用 DFS 寻找一条从源点到汇点的增广路, 注意: 此处寻找增广路的时候要按照层次图的顺序, 即如果将边(u, v)纳入这条增广路的话必须满足dis[u]=dis[v]−1, 其中 dis[i]为结点 i 的编号。找到一条路后要根据这条增广路径上的所有边的残余流量的最小值l更新所有边的残余流量(即正向弧 - l, 反向弧 + l).
<3>重复步骤 2, 当找不到一条增广路的时候, 重复步骤 1, 重新建立层次图, 直到从源点不能到达汇点为止。

(3)代码:复杂度 O(n^2m)

#include <iostream>
#include<bits/stdc++.h>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
#define INF 0x3f3f3f3f
const int maxn = 1000 + 7;
typedef long long LL;
struct Edge{
    int to,next;
    LL cap,flow;
}edge[maxn*20];
int n,m,head[maxn],tot,dist[maxn],s,t;
void addEdge(int a,int b,LL c){
      edge[tot].to = b;edge[tot].next = head[a];edge[tot].cap = c;edge[tot].flow = 0;head[a] = tot++;
      edge[tot].to = a;edge[tot].next = head[b];edge[tot].cap = 0;edge[tot].flow = 0;head[b] = tot++;
}
bool BFS(int st,int et){//BFS分层
     queue<int> que;
     memset(dist,0,sizeof(dist));
     que.push(st);
     dist[st] = 1;
     while(!que.empty()){
        int u = que.front();
        que.pop();
        for(int i = head[u];~i;i = edge[i].next){
             if(!dist[edge[i].to]&&edge[i].cap > edge[i].flow){//没走过这点 + 残量不为0
                dist[edge[i].to] = dist[u] + 1;
                que.push(edge[i].to);
             }
        }
     }
     if(dist[et]>0)return true;//若汇点有标号说明仍有增广路,否则结束
     return false;
}
LL DFS(int p,LL minFlow){//DFS找增广路
     if(p==t||minFlow==0)return minFlow;//到达汇点或者最小分到0了,结束反回
     LL flow = 0;//记录从该点分支的所有能走的增广路流量
     for(int i = head[p];~i;i = edge[i].next){
          if(dist[edge[i].to] == dist[p] + 1&&edge[i].cap > edge[i].flow){//残量不为0
              LL f = DFS(edge[i].to,min(edge[i].cap - edge[i].flow,minFlow));//沿这条路走下去的最小增广路流量
              edge[i].flow+=f;//更新到当前边
              edge[i^1].flow-=f;//反向弧
              flow+=f;
              minFlow-=f;//分流
              if(minFlow==0)break;
          }
     }
     return flow;
}
int Dinic(int st,int et){
    if(st==et)return 0;
    LL maxFlow = 0;
    while(BFS(st,et)){//不断分层
        LL res = DFS(st,INF);//DFS寻找当前分层下所有增广路
        maxFlow+=res;
    }
    return maxFlow;
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--){
        tot = 0;
        memset(head,-1,sizeof(head));
        scanf("%d%d%d%d",&n,&m,&s,&t);
        for(int i = 0;i<m;i++){
            int a,b;
            LL val;
            scanf("%d%d%lld",&a,&b,&val);
            addEdge(a,b,val);
        }
        LL ans = Dinic(s,t);
        printf("%lld\n",ans);
    }
    return 0;
}

(4)Dinic算法的当前弧优化

标记每个点遍历到哪条边了,下次遍历这个点的时候直接从这条边开始,避免了又从头开始的弊端。

代码:

#include <iostream>
#include<bits/stdc++.h>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
#define INF 0x3f3f3f3f
const int maxn = 1000 + 7;
typedef long long LL;
struct Edge{
    int to,next;
    LL cap,flow;
}edge[maxn*20];
int n,m,head[maxn],tot,dist[maxn],s,t,cur[maxn];
void addEdge(int a,int b,LL c){
      edge[tot].to = b;edge[tot].next = head[a];edge[tot].cap = c;edge[tot].flow = 0;head[a] = tot++;
      edge[tot].to = a;edge[tot].next = head[b];edge[tot].cap = 0;edge[tot].flow = 0;head[b] = tot++;
}
bool BFS(int st,int et){
     queue<int> que;
     memset(dist,0,sizeof(dist));
     que.push(st);
     dist[st] = 1;
     while(!que.empty()){
        int u = que.front();
        que.pop();
        for(int i = head[u];~i;i = edge[i].next){
             if(!dist[edge[i].to]&&edge[i].cap > edge[i].flow){
                dist[edge[i].to] = dist[u] + 1;
                que.push(edge[i].to);
             }
        }
     }
     if(dist[et]>0)return true;
     return false;
}
LL DFS(int p,LL minFlow){
     if(p==t||minFlow==0)return minFlow;
     LL flow = 0;
     for(int &i = cur[p];~i;i = edge[i].next){//!!注意这里的& , 使cur[p]跟随i变化,下次dfs到该点直接从上次退出的地方继续开始,不用从头重复了!!!
          if(dist[edge[i].to] == dist[p] + 1&&edge[i].cap > edge[i].flow){
              LL f = DFS(edge[i].to,min(edge[i].cap - edge[i].flow,minFlow));
              edge[i].flow+=f;
              edge[i^1].flow-=f;
              flow+=f;
              minFlow-=f;
              if(minFlow==0)break;
          }
     }
     return flow;
}
int Dinic(int st,int et){
    if(st==et)return 0;
    LL maxFlow = 0;
    while(BFS(st,et)){
        for(int i = 1;i<=n;i++)cur[i] = head[i];//初始化标记数组
        LL res = DFS(st,INF);
        maxFlow+=res;
    }
    return maxFlow;
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--){
        tot = 0;
        memset(head,-1,sizeof(head));
        scanf("%d%d%d%d",&n,&m,&s,&t);
        for(int i = 0;i<m;i++){
            int a,b;
            LL val;
            scanf("%d%d%lld",&a,&b,&val);
            addEdge(a,b,val);
        }
        LL ans = Dinic(s,t);
        printf("%lld\n",ans);
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_40772692/article/details/83032135