[学习笔记]网络流

网络流是一个博大精深的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]美食节——费用流(带权二分图匹配)+动态加边

原文地址:https://www.cnblogs.com/Miracevin/p/9610823.html