网络流 学习笔记

网络流 学习笔记


前言

网络流中板子并不难,真正难的是复杂度的证明还有一大堆的二级结论,本笔记主要记录学习路上遇到的各种网络流技巧与结论,相关证明可以去 OI Wiki 看。


最大流 & 最小割

首先有一个基本定理:最大流等价于最小割。

Edmonds–Karp 算法

简称 EK 算法,是最最基础的网络流算法,也是最慢的。

单次增广的过程:先 BFS 进行分层,然后从汇点回溯回去,复杂度为 \(O(|E|)\)

而增广总轮数为 \(O(|V||E|)\),总复杂度为 \(O(|V||E|^2)\)

namespace EK {
	int pa[N],dep[N],flow[N];
	template<const int N,const int M>struct CFS {
		int tot,h[N];
		struct edge {
			int u,v,c,f,nxt;
			edge(int u=0,int v=0,int c=0,int f=0,int nxt=-1):u(u),v(v),c(c),f(f),nxt(nxt) {}
		} e[M];

		edge &operator [](int i) { return e[i]; }

		void Init(int n) { RCL(h+1,-1,int,n),tot=-1; }

		void att(int u,int v,int c=0,int f=0) { e[++tot]=edge(u,v,c,f,h[u]),h[u]=tot; }

		void con(int u,int v,int c) { att(u,v,c),att(v,u); }

	};
	CFS<N,M<<1> g;

	void Build(int n,int m) {
		g.Init(n);
		FOR(i,1,m) {
			int u,v,c;
			cin>>u>>v>>c,g.con(u,v,c);
		}
	}

	bool BFS(int S,int T) {
		queue<int> q;
		RCL(dep+1,0,int,n),flow[S]=INF,dep[S]=1,q.push(S);
		while(!q.empty()&&!dep[T]) {
			int u(q.front());
			q.pop();
			EDGE(g,i,u,v)if(g[i].f<g[i].c&&!dep[v])
				pa[v]=i,flow[v]=min(flow[u],g[i].c-g[i].f),dep[v]=dep[u]+1,q.push(v);
		}
		return dep[T];
	}

	ll Max_Flow(int S,int T) {
		ll Flow(0);
		while(BFS(S,T)) {
			Flow+=flow[T];
			for(int u(T); u^S; u=g[pa[u]].u)g[pa[u]].f+=flow[T],g[pa[u]^1].f-=flow[T];
		}
		return Flow;
	}

}

Dinic 算法

Dinic 算法给人最大的印象就是比 EK 算法多了一个多路增广。同时为了实现多路增广,我们还需要给它加入当前弧优化,这个优化在欧拉路径中也有,就是为了不让 DFS 时重复访问同一条边,我们把它直接删掉之类的。

过程:单轮增广先进行一遍 BFS,再进行一遍 DFS 多路增广求阻塞流,这一过程复杂度是 \(O(|E||V|)\)

而总轮数不会超过 \(O(|V|)\),总复杂度 \(O(|V|^2|E|)\)

一张图中,任意一个连通子图中 \(|E| \ge |V| - 1\),所以我们认为 \(|E| \ge |V|\),这样来看 Dinic 算法的时间复杂度优于 EK 算法。

namespace Dinic {
	int dep[N];
	template<const int N,const int M>struct CFS {
		int tot,h[N],_h[N];
		struct edge {
			int v,c,f,nxt;
			edge(int v=0,int c=0,int f=0,int nxt=-1):v(v),c(c),f(f),nxt(nxt) {}
		} e[M];

		edge &operator [](int i) { return e[i]; }

		void Init(int n) { RCL(h+1,-1,int,n),tot=-1; }

		void Copy(int n) { CPY(_h+1,h+1,int,n); }

		void att(int u,int v,int c=0,int f=0) { e[++tot]=edge(v,c,f,h[u]),h[u]=tot; }

		void con(int u,int v,int c) { att(u,v,c),att(v,u); }

	};
	CFS<N,M<<1> g;

	void Build(int n,int m) {
		g.Init(n);
		FOR(i,1,m) {
			int u,v,c;
			cin>>u>>v>>c,g.con(u,v,c);
		}
	}

	bool BFS(int S,int T) {
		queue<int> q;
		RCL(dep+1,0,int,n),dep[S]=1,q.push(S);
		while(!q.empty()&&!dep[T]) {
			int u(q.front());
			q.pop();
			EDGE(g,i,u,v)if(g[i].f<g[i].c&&!dep[v])dep[v]=dep[u]+1,q.push(v);
		}
		return dep[T];
	}

	int DFS(int u,int T,int flow) {
		if(u==T||!flow)return flow;
		int ret(0),d(0);
		_EDGE(g,i,u,v)if(dep[v]==dep[u]+1&&(d=DFS(v,T,min(flow-ret,g[i].c-g[i].f)))) {
			g[i].f+=d,g[i^1].f-=d,ret+=d;
			if(ret==flow)return ret;
		}
		return ret;
	}

	ll Max_Flow(int n,int S,int T) {
		ll Flow(0);
		while(BFS(S,T))
			g.Copy(n),Flow+=DFS(S,T,INF);
		return Flow;
	}

}

特殊情况

对于单位容量的网络:单轮增广的时间复杂度为 \(O(|E|)\),增广轮数是 \(O(\min{(|E|^{\frac{1}{2}},|V|^{\frac{2}{3}})})\),故总复杂度为 \(O(|E|\min{(|E|^{\frac{1}{2}},|V|^{\frac{2}{3}})})\)

如果单位容量的网络满足除源汇点外每个节点 \(u\) 都满足 \(deg_{in}(u) = deg_{out}(u) = 1\),那么它的增广轮数降到 \(O(|V|^{\frac{1}{2}})\)

由此,我们可以用 Dinic 算法来求二分图最大匹配。


费用流

这部分是基于最大流算法的,故请先熟悉最大流算法的板子。

SSP 算法 & zkw 费用流

很容易想到用最短路配合 Dinic、EK 之类的算法来进行费用流的求解,其中用 Dinic 的叫做 zkw 费用流。

不过图上的反向边权会取原边的相反数,所以有负边权,不能用 Dijkstra,那么就只能用 Bellman-Ford 算法。

不过时间复杂度为 \(O(nmf)\),其中 \(f\) 为网络最大流的值。

namespace SSP {
	bool vis[N];
	int dis[N];
	template<const int N,const int M>struct CFS {
		int tot,h[N],_h[N];
		struct edge {
			int v,w,c,f,nxt;
			edge(int v=0,int w=0,int c=0,int f=0,int nxt=-1):v(v),w(w),c(c),f(f),nxt(nxt) {}
		} e[M];

		edge &operator [](int i) { return e[i]; }

		void Init(int n) { RCL(h+1,-1,int,n),tot=-1; }

		void Copy(int n) { CPY(_h+1,h+1,int,n); }

		void att(int u,int v,int w,int c=0) { e[++tot]=edge(v,w,c,0,h[u]),h[u]=tot; }

		void con(int u,int v,int w,int c) { att(u,v,w,c),att(v,u,-w); }

	};
	CFS<N,M<<1> g;

	bool SPFA(int S,int T) {
		queue<int> q;
		RCL(dis+1,INF,int,n),dis[S]=0,vis[S]=1,q.push(S);
		while(!q.empty()) {
			int u(q.front());
			q.pop(),vis[u]=0;
			EDGE(g,i,u,v)if(dis[v]>dis[u]+g[i].w&&g[i].c>g[i].f) {
				dis[v]=dis[u]+g[i].w;
				if(!vis[v])vis[v]=1,q.push(v);
			}
		}
		return dis[T]<INF;
	}

	void Build(int n,int m) {
		g.Init(n);
		FOR(i,1,m) {
			int u,v,c,w;
			cin>>u>>v>>c>>w,g.con(u,v,w,c);
		}
	}

	int DFS(int u,int T,int flow,int &cost) {
		if(u==T||!flow)return flow;
		int ret(0),d(0);
		vis[u]=true;
		_EDGE(g,i,u,v)
			if(!vis[v]&&dis[v]==dis[u]+g[i].w&&(d=DFS(v,T,min(flow-ret,g[i].c-g[i].f),cost))) {
				g[i].f+=d,g[i^1].f-=d,ret+=d,cost+=d*g[i].w;
				if(ret==flow)return vis[u]=false,ret;
			}
		return vis[u]=false,ret;
	}

	Pii MCMF(int n,int S,int T) {
		int Flow(0),Cost(0);
		while(SPFA(S,T))g.Copy(n),Flow+=DFS(S,T,INF,Cost);
		return Pii(Flow,Cost);
	}

}

Primal-Dual 原始对偶算法

不能用 Dijskstra 怎么办呢?Johnson 全源最短路径算法了解一下,用势能处理负边权,只要先跑一遍 Bellman-Ford,然后用势能在边权上做一点手脚即可。

但是如果每次都跑一遍 Bellman-Ford 的话,复杂度又退化回去了,我们可以在势能上加上 Dijkstra 跑出来的最短距离,这样就解决了。

时间复杂度 \(O(nm+m\log_2{m}f)\)

namespace PD {
	bool vis[N];
	int h[N],dis[N];
	template<const int N,const int M>struct CFS {
		int tot,h[N],_h[N];
		struct edge {
			int v,w,c,f,nxt;
			edge(int v=0,int w=0,int c=0,int f=0,int nxt=-1):v(v),w(w),c(c),f(f),nxt(nxt) {}
		} e[M];

		edge &operator [](int i) { return e[i]; }

		void Init(int n) { RCL(h+1,-1,int,n),tot=-1; }

		void Copy(int n) { CPY(_h+1,h+1,int,n); }

		void att(int u,int v,int w,int c=0) { e[++tot]=edge(v,w,c,0,h[u]),h[u]=tot; }

		void con(int u,int v,int w,int c) { att(u,v,w,c),att(v,u,-w); }

	};
	CFS<N,M<<1> g;

	void Build(int n,int m) {
		g.Init(n);
		FOR(i,1,m) {
			int u,v,c,w;
			cin>>u>>v>>c>>w,g.con(u,v,w,c);
		}
	}

	void SPFA(int S,int T) {
		queue<int> q;
		RCL(h+1,INF,int,n),h[S]=0,vis[S]=1,q.push(S);
		while(!q.empty()) {
			int u(q.front());
			q.pop(),vis[u]=0;
			EDGE(g,i,u,v)if(h[v]>h[u]+g[i].w&&g[i].c>g[i].f) {
				h[v]=h[u]+g[i].w;
				if(!vis[v])vis[v]=1,q.push(v);
			}
		}
	}

	bool Dij(int S,int T) {
		priority_queue<Pii,vector<Pii>,greater<Pii> > q;
		RCL(vis+1,false,bool,n),RCL(dis+1,INF,int,n),dis[S]=0,q.push({0,S});
		while(!q.empty()) {
			int u(q.top().Se);
			q.pop();
			if(vis[u])continue;
			vis[u]=1;
			EDGE(g,i,u,v)if(dis[v]>dis[u]+(h[u]-h[v]+g[i].w)&&g[i].c>g[i].f)
				dis[v]=dis[u]+(h[u]-h[v]+g[i].w),q.push({dis[v],v});
		}
		return dis[T]<INF;
	}

	int DFS(int u,int T,int flow,int &cost) {
		if(u==T||!flow)return flow;
		int ret(0),d(0);
		vis[u]=true;
		_EDGE(g,i,u,v)
			if(!vis[v]&&g[i].c>g[i].f&&dis[v]==dis[u]+(h[u]-h[v]+g[i].w)) {
				d=DFS(v,T,min(flow-ret,g[i].c-g[i].f),cost),g[i].f+=d,g[i^1].f-=d,ret+=d,cost+=d*g[i].w;
				if(ret==flow)return vis[u]=false,ret;
			}
		return vis[u]=false,ret;
	}

	Pii MCMF(int n,int S,int T) {
		int Flow(0),Cost(0);
		SPFA(S,T);
		while(Dij(S,T)) {
			RCL(vis+1,false,bool,n),g.Copy(n),Flow+=DFS(S,T,INF,Cost);
			FOR(i,1,n)if(dis[i]<INF)h[i]+=dis[i];
		}
		return Pii(Flow,Cost);
	}

}

模拟费用流

我们可以用费用流的思路来想贪心等算法,或者说用贪心等算法来做费用流。


题目

二分图相关

飞行员配对方案问题

明显的二分图问题,转成最大流用 Dinic 求解即可,时间复杂度懒得打了。

最小路径覆盖问题

在二分图里也有类似的题目,套路是拆点,然后源点连出点,出点连入点,入点连汇点

方格取数问题

这题先要找找性质:我们如果把相邻的点全部连起来,那么会得到一个二分图。我们左部连源点,右部连汇点,这些边的流量就是自己的权值,中间连相邻点的边流量都是无限。

最长不下降子序列问题

最小路径覆盖问题结合 DP。

  • 问题 1:DP。

  • 问题 2:建图部分拆点限制流量为 \(1\)

    然后把源点连向 DP 值为 \(1\) 的,DP 值为最大值的连向汇点,流量都为 \(1\)

    对于 \(i<j\),如果满足 \(a_i \le a_j \land f_j = f_{i} + 1\),那么 \(i\) 连向 \(j\)

  • 问题 3:把限制 \(1\)\(n\) 流量的边都改为 \(\inf\)

[国家集训队] 部落战争

最小路径覆盖问题。

[加油武汉] 疫情调查

最小路径覆盖问题 + 费用流。

这题中还有一个最短路的问题,我们拆点后再从入点向出点连一条 \((0,\inf)\) 的边即可降低复杂度。

平面图最小割

对于求解一个平面图最小割问题,我们可以把它转成对偶图最短路来解决,这也很直观。目前大多数都是直接在网格图这一类明显的且容易转换的平面图上求解,但是如果放到复杂的平面图上去,其实就不太能建出合适的图了。

[ICPC-Beijing 2006] 狼抓兔子

由于数据较水,可以直接 Dinic 求最小割解决。

[NOI2010] 海拔

这题变成了单向边,且题目有一些刻意的误导。

[CSP-S 2021] 交通规划

这道题是从部分分的平面图最小割推导到最后多个源汇点做平面图最小割配合 DP 求解的,综合性很强。

反向应用

——来自 平面图最短路与对偶图网络流 - spdarkle - 博客园 (cnblogs.com)

题目描述

给定一张有边权的 \(n\)\(m\) 列的网格图,现在每花费 \(1\) 的代价可以将任意一条边的边权 \(+1\),问:第一行的点到最后一行的点的最短路增加 \(k\) 最少要花费多少代价?

分析

先求出原图最短路长度为 \(d\)

发现这个图与平面图最小割转换过来的时候非常相似,那我们尝试把这张网格图也转成对偶图,然后发现边权 \(+1\) 就是一条边流量扩容 \(1\),那我们给每条边都建一条容量无限,代价 \(1\) 的副边,再限制最大流为 \(d+k\),然后跑费用流即可。

最大权闭合子图

源点连正点,负点连汇点,答案是正点总和减去最小割。

[NOI2009] 植物大战僵尸

拓扑排序去掉不可能选的点即可。

建模及技巧

涉及拆点等建模技巧。

教辅的组成

这一题主要考察的套路是拆点限制过某个点的流量

[SCOI2007] 蜥蜴

这题建图要略费点心思。

  • 每个石柱要先拆点限制流量。
  • 然后算出连边的点。
  • 源点连本来就有蜥蜴的点,流量为 \(1\)
  • 汇点连能够跳到图外的点,流量无限。

[ARC176E] Max Vector

图论最小割模型。

主主树

非常简单的最大流。源汇点连中间点的流量代表生命,中间点之间的流量代表攻击。

[ZJOI2009] 狼和羊的故事

明显的最小割,建图也很简单。

[国家集训队] happiness(文理分科)

这题用一个额外建的点来表示某些点一起选会有多少值

[SDOI2016] 墙上的句子

这题用一个额外建的点来表示某些点一定会一起出现

[清华集训 2012] 最小生成树 & [SHOI2010] 最小生成树

注意两题中“可能”和“一定”的区别。

要让某条边一定在最小生成树中出现,就不能让这两端点在加入边之前就连通,那么我们把边权小于它的边拿出来做最小割即可(如果换成“可能”就是小于等于)。

最大生成树同理。

[SHOI2007] 善意的投票 / [JLOI2010] 冠军调查

源点连 \(0\)\(1\) 连汇点,中间有边也连上,所有流量皆为 \(1\)

负载平衡问题

费用流。

餐巾计划问题

费用流,需要把每天拆成早晚。

[CQOI2012] 交换棋子

较难的费用流,对于中间节点交换要有所考虑。

posted @ 2025-04-03 14:16  Add_Catalyst  阅读(11)  评论(0)    收藏  举报