网络流(一)

网络流(一)

网络流问题是这样的:

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

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

  • 流量不能超过容量。f(u,v)c(u,v)
  • 对于每个点,流进了多少水,就要流出多少水。u,xf(x,u)=xf(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

如果我们不幸地选了 1324 这条增广路,那么一下子就不存在增广路了,求出的流大小为 1

然而更优的是 134124 这两条路径,这样的流大小为 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 的边走,如果找到 st 的一条路径,设路径上边权的最小值为 w,那么就将这条路径上所有边的边权都减去 w,同时将每条边反向边的边权 +w,并将最大流的值加上 w;如果 st 不连通,就认为无法增广,算法结束。

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

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

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

image-20220209155158032.png

Edmond-Karp 增广路算法

我们来进行一些优化。

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

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

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

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

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

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

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

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

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

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

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

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

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

由于 BFS 的复杂度是 O(n+m),该算法的总时间复杂度为 O(nm(n+m))=O(nm2)

Dinic 算法

Dinic 算法中,我们每次增广前先将图分层。节点 u层次 du 被定义为 su 最少需要经过的边数。

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

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

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

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

可以证明,Dinic 算法的时间复杂度为 O(n2m),实际表现则快得多,甚至可以处理 n,m105 的网络。

值得一提的是,Dinic 算法求解二分图最大匹配的复杂度是 O(mn);更一般地,而在边权均为 1 的图上跑 Dinic,复杂度将会是 O(mmin(m,n0.67))

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 @   云浅知处  阅读(136)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示