• [学习笔记]网络流


    网络流是一个博大精深的OI类别

    今天浅显地理解了一下网络流,做一下笔记

    定义:

    1.网络:是一个有向图,每个边有一个容量c(x,y),每条边也会有一个可行的流量f(x,y),这个f被称为流函数

    图中有一个源点S,不断往外流水,只流不入,汇点T则相反。其他的点流入的量等于流出的量。

    流/增广路:一条从S 出发,能到达T且经过的边的流函数最小值>0的路径,称为一个流/增广路

    网络流:顾名思义,和水流类似,就是在一张网络上流水,每条边好似一个管道。

    2.三大定律:

    ①容量限制:f(x,y)<=c(x,y) 每条边不能超过边的容量。这限制了网络一定有一个最大的总流量

    ②斜对称:f(x,y)=-f(y,x) 一条边相当于是一个正边和反边组成,从x往y流flow,相当于从y往回流-flow

    这一点其实是人为定义的,原图中一般也不画出,但是相当关键,也是可以用dinic,EK等算法直接求最大流的“反悔回流”的条件

    这个反边是一定要建的。不要忘了。

    ③流量守恒。除了源点,汇点之外,每个点流入的总流量,一定等于流出的总流量。

    这一点也是很重要的基础,为最大流求法和模型构建提供了成立的条件。

    之后边的容量保留的是边的剩余流量,正向减去,反向会加上。初始正向是最大容量,反向是0

    最大流

    之前已经说了,每个点有一个流入的和流出的量。

    定义,一个网络的总流量为:∑f(s,v)即从源点流出的总量。

    满足三大定律的流函数有很多,其中最多的总流量称为这个网络的最大流。

    网络流的扩展变形都是建立在最大流基础上的。

    算法:

    ①Edmonds-Karp(EK)利用bfs每次找到一条增广路增广。

    (留坑,学完费用流再补)

    upda:

    EK每次bfs找到一条 增广路,然后对这条增广路进行流量改变。

    记录一个incf[x],表示,进入x的流量。

    要记录一个pre[x],表示点x是通过哪条边转移过来的。

    一般能处理10^3~10^4的网络

    ②Dinic算法

    发现EK每次最多只找到一条增广路,效率不是很高

    Dinic bfs搜出分层图,dfs多路增广,效率就很高了。

    放链接:Dinic算法(研究总结,网络流)

    一般能处理10^4~10^5的网络。

    模板:

    luoguP3376 【模板】网络最大流

    #include<bits/stdc++.h>
    using namespace std;
    const int N=10000+7;
    const int M=100000+7;
    const int inf=0x3f3f3f3f;
    int n,m;
    struct node{
        int nxt,to;
        int w;
    }e[2*M];
    int hd[N],cnt=1;//从2开始编号,i^1就是反边编号 
    int s,t;
    void add(int x,int y,int z){
        e[++cnt].nxt=hd[x];
        e[cnt].to=y;
        e[cnt].w=z;
        hd[x]=cnt;
    }
    int d[N];
    queue<int>q;
    bool bfs(){//bfs找分层图 
        while(!q.empty()) q.pop();
        memset(d,0,sizeof d);
        d[s]=1;
        q.push(s);
        while(!q.empty()){
            int x=q.front();q.pop();
            for(int i=hd[x];i;i=e[i].nxt){
                int y=e[i].to;
                if(!d[y]&&e[i].w){
                    d[y]=d[x]+1;
                    q.push(y);
                    if(y==t) return 1;
                }
            }
        }
        return 0;
    }
    int dfs(int x,int flow){//dfs多路增广 
        if(x==t) return flow;
        int rest=flow;
        for(int i=hd[x];i&&rest;i=e[i].nxt){
            int y=e[i].to;
            if(d[y]==d[x]+1&&e[i].w){
                int k=dfs(y,min(rest,e[i].w));
                if(!k) d[y]=0;
                rest-=k;
                e[i].w-=k;
                e[i^1].w+=k;
            }
        }
        return flow-rest;
    }
    int main()
    {
        scanf("%d%d%d%d",&n,&m,&s,&t);
        int x,y,z;
        for(int i=1;i<=m;i++){//建边 
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);add(y,x,0);//反边起初容量是0 
        }
        int maxflow=0;
        int flow;
        while(bfs()) 
          while(flow=dfs(s,inf)) maxflow+=flow;
        printf("%d",maxflow);
        return 0;
    }
    最大流

    应用:

    ①二分图最大匹配,S向所有左部点连边,左部点向右部点连边,右部点向T连边。边权均为1

    显然,这个网络的最大流就是二分图最大匹配。

    ②最小割(见下)

    最小割

    割集:割掉一些边使得S、T不连通,这些边的集合可以作为一个割集

    最小割:一个割集的代价是所有割边的容量的和,代价最小的割集的代价就是最小割

    定理:

    最大流=最小割

    证明:假设最小割小于最大流,那么割掉所选择的边,必然残余网络还有一条增广路,与S、T不连通矛盾

    所以,最小割大于等于最大流。

    只需证明可以有一种割集,使得代价是最大流即可。

    留坑。

    理解:

    最小割和最大流的思路基本没什么共同之处,一个是不断流,取最大,一个是割边,找最小代价。

    对于建模的时候,除了明显的割掉某些边、点之外,

    类似有代价的选择,二元关系等也可以用最小割处理。

    例题:

    POJ 1966 Cable TV Network

    见另一篇博客:

    这个题,体现了“点边转化”,“容量inf”的处理思想。

    点边转化:把点的信息转移到边上,或者边信息转移到点上。

    点变成边:拆点,两个点之间的边信息是点的信息。并且要保证,实际经过这个点,必须经过这个边。

        一般从上面的点x'向下面y连边。

    边变成点:把边拆成两个,中间加一个点,记录边的信息。

    费用流

     有的时候,题目会涉及到什么最多取得情况下,最多/最少的代价等。

    这就要用到费用流了。

    注意到,最大流也不是唯一的。

    我们可以给每条边加一个单位花费c[i],表示1个流量经过,要花费的代价。

    建立反边的时候,费用是-c[i],退流的时候,把费用也就退了。

    费用流分为:最小费用最大流,最大费用最大流。

    所以,费用流的模型 本身是建立在最大流的基础上的。先满足最大流的情况下,再满足最优费用(否则最小费用就是0咯大概)

    方法:

    Edmond-Karp费用流算法。

    以最小费用最大流为例:

    把bfs改成spfa,每次找到到 t 路径上的最小费用总和。

    然后,费用就是incf[t]*dis[t]

    找不到最短路了, 那么一定就是最大流找完了,return false

    为什么是对的?

    感性理解一下,每次找增广路,就能找到最大流(虽然我不会证),那么,每次找费用最少的增广路,一定是不影响最大流的

    那么,既然最大流一定,每次乘一个最小的花费,就是最小费用了。(因为还可以退流的,所以即使当前最优解不是全局最优解,还是可以反悔的)

    dinic不是更快吗?为什么不用dinic跑?

    因为dinic多路增广啊,而且其实是随便瞎走,不关心之后走到哪里,自然何以知道怎样费用是最小的?

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    const int N=5000+4;
    const int M=50000+4;
    const int inf=0x3f3f3f3f;
    int n,m,s,t;
    struct node{
        int nxt,to;
        int w;
        int c;
    }e[2*M];
    int hd[N],cnt=1;
    void add(int x,int y,int z,int l){
        e[++cnt].nxt=hd[x];e[cnt].to=y;e[cnt].w=z;e[cnt].c=l;hd[x]=cnt;
        e[++cnt].nxt=hd[y];e[cnt].to=x;e[cnt].w=0;e[cnt].c=-l;hd[y]=cnt;
    }
    int incf[N],ans;
    int maxflow;
    int pre[N];
    queue<int>q;
    int d[N];
    bool vis[N];
    bool spfa(){
        while(!q.empty())q.pop();
        memset(d,inf,sizeof d);
        d[s]=0;vis[s]=1;
        incf[s]=inf;
        q.push(s);
        while(!q.empty()){
            int x=q.front();q.pop();
            //cout<<x<<endl;
            vis[x]=0;
            for(int i=hd[x];i;i=e[i].nxt){
                int y=e[i].to;
                //cout<<" to "<<y<<endl;
                if(!e[i].w) continue;
                if(d[y]>d[x]+e[i].c){
                    d[y]=d[x]+e[i].c;
                    pre[y]=i;
                    incf[y]=min(incf[x],e[i].w);
                    if(!vis[y]){
                        vis[y]=1;
                        q.push(y);
                    }
                }
            }
        }
        if(d[t]==inf) return false;
        return true;
    }
    
    void upda(){
        //cout<<" jhaa "<<incf[t]<<endl;
        int x=t;
        while(x!=s){
            e[pre[x]].w-=incf[t];
            e[pre[x]^1].w+=incf[t];
            
            x=e[pre[x]^1].to;
        }
        maxflow+=incf[t];
        ans+=incf[t]*d[t];
    }
    int main()
    {
        scanf("%d%d%d%d",&n,&m,&s,&t);
        int x,y,z,l;
        for(int i=1;i<=m;i++){
            scanf("%d%d%d%d",&x,&y,&z,&l);
            add(x,y,z,l);
        }
        while(spfa()) upda();
        printf("%d %d",maxflow,ans);
        return 0;
    }
    最小费用最大流

    例题:

    [SDOI2009]晨跑

    特殊标志:“在周期最长的情况下,总路程最短”,就是最小费用最大流的经典标志了。

    拆点跑费用流即可。

    动态加边

    类似于动态数组和动态开点线段树,我们申请了许多空间,但是可能根本不会用上。

    对于SPFA的许多无用边,更是如此。

    如果在费用流中,SPFA次数其实很少,但是边数会很多,而SPFA就只要求一个dis[t]和pre,incf

    并不一定需要遍历所有的边。

    可以考虑把不会影响dis[t]的边先不加上,upda时候再更新。

    详见例题:[NOI2012]美食节——费用流(带权二分图匹配)+动态加边

  • 相关阅读:
    博客阅读计数优化
    博客阅读简单计数
    博客后台富文本编辑
    博客分类统计
    Django关联关系查询
    上下篇博客,按月归档
    浅谈闭包以及常见面试题
    浅谈前端缓存(转至大佬)
    post请求头的常见类型
    浅谈RegExp 对象的方法
  • 原文地址:https://www.cnblogs.com/Miracevin/p/9610823.html
Copyright © 2020-2023  润新知