浅谈网络最大流

网络最大流

目录

  • 前言node

  • 双倍经验c++

  • 网络流初步算法

  • 网络最大流数组

  • \(EK\)增广路算法网络

  • \(Dinic\)算法框架


前言

这篇题解是当作学习记录写的,因此会对网络最大流这个概念进行讲解(\(dalao\)们能够忽略蒟蒻\(orz\)函数

双倍经验\(Time\)

  1. 洛谷P3376 【模板】\(Ek\)算法 / \(Dinic\)算法)学习

  2. 洛谷P2740 [USACO4.2]草地排水Drainage Ditches优化


网络流初步

这里主要讨论一下网络流算法可能会涉及到的一些概念性问题spa

  • 定义

对于任意一张有向图(也就是网络),其中有\(N\)个点、\(M\)条边以及源点\(S\)和汇点\(T\)

而后咱们把\(c(x,y)\)称为边的容量

  • 转换

为了通俗易懂,咱们来结合生活实际理解上面网络的定义:

将有向图理解为咱们城市的水网,有\(N\)户家庭、\(M\)条管道以及供水点\(S\)和汇合点\(T\)

是否是好理解一点?如今给出一张网络(图丑勿怪啊QAQ):

\(S->C->D->E->T\)就是该网络的一个流,\(2\)这个流的流量

  • 流函数

和上面的\(c\)差很少,咱们把\(f(x,y)\)称为边的流量,则\(f\)称为网络的流函数,它知足三个条件:

  1. \(s(x,y)≤c(x,y)\)

  2. \(f(x,y)=-f(y,x)\)

  3. \(\forall\) \(x\)\(S\)\(x≠T\), \(\sum_{(u,x)∈E }f(u,x)=\sum_{(x,v)∈E }f(x,v)\)

这三个条件其实也是流函数的三大性质:

  1. 容量限制:每条边的流量总不可能大于该边的容量的(否则水管就爆了)

  2. 斜对称:正向边的流量=反向边的流量(反向边后面会具体讲)

  3. 流量守恒:正向的全部流量和=反向的全部流量和(就是总量始终不变)

  • 残量网络

在任意时刻,网络中全部节点以及剩余容量大于\(0\)的边构成的子图被称为残量网络


最大流

对于上面的网络,合法的流函数有不少,其中使得整个网络流量之和最大的流函数称为网络的最大流,此时的流量和被称为网络的最大流量

最大流能解决许多实际问题,好比:一条完整运输道路(含多条管道)的一次最大运输流量,还有二分图(蒟蒻还没学二分图,学了以后会更新的qwq)

下面就来介绍计算最大流的两种算法:\(EK\)增广路算法和\(Dinic\)算法


\(Edmonds-Karp\)增广路算法

(为了简便,习惯称为\(EK\)算法)

  • 首先来说增广路是什么:

若一条从\(S\)\(T\)的路径上全部边的剩余容量都大于0,则称这样的路径为一条增广路(剩余流量:\(c(x,y)-f(x,y)\)

  • 而后就是\(EK\)算法的核心思想啦:

如上,显然咱们可让一股流沿着增广路从\(S\)流到\(T\),而后使网络的流量增大

\(EK\)算法的思想就是不断用BFS寻找增广路并不断更新最大流量值,直到网络上不存在增广路为止

  • 再来说理论实现过程:

\(BFS\)寻找一条增广路时,咱们只须要考虑剩余流量不为\(0\)的边,而后找到一条从\(S\)\(T\)的路径,同时计算出路径上各边剩余容量值的最小值\(dis\),则网络的最大流量就能够增长\(dis\)通过的正向边容量值所有减去\(dis\),反向边所有加上\(dis\)

  • 反向边

插入讲解一下反向边这个概念,这是网络流中的一个重点

为何要建反向边?

由于可能一条边能够被包含于多条增广路径,因此为了寻找全部的增广路经咱们就要让这一条边有屡次被选择的机会

而构建反向边则是这样一个机会,至关于给程序一个反悔的机会!

为何是反悔?

由于咱们在找到一个\(dis\)后,就会对每条边的容量进行减法操做,而直接更改值就会影响到以后寻找另外的增广路

还很差理解?那咱们举个通俗易懂的例子吧:

本来\(A\)\(B\)的正边权是一、反边权是0,在第一次通过该边后(假设\(dis\)值为1),则正边权变为0,反边权变为1

当咱们须要第二次通过该边时,咱们就可以经过走反向边恢复这条边的原样(可能有点绕,你们好好理解一下)

以上都是我我的的理解,如今给出《算法竞赛进阶指南》上关于反向边的证实:

当一条边的流量\(f(x,y)>0\)时,根据斜对称性质,它的反向边流量\(f(y,x)<0\),此时一定有\(f(y,x)<c(y,x)\),因此\(EK\)算法除了遍历原图的正向边之外还要考虑遍历每条反向边

  • 邻接表“成对存储”

咱们将正向边和反向边存在“2和3”、“4和5”、“6和7”····

为何?

由于在更新边权的时候,咱们就能够直接使用\(xor 1\)的方式,找到对应的正向边和反向边(奇数异或1至关于-1,偶数异或1至关于+1)

代码实现以下(整个更新边权的操做函数):

inline void update() {
    int x=t;
    while(x!=s) {
        int v=pre[x];
        e[v].val-=dis[t];
        e[v^1].val+=dis[t];
        x=e[v^1].to;
    }
    ans+=dis[t];
}
  • 适用范围

时间复杂度为\(O(nm^2)\),通常能处理\(10^3\)~\(10^4\)规模的网络

  • 代码\(Code\)

(以本道模板题的代码为准,其余题能够将\(longlong\)换成\(int\)而且能够去掉处理重边操做)

#include <bits/stdc++.h>
using namespace std;
int n,m,s,t,u,v;
long long w,ans,dis[520010];
int tot=1,vis[520010],pre[520010],head[520010],flag[2510][2510];

struct node {
	int to,net;
	long long val;
} e[520010];

inline void add(int u,int v,long long w) {
	e[++tot].to=v;
	e[tot].val=w;
	e[tot].net=head[u];
	head[u]=tot;
	e[++tot].to=u;
	e[tot].val=0;
	e[tot].net=head[v];
	head[v]=tot;
}

inline int bfs() {  //bfs寻找增广路 
	for(register int i=1;i<=n;i++) vis[i]=0;
	queue<int> q;
	q.push(s);
	vis[s]=1;
	dis[s]=2005020600;
	while(!q.empty()) {
		int x=q.front();
		q.pop();
		for(register int i=head[x];i;i=e[i].net) {
			if(e[i].val==0) continue;  //咱们只关心剩余流量>0的边 
			int v=e[i].to;
			if(vis[v]==1) continue;  //这一条增广路没有访问过 
			dis[v]=min(dis[x],e[i].val);
			pre[v]=i;  //记录前驱,方便修改边权 
			q.push(v);
			vis[v]=1;
			if(v==t) return 1;  //找到了一条增广路 
		}
	}
	return 0;
}

inline void update() {  //更新所通过边的正向边权以及反向边权 
	int x=t;
	while(x!=s) {
		int v=pre[x];
		e[v].val-=dis[t];
		e[v^1].val+=dis[t];
		x=e[v^1].to;
	}
	ans+=dis[t];   //累加每一条增广路经的最小流量值 
}

int main() {
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(register int i=1;i<=m;i++) {
		scanf("%d%d%lld",&u,&v,&w);
		if(flag[u][v]==0) {  //处理重边的操做(加上这个模板题就能够用Ek算法过了) 
			add(u,v,w);
			flag[u][v]=tot;
		}
		else {
			e[flag[u][v]-1].val+=w;
		}
	}
	while(bfs()!=0) {  //直到网络中不存在增广路 
		update();
	}
	printf("%lld",ans);
	return 0;
}

\(Dinic\)算法

\(EK\)算法每次均可能会遍历整个残量网络,但只找出一条增广路

是否是有点不划算?能不能一次找多条增广路呢?

答案是能够的:\(Dinic\)算法

  • 分层图&\(DFS\)

根据\(BFS\)宽度优先搜索,咱们知道对于一个节点\(x\),咱们用\(d[x]\)来表示它的层次,即\(S\)\(x\)最少须要通过的边数。在残量网络中,知足\(d[y]=d[x]+1\)的边\((x,y)\)构成的子图被称为分层图(相信你们已经接触过了吧),而分层图很明显是一张有向无环图

为何要建分层图?

讲这个缘由以前, 咱们还要知道一点:\(Dinic\)算法还须要\(DFS\)

如今再放上第一张图,咱们来理解

根据层次的定义,咱们能够得出:

第0层:S
第1层:A、C
第2层:B、D
第3层:E、T

\(DFS\)中,从\(S\)开始,每次咱们向下一层次随便找一个点,直到到达\(T\),而后再一层一层回溯回去,继续找这一层的另外的点再往下搜索

这样就知足了咱们同时求出多条增广路的需求!

  • \(Dinic\)算法框架
  1. 在残量网络上\(BFS\)求出节点的层次,构造分层图

  2. 在分层图上\(DFS\)寻找增广路,在回溯时同时更新边权

  • 适用范围

时间复杂度:\(O(n^2m)\),通常可以处理\(10^4\)~\(10^5\)规模的网络

相较于\(EK\)算法,显然\(Dinic\)算法的效率更优也更快:虽然在稀疏图中区别不明显,但在稠密图中\(Dinic\)的优点便凸显出来了(因此\(Dinic\)算法用的更多)

此外,\(Dinic\)算法求解二分图最大匹配的时间复杂度为\(O(m\sqrt{n})\)

  • 代码\(Code\)

这份代码是本模板题的AC代码,可是使用到了\(Dinic\)算法的两个优化:当前弧优化+剪枝

#include <bits/stdc++.h>
using namespace std;
const long long inf=2005020600;
int n,m,s,t,u,v;
long long w,ans,dis[520010];
int tot=1,now[520010],head[520010]; 

struct node {
	int to,net;
	long long val;
} e[520010];

inline void add(int u,int v,long long w) {
	e[++tot].to=v;
	e[tot].val=w;
	e[tot].net=head[u];
	head[u]=tot;
	
	e[++tot].to=u;
	e[tot].val=0;
	e[tot].net=head[v];
	head[v]=tot;
}

inline int bfs() {  //在惨量网络中构造分层图 
	for(register int i=1;i<=n;i++) dis[i]=inf;
	queue<int> q;
	q.push(s);
	dis[s]=0;
	now[s]=head[s];
	while(!q.empty()) {
		int x=q.front();
		q.pop();
		for(register int i=head[x];i;i=e[i].net) {
			int v=e[i].to;
			if(e[i].val>0&&dis[v]==inf) {
				q.push(v);
				now[v]=head[v];
				dis[v]=dis[x]+1;
				if(v==t) return 1;
			}
		}
	}
	return 0;
}

inline int dfs(int x,long long sum) {  //sum是整条增广路对最大流的贡献
	if(x==t) return sum;
	long long k,res=0;  //k是当前最小的剩余容量 
	for(register int i=now[x];i&&sum;i=e[i].net) {
		now[x]=i;  //当前弧优化 
		int v=e[i].to;
		if(e[i].val>0&&(dis[v]==dis[x]+1)) {
			k=dfs(v,min(sum,e[i].val));
			if(k==0) dis[v]=inf;  //剪枝,去掉增广完毕的点 
			e[i].val-=k;
			e[i^1].val+=k;
			res+=k;  //res表示通过该点的全部流量和(至关于流出的总量) 
			sum-=k;  //sum表示通过该点的剩余流量 
		}
	}
	return res;
}

int main() {
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(register int i=1;i<=m;i++) {
		scanf("%d%d%lld",&u,&v,&w);
		add(u,v,w);
	}
	while(bfs()) {
		ans+=dfs(s,inf);  //流量守恒(流入=流出) 
	}
	printf("%lld",ans);
	return 0;
}
  • 当前弧优化

对于一个节点\(x\),当它在\(DFS\)中走到了第\(i\)条弧时,前\(i-1\)条弧到汇点的流必定已经被流满而没有可行的路线了

那么当下一次再访问\(x\)节点时,前\(i-1\)条弧就没有任何意义了

因此咱们能够在每次枚举节点\(x\)所连的弧时,改变枚举的起点,这样就能够删除起点之前的全部弧,来达到优化剪枝的效果

对应到代码中,就是\(now\)数组


后序

终于写完了....如今来特别感谢一些:@那一条变阻器 对于使用\(EK\)算法过掉本题的帮助 以及 @取什么名字 讲解\(Dinic\)算法的\(DFS\)部份内容

若是本篇题解有任何错误或您有任何不懂的地方,欢迎留言区评论,我会及时回复、更正,谢谢你们orz!

相关文章
相关标签/搜索