zoukankan      html  css  js  c++  java
  • 网络流

    网络流

    网络流的概念

        在一个有向图上选择一个源点,一个汇点,每一条边上都有一个流量上限(以下称为容量),

       即经过这条边的流量不能超过这个上界,同时,除源点和汇点外,所有点的入流和出流都相等

       而源点只有流出的流,汇点只有汇入的流。这样的图叫做网络流

    网络流的相关定义

    •   源点:有n个点,有m条有向边,有一个点很特殊,只出不进,叫做源点

    •   汇点:另一个点也很特殊,只进不出,叫做汇点

    •   容量和流量:每条有向边上有两个量,容量和流量,从i到j的容量通常用c[i,j]表示,流量则通常是f[i,j].

      通常可以把这些边想象成道路,流量就是这条道路的车流量,容量就是道路可承受的最大的车流量

      很显然的,流量<=容量。而对于每个不是源点和汇点的点来说,可以类比的想象成没有存储功能的货物的中转站,

      所有“进入”他们的流量和等于所有从他本身“出去”的流量。

    •   最大流:把源点比作工厂的话,问题就是求从工厂最大可以发出多少货物,是不至于超过道路的容量限制,也就是,最大流

      Ford-Fulkerson 增广路算法

         该方法通过寻找增广路来更新最大流,有 EK,dinic,SAP,ISAP 主流算法。

          求解最大流之前,我们先认识一些概念。

       最常用的就是dinic(据说隔壁treaker只会这一种)。

       但是费用流需要用到EK算法,so,要学会EK算法,dinic;

       增广路:在图中若一条从源点到汇点的路径上所有边的 剩余容量都大于 0 (注意是大于不是大于等于),这条路被称为增广路。

       

       EK算法O(nm2)

       求解思路:

       从图中找一条增广路,然后增广,怎么找?

       1.从源点开始bfs,找到到汇点的一条路径,并记录这条路径上所有边剩余流量最小值,因为要找增广

       路,所以我们在bfs时要判断一下边的剩余容量是否为0,记得用一个pre数组记录下路径。

       2.找到路径后,对其进行增广(代码里的up函数),增广就是把这条路径的每条边都减去这

       些边中剩余流量的最小值(bfs时记录),反向边加上这个最小值(关于方向边下面再解释)。

       3.一直找增广路增广,直到不能增广为止(找不到增广路)。

       可以看下面这张图。

       感谢SYCstudio的图

       感谢SYCstudio的图(本人较菜不会画)。

       上面我们提到了反向边,下面我们解释下为什么要建反向边。(放图,简单图我还是可以的现学的

       

       像我们上面这张图,因为我们bfs时不能确定第一次走哪条边,要是你像bmf一样运气不好,

       如果bfs第一次找到的增广路是1→3→2→1的话,我们最后求得的最大流就是1.

       但是很明显这张图最大流是2,所以我们发现第一次找的增广路不是最优的,这时候你就凉了。

       那我们怎么解决呢反向边,反向边其实就是一个反悔的机会,也就是让流过的流量流回去。

       如果还不明白的话还学什么网络流,下面模拟一下这个过程。

       先说一下反向边的一些东西,反向边初始化为0,当正向边剩余流量减少的是时候,

       反向边流量增加同样的值,因为我们可以反悔的流量等于已经从这条边正向流过的流量。

       下面看一下我们是如何通过反向边反悔的又要做图qaq

       因为我不会画反向边,所以我们假设‘1/0’左边那个数字表示正向变边权,右边是反向边。

       下面这张图就是建完边后的图。

       

       如果我们第一次找到的增广路是1→3→2→1的话,总流量+1=1,图就变成了

       

       我们发现我们还可以找到增广路1→2→3→4,总流量+1=2,图变成

       

       然后发现,找不到增广路了,程序结束不用作图了,我们发现

       再找增广路的过程中3→2这条边正向一次,反向一次,相当于流量为0.

       这就是反向边的作用。

       另外提供一种小技巧,使用邻接表建图的话,可以边的编号从2开始建,我们知道

       2^1=3,3^1=2……

       这样的话我们可以通过异或得到反向边的编号(记得建完正向边,紧接着就建反向边),具体看代码

       时间复杂度为O(nm2)至于为什么,本人很菜不会,另外,一般时间复杂度是远远达不到这个值的。

       代码(EK)

       

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<queue>
    #define LL long long
    using namespace std;
    const int inf=1<<29;
    const int N=207;
    const int M=5e3+7;
    int n,m,s,t,cnt=1;//从编号2开始建边 
    LL maxf;//最大流 
    int head[N],vis[N],pre[N];
    LL incf[N];
    LL v[N][N]; 
    struct edge{
        int v,nxt;
        LL w;
    }e[M<<1];//因为要建反向边,所以开二倍空间 
    void add_edge(int u,int v,LL w){//存边 
        cnt++;
        e[cnt].v=v;
        e[cnt].w=w;
        e[cnt].nxt=head[u];
        head[u]=cnt;
    }
    bool bfs(){
        memset(vis,0,sizeof(vis));
        queue<int>q;
        q.push(s);
        vis[s]=1;
        incf[s]=inf;
        while(q.size()){
            int now=q.front();q.pop();
            for(int i=head[now];i;i=e[i].nxt){
                if(e[i].w==0||vis[e[i].v])continue;
                int to=e[i].v;
                incf[to]=min(incf[now],e[i].w);//记录路径最小边的流量 
                pre[to]=i;//记录路径边的编号 
                q.push(to);
                vis[to]=1;
                if(to==t)return 1;
            }
        }
        return 0;
    }
    void up(){
        int x=t;
        while(x!=s){
            int i=pre[x];
             e[i].w-=incf[t];
             e[i^1].w+=incf[t];//反向边加上正向边减少的流量 
             x=e[i^1].v;
        }
        maxf+=incf[t];
    } 
    inline int read()
    {
        int x = 0 , f = 1;  char ch = getchar();
        while(ch < '0' || ch > '9') {if(ch == '-')  f = -1; ch = getchar();}
        while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
        return x * f;
    }
    int main(){
        n = read(); m = read(); s = read(); t = read();
        for(int i=1;i<=m;i++){
            int x,y,z;
            x = read(); y = read(); z = read();
            v[x][y] += z;
        }
        for(int i = 1;i <= n;i ++) for(int j = 1;j <= n;j ++) if(v[i][j]) add_edge(i,j,v[i][j]),add_edge(j,i,0);
        while(bfs())up();
        cout<<maxf<<endl;
    }//可能跟你们码风不同,╮(╯▽╰)╭

      Dinic

       会了EK还学dinic有什么用呢,有用,我们来分析下下面这张图不小心把颜色改了下,懒得再做一张

       

       

       如果你运气不好的话像bmf一样,若你每次找到的增广路都经过了2→3或3→2这条边的话你又凉了

       所以这时候就用到了我们的Dinic算法。

       Dinic 算法 的过程是这样的:每次增广前,我们先用 BFS 来将图分层。设源点的层数为1

       那么一个点的层数便是它离源点的最近距离。

       层次用数组dep表示。分层图对于每条边满足dep[v]=dep[u]+1。

       我们思考,EK算法每轮可能会遍历整个图,但只找出一条增广路,属于单路增广。

       而Dinic属于多路增广,时间复杂度O(n2m)

       求解思路:

       1.bfs求出节点的层次,构建分层图

       2.对于求出的分层图,用dfs进行多路增广,由于本人菜,讲的不是很明白,我们可以看代码

       3.当前弧优化 :cur[]数组的应用,如果一条边已经被增广过,那么它就没有可能被增广第二次。那么,我们下一次进行增广的时候,

       就可以不必再走那些已经被增广过的边。

       

    #include<bits/stdc++.h>
    #define LL long long
    const int N=210;
    const int M=5e3+7;
    using namespace std;
    int n,m,s,t,cnt=1;
    LL max_flow;
    int dep[N],head[N],cur[N];
    struct edge{
        int v,w,nxt;
    }e[M<<1];//数组含义与上一篇EK含义一样,cur[]数组是dinic的一个优化,下面会提到 
    void add_edge(int u,int v,int w){
        cnt++;
        e[cnt].v=v;
        e[cnt].w=w;
        e[cnt].nxt=head[u];
        head[u]=cnt;
    }
    
    int bfs(){//构建分层图 
        for(int i=1;i<=n;i++) dep[i]=0;//每个节点层次初始化 
        queue<int>q;
        q.push(s);
        dep[s]=1;//源点初始化为1; 
        while(q.size()){
            int now=q.front();
            q.pop();
            for(int i=head[now];i;i=e[i].nxt){
                int to=e[i].v,val=e[i].w;
                if(val&&(!dep[to])){//构建分层图的时候要保证边不为0,如果dep[]已经被更新就不用更新了 
                    q.push(to);
                    dep[to]=dep[now]+1;
                    if(to==t)return 1;//如果到达汇点,进行dfs 
                }
            }
        }
        return 0;
    }
    
    int dfs(int u,int flow){
        if(u==t)return flow;
        int rest=flow,k;//rest表示当前这个节点最大允许通过流量 
        for(int i=cur[u];i&&rest;i=e[i].nxt){ 
            cur[u]=i;//当前弧优化
            int to=e[i].v,val=e[i].w;
            if(val&&(dep[to]==dep[u]+1)){
                int k=dfs(to,min(rest,val));//寻找增广路 
                if(!k)dep[to]=0;//如果在to之后的路径找不到增广路,踢出分层图 
                e[i].w-=k;//回溯时更新边权 
                e[i^1].w+=k;
                rest-=k;//当前节点最大允许通过流量减去这次通过的流量 
            }
        }
        return flow-rest; 
    }
    
    int main(){
        scanf("%d%d%d%d",&n,&m,&s,&t);
        for(int i=1;i<=m;i++){
            int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            add_edge(x,y,z);
            add_edge(y,x,0);
        }
        int flow=0;
        while(bfs()){
            for(int i=1;i<=n;i++){
                cur[i]=head[i]; //当前弧初始化 
            }
            while(flow=dfs(s,1<<29))max_flow+=flow;
        }
        cout<<max_flow<<"
    ";
    }

     最小割

       最小割问题是指:给出一种有向图(无向图)和两个点s,t以及图中的边的边权,

       求一个权值和最小的边集,使得删除这些边之后是s,t不连通。这类问题,一般运用最

       大流等于最小流定理,求出最大流来解决。证明自行百度百科

       附上代码

       

    int bfs(){
        for(int i=1;i<=2*n;i++)dep[i]=0;
        queue<int>q;
        q.push(s);
        dep[s]=1;
        while(q.size()){
            int now=q.front();
            q.pop();
            for(int i=head[now];i;i=e[i].nxt){
                int to=e[i].v,val=e[i].w;
                if(val&&!dep[to]){
                    q.push(to);
                    dep[to]=dep[now]+1;
                    if(to==t)return 1;
                }
            }
        }
        return 0;
    }
    int dfs(int u,int flow){
        if(u==t)return flow;
        int rest=flow,k;
        for(int i=cur[u];i;i=e[i].nxt){
            cur[u]=i;
            int to=e[i].v,val=e[i].w;
            if(val&&(dep[to]==dep[u]+1)){
                k=dfs(to,min(val,rest));
                if(!k)dep[to]=0;
                e[i].w-=k;
                e[i^1].w+=k;
                rest-=k;
            }
        }
        return flow-rest;
    }

    费用流

       费用流就是每条边除了有容量限制外,还有一个给定的单位费用 w(x,y),

       当流过(x,y)这条边时,要花费 f(x,y)*w(x,y)的费用

       一般求解的问题是最小费用最大流最大费用最大流,

       基于之前提到的Ek算法,把bfs改成spfa即可,

       就是每次先增广费用最小的流。。。

       

    int spfa(){//最小费用最大流
        for(int i=0;i<=n;i++)dis[i]=inf,vis[i]=0;
        queue<int>q;
        q.push(s);
        dis[s]=0;
        vis[s]=1;
        incf[s]=inf;
        while(q.size()){
            int now=q.front();
            vis[now]=0;
            q.pop();
            for(int i=head[now];i;i=e[i].nxt){
                int to=e[i].v,val=e[i].w;
                if(!val)continue;
                if(dis[to]>dis[now]+e[i].f){
                    dis[to]=dis[now]+e[i].f;
                    incf[to]=min(incf[now],val);
                    pre[to]=i;
                    if(!vis[to]){
                        vis[to]=1;
                        q.push(to);
                    }
                }
            }
        }
        if(dis[t]==inf)return 0;
        return 1;
    }
    int up(){
        int x=t;
        max_flow+=incf[t];
        min_cost+=dis[t]*incf[t];
        while(x!=s){
            int i=pre[x];
            e[i].w-=incf[t];
            e[i^1].w+=incf[t];
            x=e[i^1].v;
        }
    }

       end......

       

       

  • 相关阅读:
    Kubernetes 集成研发笔记
    Rust 1.44.0 发布
    Rust 1.43.0 发布
    PAT 甲级 1108 Finding Average (20分)
    PAT 甲级 1107 Social Clusters (30分)(并查集)
    PAT 甲级 1106 Lowest Price in Supply Chain (25分) (bfs)
    PAT 甲级 1105 Spiral Matrix (25分)(螺旋矩阵,简单模拟)
    PAT 甲级 1104 Sum of Number Segments (20分)(有坑,int *int 可能会溢出)
    java 多线程 26 : 线程池
    OpenCV_Python —— (4)形态学操作
  • 原文地址:https://www.cnblogs.com/Aswert/p/13287851.html
Copyright © 2011-2022 走看看