图论-网络流②-最大流①
上一篇:图论-网络流①-什么是网络流
下一篇:图论-网络流③-最大流②
参考文献:
- https://www.cnblogs.com/DuskOB/p/11216861.html
- https://blog.csdn.net/yjr3426619/article/details/82808303
- https://blog.csdn.net/lym940928/article/details/90209172
- https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E6%B5%81/2987528?fr=aladdin
- https://www.cnblogs.com/pk28/p/8039645.html
- https://blog.csdn.net/disgustinglemon/article/details/51296636
大纲
- 什么是网络流
- 最大流(最小割)
- (常用)
- (不讲)
- (快)
- 最大流解题
- 费用流
- 费用流
- 费用流
- 费用流
- 费用流
- 费用流解题
- 有上下界的网络流
- 无源汇上下界可行流
- 有源汇上下界可行流
- 有源汇上下界最大流
- 有源汇上下界最小流
- 最大权闭合子图
- 有上下界的网络流解题
最大流
即源点到汇点的最大总流量。如果把网络流图比作水管道系统,那么就是求单位时间内源点到汇点能流多少水。概念较抽象,那么拿上面那张网络流图举例:
结论:最大流为 。
首先 处可以流出无限流量,但因为 到 的边流量只有 ,所以 先向 流 的流量,现在 处有 的流量。因为是单位时间内的流量,所以 的边就不能再用了。
同理, 处的 份流量分到 和 处。 和 的边不能再用。 处有 流量, 处有 流量。然后再通过边 和 向汇点 流 的流量。
然后再用 ,于是 处有了 的流量。然后再走 ,因为 处只有 的流量,所以虽然 能流 的流量, 也只能得到 的流量。 然后再看 ,因为 的边已经用完,所以即使 处有 的流量, 也得不到一点流量。
所以最终最大流为 。
不过现在先不讲最大流算法,过会儿你就知道为什么了。
最小割
定义
给定一个网络流图 ,源点为 ,汇点为 。若一个边集 被删去后, 和 不再联通,则称该边集为网络的割。边流量之和的最小的割称为网络的最小割。
再回到刚刚那个网络流图:
结论:最小割为
。
先割断 ,再割断 , 和 不再联通。并且保证没有别的割法比这样更优。所以该网络流图的最小割为 。
重要结论:
最大流(最小割)算法
通过上文我们已知一个网络流图的最小割等于它的最大流,所以求最大流的算法同样也是求最小割的算法。求最大流的算法很多,其中 最为普遍使用。
Dinic
算法会对网络流图做一个分层。其实也就是做一次 ,把源点设为第 层,然后遍历源点的相邻节点,设为第 层,再遍历这些节点的相邻节点……直到汇点。
实现:
1.将源点的层次设为 ,把源点设为已经访问,别的点设为未访问。
2.按照 序遍历网络流图,为节点分层,遍历到终点结束。
3.在层次网络中,沿着相邻层 搜索所有的增广路,并做相应的流量调整。
4.重复 。
算法较抽象,拿图举例(节点上的数为层次):
第一次建立层次网络,找到了蓝线表示的三条增广路,做流量调整。
第二次建立层次网络,上次流光的边这次不发挥作用,找到一条增广路。
第三次建立层次网络,汇点已经不能通过有用的边联通,算法终止。
如果你懂了,蒟蒻就放代码了。 最大流最小割算法:
#include <bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
template<int V,int M>
class Dinic{
public:int E,g[V],to[M],nex[M],fw[M];
void clear(){memset(g,0,sizeof g),E=1;}
//E=1保证了互为反边的两条边可以通过^1互相得到
Dinic(){clear();}
//初始化
void add(int x,int y,int f){nex[++E]=g[x],to[E]=y,fw[E]=f,g[x]=E;}
//标准加边
void Add(int x,int y,int f){add(x,y,f),add(y,x,0);}
//加正边和反边,使得增广可以反悔
int dep[V],cur[V];bool vis[V];queue<int> q;
//dep表示层次,cur为单前弧优化,下面会讲。
//vis表示是否访问,queue维护bfs
bool bfs(int s,int t,int p){
for(int i=1;i<=p;i++) vis[i]=0,cur[i]=g[i];
q.push(s),vis[s]=1,dep[s]=0; //从源点开始bfs
while(q.size()){
int x=q.front();q.pop();
for(int i=g[x];i;i=nex[i])if(!vis[to[i]]&&fw[i])
q.push(to[i]),vis[to[i]]=1,dep[to[i]]=dep[x]+1;
//bfs过程中顺便给每个节点标上层次。
}
return vis[t]; //表示联通
}
int dfs(int x,int t,int F){
if(x==t||!F) return F;
int f,flow=0;
for(int&i=cur[x];i;i=nex[i]) //即i=g[x]
if(dep[to[i]]==dep[x]+1&&(f=dfs(to[i],t,min(F,fw[i])))>0) //沿着层次增广
{fw[i]-=f,fw[i^1]+=f,F-=f,flow+=f;if(!F) break;}
//边的流量调整
return flow; //一次增广的流量。
}
int dinic(int s,int t,int p){ //多次增广函数
int res=0,f;
while(bfs(s,t,p)) while((f=dfs(s,t,inf))) res+=f;
return res;
}
};
int n,m,s,t,p;
Dinic<10010,200010> net;
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t),p=n;
for(int i=1,x,y,f;i<=m;i++){
scanf("%d%d%d",&x,&y,&f);
net.Add(x,y,f);
}
printf("%d\n",net.dinic(s,t,p));
return 0;
}
以上代码读者可能有两个地方不懂:
1.反向反悔边
因为一条边的流量是通过以下代码调整的:
fw[i]-=f,fw[i^1]+=f,F-=f,flow+=f;if(!F) break;
所以可以把反悔看成走反边,以得到最大流。
2.单前弧优化
for(int i=1;i<=p;i++) vis[i]=0,cur[i]=g[i];
for(int&i=cur[x];i;i=nex[i])
因为 的增广是沿着加边顺序(严谨地说,是沿着加边反顺序)增广的,所以每一次增广时,前面几条边可能已经增广完了,这时,如果记录第一条没增广完的边,下一次增广从这条边开始,就方便、快很多。
下一篇会讲最大流的剩下 种算法。
祝大家学习愉快!