介绍
网络流是有向图,边权是边的容量,形象的理解就是水管单位时间的流水量
源点\((S)\):水源,可以提供无穷多的水
汇点\((T)\):水的汇集点
对于每条边:它的流量小于它的容量,且流量不能为负
同时,除了汇点与源点的其他节点,流入总量=流出总量
汇点流入总量=最大流
最大流
顾名思义,最大流就是流入汇点总量最大。
不断地找增广路,即找到一条路,路上每条边的流量都小于容量。找到一条路后,修改每条路上的流量,这条路的流量=路上的边\(min(容量-流量)\)
上图中第一次找路,找到\(1->2->3->4\),找到后,修改每条边上的容量,就再也找不到路了。
但如果边的流量可以回退,下次找到\(1->3->2->4\)才能算出真正的最大流。
所以网络流的存图:每条边都可以后悔,即每条边存正反两条边。
一开始:正边容量=原来容量,反边容量=0
修改流量:正边容量-=路容量,反边容量+=路容量
在写程序时,为了快速知道一条边的反边,我们让正反边边号满足:反边=正边 XOR 1
找增广路的算法:isap
isap有个\(dis\)数组,增广时沿着\(dis\)数组连续减一的路线走,isap不依靠\(bfs\)来修改\(dis\)数组,在\(dfs\)增广时,如果找不到增广路了,就修改\(dis\)数组:\(dis[x]++\)
虽然\(isap\)不用\(bfs\),但为了减少常数,最好事先\(bfs\)一次,预处理出初始的\(dis\)数组
非常重要的优化:GAP优化
在增广的时候,随时可能\(dis[x]++\),可能某个时候,全图中已经没有\(dis[y]=dis[x]\)的节点\(y\)了,此时,\(dis<x\)或\(dis>x\),该图断层了,说明增广路已经找完,可以返回了,因此记录\(dis\)数组出现次数,如果为\(0\),则标记\(dis[S]=n\)表示算法结束。
\(code:\)
int S,T,Last[MAX_V],Next[MAX_E<<1],End[MAX_E<<1],len[MAX_E<<1],tot=1;
inline void addedge(int x,int y,int z){//建网络流正反边
End[++tot]=y,Next[tot]=Last[x],Last[x]=tot,len[tot]=z;
End[++tot]=x,Next[tot]=Last[y],Last[y]=tot,len[tot]=0;
}
int dis[MAX_V],gap[MAX_V];
queue<int> q;
void bfs(){
for(int i=1;i<=T;i++) dis[i]=T;//初始化dis数组
dis[T]=0;
queue<int> q;
q.push(T);//倒着进行bfs
while(q.size()){
int x=q.front();
q.pop();
for(int i=Last[x];i;i=Next[i]){
int y=End[i];
if(dis[y]>dis[x]+1){//更新
dis[y]=dis[x]+1;
q.push(y);
}
}
}
for(int i=1;i<=T;i++) gap[dis[i]]++;//记录dis数组次数
return;
}
int isap(int x,int flow){
if(x==T) return flow;//到了源点
int flow_now=0;
for(int i=_last[x];i;i=Next[i]){
int y=End[i];
if(len[i] && dis[x]==dis[y]+1){
int f=isap(y,min(len[i],flow-flow_now));
flow_now+=f;
len[i]-=f;//正边-=f
len[i^1]+=f;//反边+=f
if(flow==flow_now || dis[S]==T) return flow_now;
}
}
gap[dis[x]]--;
if(!gap[dis[x]]) dis[S]=T;//断层
dis[x]++;
gap[dis[x]]++;
return flow_now;
}
最小割
割集:选一些边,如果删掉这些边,源点和汇点就无法互相到达了,则这些边所组成的集合就是割集
最小割:容量总和最小的割集
在网络流上,最小割=最大流
其实,最小割可以看成是一刀将图的元素分割成了两个集合,一些属于源点,一些属于汇点,所以通常最小割的题目需要将题目信息给分开成两个不同集合。
注意:最小割的题,一定要将题目信息转化成“xxx的总和的最小值”,才能最小割建图。