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

    网络流

    网络流概念

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

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

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

    相关定义

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

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

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

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

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

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

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

    增广路算法

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

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

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

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

    求解思路

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

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

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

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

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

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

      可以看下面这张图。

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

     像我们上面这张图,因为我们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开始建,我们知道

    21=3,31=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;
    }
    

    //码风过丑,请联系Aswert 这锅我不背

    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<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<queue>
    using namespace std;
    #define LL long long
    const int INF = 2333333;
    const int N = 210;
    int n,m,x,y,w,s,t,tot = 1;
    LL maxflow;
    int dep[N],head[N];
    struct node{
    	int to , net , w;
    }e[1000010];
    inline int read()
    {
    	int s = 0 , w = 1; char ch = getchar();
    	while(ch < '0' || ch > '9'){if(ch == '-')w = -1; ch = getchar();}
    	while(ch >= '0' && ch <= '9'){s = s * 10+ch-'0'; ch = getchar();}
    	return s * w;
    }
    void add(int x, int y , int w)
    {
    	e[++tot].w = w;
    	e[tot].to = y;
    	e[tot].net = head[x];
    	head[x] = tot;
    }
    bool bfs()//构建分层图 
    {
    	memset(dep , 0 , sizeof(dep));//每个节点层次初始化 
    	queue<int> q;
    	q.push(s); dep[s] = 1;//源点初始化为1; 
    	while(!q.empty())
    	{
    		int x = q.front(); q.pop();
    		for(int i = head[x]; i; i = e[i].net)
    		{
    			int to = e[i].to;
    			if(e[i].w && !dep[to])//构建分层图的时候要保证边不为0,如果dep[]已经被更新就不用更新了 
    			{
    				q.push(to);
    				dep[to] = dep[x] + 1;
    				if(to == t) return 1;//如果到达汇点,进行dfs 
    			}
    		}
    	}
    	return 0;
    }
    int dinic(int x , int flow)
    {
    	if(x == t) return flow;
    	int rest = flow , k;//rest表示当前这个节点最大允许通过流量 
    	for(int i = head[x]; i && rest; i = e[i].net)
    	{
    		int to = e[i].to; int val = e[i].w;
    		if(val && (dep[to] == dep[x] + 1))//寻找增广路 
    		{
    			k = dinic(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()
    {
    	n = read(); m = read(); s = read(); t = read();
    	for(int i = 1; i <= m; i++)
    	{
    		x = read(); y = read(); w = read();
    		add(x,y,w); add(y,x,0);
    	}
    	int flow = 0;
    	while(bfs())
    	{
    		while(flow = dinic(s,INF)) maxflow += 1LL*flow;
    	}
    	printf("%lld\n",maxflow);
    	return 0;
    }
    

    最小割

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

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

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

    代码同上。

    转载于jbh大佬的博客
    ENDing!

  • 相关阅读:
    高盛、沃尔玛 题做出来还挂了的吐槽
    amazon师兄debrief
    到所有人家距离之和最短的中点 296. Best Meeting Point
    问问题没人回答的情况怎么办终于有解了
    找名人 277. Find the Celebrity
    数组生存游戏 289. Game of Life
    547. Number of Provinces 省份数量
    428. Serialize and Deserialize Nary Tree 序列化、反序列化n叉树
    alias别名简介和使用
    面试官:线程池执行过程中遇到异常会发生什么,怎样处理? Vincent
  • 原文地址:https://www.cnblogs.com/genshy/p/13369027.html
Copyright © 2011-2022 走看看