网络流(一)

网络流(一)

网络流问题是这样的:

  • 给定一个有向图 \(G=(V,E)\),把图中的边看作管道,每条边有一个权值 \(c(u,v)\),表示该管道的流量上限。
  • 给定源点 \(s\) 和汇点 \(t\),现在假设在 \(s\) 处有一个水源,\(t\) 处有一个蓄水池,问从 \(s\)\(t\) 的最大水流量是多少,类似于这类的问题都可归结为网络流问题。
  • \(s\)\(t\) 的一条可流路径称之为增广路。

让我们来对它进行一个形式化。考虑流 \(f\),它需要满足的条件有:

  • 流量不能超过容量。\(f(u,v)\le c(u,v)\)
  • 对于每个点,流进了多少水,就要流出多少水。\(\forall u,\sum_{x}f(x,u)=\sum_{x}f(u,x)\)
  • 为了方便起见,我们还定义一条边的反向边 \((v,u)\) 的流量满足 \(f(v,u)=-f(u,v)\)。稍后我们将会看到这么做有什么用。

对于一个流 \(f\)残量网络定义为一个点集与边集都和原图相同的图,但 \((u,v)\) 的边权为 \(c(u,v)-f(u,v)\)

Start

一个显然的想法是考虑贪心:对于一条增广路,我们找到路上的边权最小值 \(x\),然后将路径上每条边都减去 \(x\);不断找增广路,最后没有增广路的时候就认为算法结束。

然而有一个很显然的反例:

graph _1_.png

如果我们不幸地选了 \(1\to 3\to 2\to 4\) 这条增广路,那么一下子就不存在增广路了,求出的流大小为 \(1\)

然而更优的是 \(1\to 3\to 4\)\(1\to 2\to 4\) 这两条路径,这样的流大小为 \(2\)

解决方法是建立反向边。我们如果在一条边 \((u,v)\) 上添加了一个大小为 \(k\) 的流,那么不仅要将边 \((u,v)\) 的边权减去 \(k\),同时还将其反向边 \((v,u)\) 的边权加上 \(k\)。这可以看做一种「反悔」机制。

实际上回忆上面「流」的第三条性质,我们将 \(f(u,v)\) 加上 \(k\) 的同时,自然要给 \(f(v,u)\) 减去 \(k\)。由于残量网络上的边权是 \(c(v,u)-f(v,u)\),这条边的边权就会加上 \(k\)

那么我们就有了大致的算法框架:每次在图上只沿着容量 \(>0\) 的边走,如果找到 \(s\)\(t\) 的一条路径,设路径上边权的最小值为 \(w\),那么就将这条路径上所有边的边权都减去 \(w\),同时将每条边反向边的边权 \(+w\),并将最大流的值加上 \(w\);如果 \(s\)\(t\) 不连通,就认为无法增广,算法结束。

如果每次任意找一条增广路进行增广,我们就得到了 \(\text{Ford-Fulkerson}\) 算法。

你想必有这样的疑惑:建完反向边后,一条边可能会被访问很多次,算法的效率将会很低下;甚至于,增广会不会一直进行下去?我们敏锐地注意到每次增广后最大流必然严格增长,因此增广不会无限地进行下去。但,这仍然无法改变算法效率低下的事实。

事实上,这个算法确实很容易被卡掉。如图所示:(借算导上的图)

image-20220209155158032.png

Edmond-Karp 增广路算法

我们来进行一些优化。

你想必早已听说过:只要每次使用 \(\text{BFS}\) 找到 \(s\to t\) 的一条最短增广路,增广次数将不超过 \(O(nm)\)

这里我们来简要证明一下它的复杂度。

对于一条增广路 \(p\),如果其上一条路径 \((u,v)\in p\) 满足其容量 \(c(u,v)\) 恰为 \(p\) 中所有路径的容量最小值,则称其为 \(p\) 上的关键边。一条增广路上可以有多条关键边。

显然,每条增广路上都至少有一条关键边。这意味着增广路的个数不会超过关键边的个数,也就是说,增广次数不会超过关键边的总数。

我们将证明:每条边成为关键边的次数不超过 \(O(n)\)。一旦该结论成立,关键边的总数将为 \(O(nm)\),则增广次数不超过 \(O(nm)\)

为了证明这一结论,我们记 \(\text{Dist}(u)\)\(s\to u\) 的最短路长度,这里边的权重为 \(1\),且只能经过边权 \(>0\) 的边。

可以证明,无论如何增广,\(\text{Dist}(u)\) 必然不会减少。

对于一条边 \((u,v)\),当其成为关键边时,由于我们走的是最短路径,故必有 \(\text{Dist}(v)=\text{Dist}(u)+1\)

由于这条边是增广路上的关键边,因此在增广过后这条边的残存容量将降至 \(0\);反向边的容量则会相应增加。

想要让这条边再次成为关键边,必须要存在一次增广,使得增广路经过其反向边 \((v,u)\)

记增广后的最短路长为 \(\text{Dist'}\),那么有 \(\text{Dist'}(u)=\text{Dist'}(v)+1\le \text{Dist}(v)+1=\text{Dist}(u)+2\)

这表明每条边每成为一次关键边,\(s\) 到其一端点的最短路长就会 \(+2\)。最短路长最多为 \(n-2\),则每条边成为关键边的次数不超过 \(\dfrac{n-2}{2}=O(n)\)。这样就完成了我们的证明。

上面的证明其实主要在说明一个事:每条边被访问的次数并不会太多。

由于 \(\text{BFS}\) 的复杂度是 \(O(n+m)\),该算法的总时间复杂度为 \(O(nm(n+m))=O(nm^2)\)

Dinic 算法

\(\text{Dinic}\) 算法中,我们每次增广前先将图分层。节点 \(u\)层次 \(d_u\) 被定义为 \(s\to u\) 最少需要经过的边数。

一次 \(\text{BFS}\) 即可完成分层。分层后,若无法求出 \(t\) 的层次,则不存在增广路,算法终止。

在增广的时候,我们只走满足 \(d_v=d_u+1\) 的边 \((u,v)\)。我们称满足上面等式的边为允许弧

增广的操作使用 \(\text{DFS}\) 来实现。与 \(\text{Edmond-Karp}\) 算法不同的是,\(\text{Dinic}\) 算法每次会增广多条路径。

\(\text{DFS}\) 的过程中,我们从 \(s\) 出发,每次只走允许弧,同时记录一下流到当前节点时最多能流入的流量 flow;在访问一个节点 \(u\) 时,我们遍历其在残量网络上的每条出边 \((u,v)\),对 \(v\) 进行 \(\text{DFS}\)(此时传入的 flow 即为 \(\min\{\texttt{flow}-\sum k_v,c(u,v)\}\)),并返回从 \(v\) 所能流出的最大流量 \(k_v\)\(\text{DFS}\) 的返回值就是 \(\sum_v{k_v}\)

可以证明,\(\text{Dinic}\) 算法的时间复杂度为 \(O(n^2m)\),实际表现则快得多,甚至可以处理 \(n,m\le 10^5\) 的网络。

值得一提的是,\(\text{Dinic}\) 算法求解二分图最大匹配的复杂度是 \(O(m\sqrt{n})\);更一般地,而在边权均为 \(1\) 的图上跑 \(\text{Dinic}\),复杂度将会是 \(O(m\cdot\min(\sqrt{m},n^{0.67}))\)

\(\text{Dinic}\) 算法有一个效果显著的优化——当前弧优化

我们注意到,在一次增广中,如果一条边的流量已经满了,那么这次增广无论如何都不会再走这条边了。

因此,我们对每个节点 \(u\) 记录一条「当前弧」,表示 \(u\) 下一条应该增广的边。下次访问 \(u\) 时,直接从当前弧开始往后遍历出边,就省下了遍历前面那些已经满流的边的时间。

代码实现

int head[MN],nxt[MN],edge[MN],ver[MN],tot=1;
int now[MN],d[MN],n,m,s,t;

void adde(int x,int y,int z){
	ver[++tot]=y,edge[tot]=z,nxt[tot]=head[x],head[x]=tot;
	ver[++tot]=x,edge[tot]=0,nxt[tot]=head[y],head[y]=tot;
}

queue<int>q;

bool bfs(){
	memset(d,0,sizeof(d));
	while(q.size())q.pop();
	q.push(s),d[s]=1,now[s]=head[s];
	while(q.size()){
		int u=q.front();q.pop();
		for(int i=head[u];i;i=nxt[i]){
			if((!edge[i])||d[ver[i]])continue;
			int v=ver[i];now[v]=head[v];
			d[v]=d[u]+1,q.push(v);
			if(v==t)return 1;
		}
	}
	return 0;
}

int dinic(int u,int flow){
	if(u==t)return flow;
	int rest=flow;
	for(int i=now[u];i&&rest;i=nxt[i]){
		now[u]=i;int v=ver[i];
		if(d[v]==d[u]+1&&edge[i]){
			int k=dinic(v,min(rest,edge[i]));
			if(!k)d[v]=0;//去掉增广完毕的节点
			edge[i]-=k,edge[i^1]+=k,rest-=k;
		}
	}
	return flow-rest;
}

void solve(){
	n=read(),m=read(),s=read(),t=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read();
		adde(u,v,w);
	}
	int flow=0,ans=0;
	while(bfs()){
		while(flow=dinic(s,INF))ans+=flow;
		//让一次 BFS 得出的结果可以使用多次
	}
	cout<<ans<<endl;
}
posted @ 2022-02-15 17:59  云浅知处  阅读(126)  评论(0编辑  收藏  举报