网络流(一)
网络流(一)
网络流问题是这样的:
- 给定一个有向图 ,把图中的边看作管道,每条边有一个权值 ,表示该管道的流量上限。
- 给定源点 和汇点 ,现在假设在 处有一个水源, 处有一个蓄水池,问从 到 的最大水流量是多少,类似于这类的问题都可归结为网络流问题。
- 从 到 的一条可流路径称之为增广路。
让我们来对它进行一个形式化。考虑流 ,它需要满足的条件有:
- 流量不能超过容量。。
- 对于每个点,流进了多少水,就要流出多少水。。
- 为了方便起见,我们还定义一条边的反向边 的流量满足 。稍后我们将会看到这么做有什么用。
对于一个流 ,残量网络定义为一个点集与边集都和原图相同的图,但 的边权为 。
Start
一个显然的想法是考虑贪心:对于一条增广路,我们找到路上的边权最小值 ,然后将路径上每条边都减去 ;不断找增广路,最后没有增广路的时候就认为算法结束。
然而有一个很显然的反例:
如果我们不幸地选了 这条增广路,那么一下子就不存在增广路了,求出的流大小为 ;
然而更优的是 与 这两条路径,这样的流大小为 。
解决方法是建立反向边。我们如果在一条边 上添加了一个大小为 的流,那么不仅要将边 的边权减去 ,同时还将其反向边 的边权加上 。这可以看做一种「反悔」机制。
实际上回忆上面「流」的第三条性质,我们将 加上 的同时,自然要给 减去 。由于残量网络上的边权是 ,这条边的边权就会加上 。
那么我们就有了大致的算法框架:每次在图上只沿着容量 的边走,如果找到 到 的一条路径,设路径上边权的最小值为 ,那么就将这条路径上所有边的边权都减去 ,同时将每条边反向边的边权 ,并将最大流的值加上 ;如果 与 不连通,就认为无法增广,算法结束。
如果每次任意找一条增广路进行增广,我们就得到了 算法。
你想必有这样的疑惑:建完反向边后,一条边可能会被访问很多次,算法的效率将会很低下;甚至于,增广会不会一直进行下去?我们敏锐地注意到每次增广后最大流必然严格增长,因此增广不会无限地进行下去。但,这仍然无法改变算法效率低下的事实。
事实上,这个算法确实很容易被卡掉。如图所示:(借算导上的图)
Edmond-Karp 增广路算法
我们来进行一些优化。
你想必早已听说过:只要每次使用 找到 的一条最短增广路,增广次数将不超过 。
这里我们来简要证明一下它的复杂度。
对于一条增广路 ,如果其上一条路径 满足其容量 恰为 中所有路径的容量最小值,则称其为 上的关键边。一条增广路上可以有多条关键边。
显然,每条增广路上都至少有一条关键边。这意味着增广路的个数不会超过关键边的个数,也就是说,增广次数不会超过关键边的总数。
我们将证明:每条边成为关键边的次数不超过 。一旦该结论成立,关键边的总数将为 ,则增广次数不超过 。
为了证明这一结论,我们记 为 的最短路长度,这里边的权重为 ,且只能经过边权 的边。
可以证明,无论如何增广, 必然不会减少。
对于一条边 ,当其成为关键边时,由于我们走的是最短路径,故必有 。
由于这条边是增广路上的关键边,因此在增广过后这条边的残存容量将降至 ;反向边的容量则会相应增加。
想要让这条边再次成为关键边,必须要存在一次增广,使得增广路经过其反向边 。
记增广后的最短路长为 ,那么有 。
这表明每条边每成为一次关键边, 到其一端点的最短路长就会 。最短路长最多为 ,则每条边成为关键边的次数不超过 。这样就完成了我们的证明。
上面的证明其实主要在说明一个事:每条边被访问的次数并不会太多。
由于 的复杂度是 ,该算法的总时间复杂度为 。
Dinic 算法
在 算法中,我们每次增广前先将图分层。节点 的层次 被定义为 最少需要经过的边数。
一次 即可完成分层。分层后,若无法求出 的层次,则不存在增广路,算法终止。
在增广的时候,我们只走满足 的边 。我们称满足上面等式的边为允许弧。
增广的操作使用 来实现。与 算法不同的是, 算法每次会增广多条路径。
的过程中,我们从 出发,每次只走允许弧,同时记录一下流到当前节点时最多能流入的流量 flow
;在访问一个节点 时,我们遍历其在残量网络上的每条出边 ,对 进行 (此时传入的 flow
即为 ),并返回从 所能流出的最大流量 。 的返回值就是 。
可以证明, 算法的时间复杂度为 ,实际表现则快得多,甚至可以处理 的网络。
值得一提的是, 算法求解二分图最大匹配的复杂度是 ;更一般地,而在边权均为 的图上跑 ,复杂度将会是 。
算法有一个效果显著的优化——当前弧优化。
我们注意到,在一次增广中,如果一条边的流量已经满了,那么这次增广无论如何都不会再走这条边了。
因此,我们对每个节点 记录一条「当前弧」,表示 下一条应该增广的边。下次访问 时,直接从当前弧开始往后遍历出边,就省下了遍历前面那些已经满流的边的时间。
代码实现
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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App