[学习笔记]网络流
网络流是一个博大精深的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多路增广,效率就很高了。
一般能处理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]美食节——费用流(带权二分图匹配)+动态加边