网络流

由于笔者写这篇博客时比现在还要菜得多,容易发现其中出现了较多的事实性错误,之后可能会找时间修一遍。

关于学习途径

显然有无数人在自学网络流的时候因为网上大部分题解的姿势都过于抽象而被劝退,所以提一下。可作参考。

首先不建议看我写的这个。

入门网络流的话建议看某 18 年日报,虽然没有具体的复杂度之类的证明,但是确实可以包你看懂,至少理解最大流算法什么的基本思想是够了。

然后讲解最严谨知识点最全面例题最多的一篇应该是 Alex_Wei 神仙的,但是阅读起来不太友好。


定义

大概包含我在内的不少人最开始就是被网络流的巨大丑定义劝退的。

比较优雅地说就是:

  • 我们把城市抽象成一张有向图。
  • 图上有很多很多的节点,其中有一个源点 s 和一个汇点 t。(后面会出现无源汇)
  • 把连接每个点的水管抽象为有向边,它们都有一个容量 c(i,j)
  • 假设整座城市只有你一个无业游民,你在汇点等着喝水。
  • 现在水厂在源点放水,水会顺着边的方向不停的流,一路流向你所在的汇点。
  • 一条边的流量 f(i,j) 就是这条边里流了多少水,而且这条边的流量不能超过容量,不然它就爆了。有 f(i,j)=f(j,i)
  • 除了源汇点之外的每个点都不会漏水,所以流进一个点的流量流出这个点的流量是相等的。
  • 水厂不想浪费水,需要让流出来的所有水都进你嘴里,于是规定流出源点和流进汇点的流量相等,那么这个流进汇点的流量就叫整张网络的流量

其它的一些定义和性质要用再提。


网络最大流

你想在汇点喝到尽量多的水,那你就需要通过规划每条边里流多少水来使这个网络的流量最大。现在我们需要一些算法来求出这个最大流量。

因为有着容量限制,这个问题并没有那么简单。

EK

  • 增广路:从源点到汇点的一条使得当前流量可以增加的路径。
  • 剩余流量:一条边的容量减去流量,即还能流多少水。
  • 残量网络:剩余流量不为 0 的边组成的网络。

EK 就是直接通过 bfs 不停地找增广路,直到实在找不到了(即源汇点在残量网络上不连通)为止。bfs 时需要跳过那些已经被流满的边

先不管它看上去对不对哈,考虑一下这玩意怎么实现。

显然每次找到的增广路的流量就是这条路径上的边的剩余流量的最小值,那么我们每次就把增广路上的边的剩余流量减去整条增广路的流量,不断重复这个过程,直至没有任何不经过剩余流量为 0 的边到达汇点的路径,即没有增广路了,那么求出来的增广路流量之和就是答案。

然而用脖子都能想到这么直接做有很大概率出来的不是最大流,所以我们要做一点处理,使得 EK 在跑的过程中可以不断修正流方案。

也就是建反边。我们每次确定一条增广路,就使路径上的所有边的反边容量加上目前这条增广路的流量,表示这条边上的流量有多少是可以被撤销的,显然初值是 0

这样当我们以后再走到这里时,从反边走过来,就可以看作是把之前从正边流过去的流量匀回来,就当作没流过这条边(或者是没流过那么多),同样进行容量加减处理。这样就达到了撤销效果。因为每次增广都会使流量增加,不存在一条边的流量被反复撤销导致死循环的情况。

然后关于怎么建反边:设正边编号为 2i,反边编号为 2i+1,那么正边编号异或一下 1 就是反边了,在建图的时候把编号带进边里,其它信息放在外面即可。

于是跑就完啦。这个撤销机制相当有用,后面网络流的很多特性都是基于这个的。

然后因为使用了 bfs,且在增广过程中流量不断递增,于是时间复杂度理论上是 O(nm2)(也就是说如果你随便乱找增广路的话复杂度是假的……)。不过很明显这玩意很难跑满,如果不是出题人故意卡你的话理论上点数在 103104 规模的网络它都能跑。

然后你就可以愉快地 AC 掉 P3376P2740 了。

const ll I=1e18,N=207,W=1e4+7;
ll n,m,s,t,ans;
ll u[W],v[W],w[W],val[W];
ll vis[N],pth[N],lst[N];
vector<pll > e[N];
bool bfs() {
	memset(vis,0,sizeof(vis));
	queue<ll> q;
	q.push(s),vis[s]=1;
	while (!q.empty()) {
		ll p=q.front();
		q.pop();
		if (p==t) return 1;
		for (pll i:e[p]) if (!vis[i.first]&&val[i.second])
			vis[i.first]=1,pth[i.first]=i.second,lst[i.first]=p,q.push(i.first);
	}
	return 0;
}
void EK() {
	while (bfs()) {
		ll res=I;
		for (ll i=t;i!=s;i=lst[i]) res=min(res,val[pth[i]]);
		for (ll i=t;i!=s;i=lst[i]) val[pth[i]]-=res,val[pth[i]^1]+=res;
		ans+=res;
	}
}
int main() {
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	for (ll i=1;i<=m;i++)
		scanf("%lld%lld%lld",&u[i],&v[i],&w[i]),val[i*2]=w[i],
		e[u[i]].pb({v[i],i*2}),e[v[i]].pb({u[i],i*2+1});
	EK();
	cout<<ans;
	return 0;
}

Dinic

但是感觉 EK 不够快啊,看看有没有可以优化的地方。

回顾 EK 的过程,发现它每次 bfs 居然只能找出一条增广路,那不在增广路上的信息是不是白白浪费了啊,有点可惜,考虑把它们利用起来,一次多求几条增广路。

首先我们跑一遍 bfs。设在当前残量网络上,源点到每个点的距离为 disi。EK 里决定当前状态一个点是否可能流向另一个点的条件是在去掉已经流满的边的情况下,另一个点的 disj 是否等于这个点的 disi+1,即是否可能在一条 bfs 路径上。而这些距离信息是一次 bfs 就可以知道的。

那我们是不是可以直接知道在当前的 bfs 条件下,有哪些点之间是可能有流量的,然后一次性榨干目前状态下的所有增广路呢?

所以知道了每个点的 disi 之后,我们可以再用几次从源点开始的 dfs 把增广路找出来。在 dfs 的过程中,我们找到一条边 uv,只要满足 disv=disu+1,那么这条边就有可能在增广路上,可以走。那么几次 dfs 就可以找出目前能走的所有的增广路。这样一次 bfs 就能找出巨大多的增广路,实在是美哉!

这时还能流水的边可能会减少,于是再跑一遍 bfs 求出找完增广路的网络的深度信息,再 dfs……不断重复以上过程直到 dfs 找不出增广路为止。

于是我们学会了 Dinic。

  • 但它比 EK 难写诶,而且这又 bfs 又 dfs 的,真的能比 EK 快吗?

还真不一定。

  • 那它有啥用啊。

但是它能优化啊。

  • 多路增广

发现一次 dfs 只能找出一条增广路好像跟 EK 没有什么差别,不如用一次 dfs 全部找出来,因为 Dinic 使你可以确定当前所有可以流的边。

也就是在 dfs 的过程中记目前这个点还有多少流量能用,回来的时候不用从头再找,继续用剩下的流量从这个点开始走去找即可。

这么搞完复杂度没有变化,是个常数优化。

  • 当前弧优化

发现我们很可能大量地重复遍历了已经流满的路径。这个流满指的并不是一个点的出边被流满,而是从这个出边流出去会在后面被阻断,没法再流到汇点了。这个你无法快速判断,很不好。

发现由于 dfs 找增广路的特性,如果走完了一条边然后出来,之后如果再从这条边过去(注意正向反向边这里不看成同一条),是不会对流量产生任何贡献的。所以可以对每个点记录一下上次走了哪条边,下一次 dfs 到这直接接着记录的边再搜

用链式前向星就记上次的边的编号,每次重新开始就把它当做 head 搞。

用 vector 就记上次的边的下标,下次从它开始。

注意这里不是跳过当前弧,因为当前弧上的边有可能还没流满,还能再流,是从当前弧开始继续搜。

(所以当前弧这个名字应该指的就是目前搜到的从源点开始的路径吧?)

在这个优化的加持下,Dinic 的效率直接起飞。理论大概是 O(n2m),在稠密图上可以吊打 EK。而且在正解是网络流的题里它很少被卡,能跑 104105 规模的网络。然而不加这俩优化或者优化写错就可能跑得还没 EK 快,需要注意。

其实有些东西就不需要在 bfs 的时候记了,没难写多少。个人认为它比 EK 优雅。

ll n,m,s,t;
ll u[W],v[W],w[W],val[W];
ll dis[N],cur[N];
vector<pll > e[N];
bool bfs() {
	memset(dis,-1,sizeof(dis)),memset(cur,0,sizeof(cur));
	queue<ll> q;
	q.push(s),dis[s]=0;
	while (!q.empty()) {
		ll p=q.front();
		q.pop();
		if (p==t) return 1;
		for (pll i:e[p]) if (dis[i.first]==-1&&val[i.second])
			dis[i.first]=dis[p]+1,q.push(i.first);
	}
	return 0;
}
ll dfs(ll p,ll fl) {
	if (p==t) return fl;
	ll pf=0;
	for (ll ii=cur[p];ii<e[p].size();ii++) { pll i=e[p][ii];
		cur[p]=ii;
		if (val[i.second]&&dis[i.first]==dis[p]+1) {
			ll nf=dfs(i.first,min(fl-pf,val[i.second]));
			if (nf) {
				pf+=nf,val[i.second]-=nf,val[i.second^1]+=nf;
				if (pf==fl) break;
			}
		}
	}
	return pf;
}
ll din() {
	ll res=0;
	while (bfs()) res+=dfs(s,J);
	return res;
}
void mian() {
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	for (ll i=1;i<=m;i++)
		scanf("%lld%lld%lld",&u[i],&v[i],&w[i]),val[i*2]=w[i],
		e[u[i]].pb({v[i],i*2}),e[v[i]].pb({u[i],i*2+1});
	cout<<din();
}

其它的玩意

好像还有更快的 ISAP 什么的东西,有机会再写。

封装

考虑到网络流这玩意一般就单纯用来建个模跑个最大流费用流啥的然后就扔了,单独弄一堆数组什么的去开一遍似乎不太优雅,所以个人认为封装一下用起来会舒服很多。

namespace D {
	const ll N=2e5+7,M=6e5+7;
	ll n,m,s,t,mxf;
	ll f[M<<1];
	ll cur[N],dis[N];
	vector<pll> e[N];
	void ini(ll _n) {
		n=_n,m=1,mxf=0;
		for (ll i=0;i<=n;i++) vector<pll>().swap(e[i]);
	}
	void add(ll x,ll y,ll z) { f[++m]=z,e[x].pb({y,m}),f[++m]=0,e[y].pb({x,m}); }
	bool bfs() {
		for (ll i=0;i<=n;i++) dis[i]=J,cur[i]=0;
		queue<ll> q;
		q.push(s),dis[s]=0;
		while (!q.empty()) {
			ll p=q.front();
			q.pop();
			if (p==t) return 1;
			for (pll i:e[p]) if (f[i.se]&&dis[i.fi]>dis[p]+1)
				dis[i.fi]=dis[p]+1,q.push(i.fi);
		}
		return 0;
	}
	ll dfs(ll p,ll fl) {
		if (p==t) return fl;
		ll nf=0;
		for (ll ii=cur[p];ii<e[p].size();ii++) { pll i=e[p][ii];
			cur[p]=ii;
			if (f[i.se]&&dis[i.fi]==dis[p]+1) {
				ll cf=dfs(i.fi,min(fl-nf,f[i.se]));
				nf+=cf,f[i.se]-=cf,f[i.se^1]+=cf;
				if (fl==nf) return fl;
			}
		}
		return nf;
	}
	void din(ll _s=0,ll _t=n) { s=_s,t=_t; while (bfs()) mxf+=dfs(s,J); }
}

基础应用

有个叫网络流 24 题的东西。应该包含了网络流的一些基础题目。

里面的题要用再讲。

P2756 飞行员配对方案问题

这玩意一眼二分图最大匹配,实际上也是可以用网络流做的。

如果源点向每个左部点,每个右部点向汇点连一条容量为 1 的边,然后每条原图中的边容量也设为 1,那么跑一遍最大流就是答案了。求方案就枚举每条左部点和右部点之间的连边,看它们的流量(反向边容量)。

至于这种神秘的建图方法都是怎么想到的:或许是这种“每个人只能选一种配对方案”的限制让发明者联想到了网络流的容量限制吧……

用 Dinic 跑二分图匹配会比匈牙利快得多,是 O(mn) 的,实际还是跑不满。

P3254 圆桌问题

二分图多重匹配问题,即一个点可以连若干条边。把源点与左部点,右部点与汇点的容量调整一下,表示能连的边数就可以了。输出方案还是一样。

P2763 试题库问题

也差不多。源点向每个题连容量为 1 的边,每个题向对应的类型连容量为 1 的边,每个类型向汇点连容量为需要的题数的边。


最小费用最大流

EK & Dinic

看我们用这么一堆管子居然获得了如此之大的流量,黑心水厂不开心了,于是规定:第 i 条管子里每流一单位水就要向他们支付 ci 的费用。

作为一位心系人民的 OIer,你肯定要坚持底线,不让流量变动丝毫。但是为了节省经费,你需要在流量不变的前提下找到最小费用。这就是最小费用最大流问题。

其实相当简单啊。我们只要保证在目前的状态下,所有水都流在从源点出发的关于单位水的费用的最短路上,等到有管道的容量被榨干再重新求一遍最短路,反复执行下去直到找不到增广路为止。这样就能保证费用最小。

所以只要把 bfs 换成 spfa 即可。

  • Dijkstra 不行吗?

注意这里反向边的花费是正向边的相反数,相当于走回去就督促甲方退钱,所以 Dijkstra 跑不了。不过好像确实有一个叫 Primal-Dual 的东西可以做到 Dijkstra 求解最小费用最大流,但是一般也没哪个缺德的出题人会在网络流里卡 SPFA……

还真有人卡过。后面会写。

  • 费用为负的话不会有负环吗?

如果原图没有负环,网络流在 spfa 过程中也就不会搜出负环,顶多搜出来一堆 0 环,不至于出问题。但是如果原图有负环的话就寄了,后面也会写。

对于 EK,增广路就是每次 spfa 搜到的源点到汇点的路径

对于 Dinic,一条增广路合法仅当路径上所有相邻的点 u,v 均满足 disu+du,v=disv,其中 disi 表示源点到 i 的最短路,di,j 表示两点间边的长度。就是最大流改一下。但是 Dinic 有个坑啊,这么写的话遇到费用为 0 的边时,它会去死,所以要再开个标记记一下这个点是否被 dfs 过,回溯时记得撤销,否则会导致时间复杂度不太对。

于是你就可以愉快地过掉 P3381 了。

EK:

ll n,m,s,t,ans1,ans2;
ll u[W],v[W],w[W],c[W],val[W],cs[W];
ll vis[N],pth[N],lst[N],dis[N];
vector<pll > e[N];
bool spfa() {
	memset(vis,0,sizeof(vis)),memset(dis,45,sizeof(dis));
	queue<ll> q;
	q.push(s),vis[s]=1,dis[s]=0; 
	while (!q.empty()) {
		ll p=q.front();
		q.pop();
		vis[p]=0;
		for (pll i:e[p]) if (dis[i.first]>dis[p]+cs[i.second]&&val[i.second]) {
			dis[i.first]=dis[p]+cs[i.second];
			pth[i.first]=i.second,lst[i.first]=p;
			if (!vis[i.first]) vis[i.first]=1,q.push(i.first);
		}
	}
	return dis[t]<I;
}
void EK() {
	while (spfa()) {
		ll res=I;
		for (ll i=t;i!=s;i=lst[i]) res=min(res,val[pth[i]]);
		for (ll i=t;i!=s;i=lst[i]) val[pth[i]]-=res,val[pth[i]^1]+=res;
		ans1+=res,ans2+=res*dis[t];
	}
}
int main() {
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	for (ll i=1;i<=m;i++)
		scanf("%lld%lld%lld%lld",&u[i],&v[i],&w[i],&c[i]),
		val[i*2]=w[i],cs[i*2]=c[i],cs[i*2+1]=-c[i],
		e[u[i]].pb({v[i],i*2}),e[v[i]].pb({u[i],i*2+1});
	EK();
	cout<<ans1<<" "<<ans2;
	return 0;
}

Dinic:

namespace D {
	const ll N=5e3+7,M=5e4+7;
	ll n,m,s,t,mxf,mxc;
	ll f[M<<1],c[M<<1];
	ll cur[N],vis[N],dis[N];
	vector<pll> e[N];
	void ini(ll _n) {
		n=_n,m=1,mxf=mxc=0;
		for (ll i=0;i<=n;i++) vector<pll>().swap(e[i]);
	}
	void add(ll x,ll y,ll z,ll w) {
		e[x].pb({y,++m}),f[m]=z,c[m]=w;
		e[y].pb({x,++m}),f[m]=0,c[m]=-w;
	}
	bool spf() {
		for (ll i=0;i<=n;i++) dis[i]=J,cur[i]=vis[i]=0;
		queue<ll> q;
		q.push(s),dis[s]=0;
		while (!q.empty()) {
			ll p=q.front();
			q.pop(),vis[p]=0;
			for (pll i:e[p]) {
				if (f[i.se]&&dis[i.fi]>dis[p]+c[i.se]) {
					dis[i.fi]=dis[p]+c[i.se];
					if (!vis[i.fi]) vis[i.fi]=1,q.push(i.fi);
				}
			}
		}
		return dis[t]<J;
	}
	ll dfs(ll p,ll fl) {
		if (p==t) return fl;
		vis[p]=2;
		ll nf=0;
		for (ll ii=cur[p];ii<e[p].size();ii++) { pll i=e[p][ii];
			cur[p]=ii;
			if (vis[i.fi]<2&&f[i.se]&&dis[i.fi]==dis[p]+c[i.se]) {
				ll cf=dfs(i.fi,min(fl-nf,f[i.se]));
				nf+=cf,mxc+=c[i.se]*cf,f[i.se]-=cf,f[i.se^1]+=cf;
				if (fl==nf) return vis[p]=0,fl;
			}
		}
		return vis[p]=0,nf;
	}
	void din(ll _s=0,ll _t=n) { s=_s,t=_t,mxc=mxf=0; while (spf()) mxf+=dfs(s,J); }
}

最大费用最大流

反过来变成最小费用。或者直接把 SPFA 换成最长路。如果有环也得做后面讲的负环处理。

Dijkstra 费用流

就是上面提到的 Primal-Dual。

前置知识:Johnson 全源最短路

就是通过一些神秘的技巧把负权边搞掉然后跑 n 次 Dijkstra。

具体地,建个超级源点,然后从它向所有点连一条边权为 0 的单向边,再以它为起点跑一次 SPFA(Bellman-Ford) 得到每个点的最短路 hi。那么你再把原图中的每个 (u,v) 的边权 w 替换为 w+huhv 就可以跑 Dijkstra,跑完记得把答案替换回来。

简单解释一下:其实这扯到了一个叫势能法的东西。叫势能是因为势能只跟起点和终点有关,类比一下这个性质。你可以发现不管从起点到终点走的是哪条路径,hsht 的值都不变。那么推一下式子就可以发现替换完之后边权确实没有负的。同样用到了势能法的题有 P1861 星之器

  • 诶那我就可以用 Dijkstra 求负权图最短路了,那 SPFA 不就彻底死了吗。

你看看你预处理跑了个什么东西。

Primal-Dual

首先你正常地从源点出发求一遍所有点的 hi(因为求费用流只从源点出发跑最短路,所以不用建超源连过去都跑一遍),把边权替换一下。主要问题在于每次增广完之后残量网络的形态会发生变化,同时 hi 也会变。显然你不能再求一次,考虑怎么快速处理这个变化。

直接 hihi+disi 即可,disi 是这次 Dijkstra 跑出来的替换过的最短路。

挺显然的。这时你的 disi 其实是最短路加上 hshi=hi,再跟前面的 hi 加一下抵消掉,剩下那个 disi 正好就是从源点出发的势能。回收下次利用即可。

namespace din {
	const ll N=5e3+7,M=1e5+7;
	ll n,m,s,t,mxf,mxc;
	ll dis[N],vis[N],h[N];
	ll val[M],cs[M];
	ll cur[N];
	vector<pll > e[N];
	void joh() {
		memset(h,20,sizeof(h));
		queue<ll> q;
		q.push(s),h[s]=0,vis[s]=1;
		while (!q.empty()) {
			ll p=q.front(); q.pop(),vis[p]=0;
			for (pll i:e[p]) if (val[i.se]&&h[i.fi]>h[p]+cs[i.se]) {
				h[i.fi]=h[p]+cs[i.se];
				if (!vis[i.fi]) q.push(i.fi),vis[i.fi]=1;
			}
		}
	}
	bool dij() {
		memset(dis,20,sizeof(dis)),memset(vis,0,sizeof(vis)),memset(cur,0,sizeof(cur));
		priority_queue<pll > q;
		q.push({dis[s]=0,s});
		while (!q.empty()) {
			ll p=q.top().se; q.pop();
			if (vis[p]) continue; vis[p]=1;
			for (pll i:e[p]) if (val[i.se]&&dis[i.fi]>dis[p]+cs[i.se]+h[p]-h[i.fi])
				q.push({-(dis[i.fi]=dis[p]+cs[i.se]+h[p]-h[i.fi]),i.fi});
		}
		return dis[t]<J;
	}
	ll dfs(ll p,ll fl) {
		if (p==t) return fl;
		vis[p]=2;
		ll pf=0;
		for (ll ii=cur[p];ii<e[p].size();ii++) { pll i=e[p][ii];
			cur[p]=ii;
			if (vis[i.fi]!=2&&val[i.se]&&dis[i.fi]==dis[p]+cs[i.se]+h[p]-h[i.fi]) {
				ll nf=dfs(i.fi,min(fl-pf,val[i.se]));
				if (nf) {
					pf+=nf,val[i.se]-=nf,val[i.se^1]+=nf;
					if (pf==fl) break;
				}
			}
		}
		vis[p]=0;
		return pf;
	}
	void mian() {
		joh();
		for (ll tmp=0;dij();) {
			tmp=dfs(s,J);
			for (ll i=1;i<=n;i++) h[i]+=dis[i];
			mxf+=tmp,mxc+=tmp*h[t];
		}
	}
	void ini(ll _n,ll _s,ll _t) { n=_n,s=_s,t=_t,m=1,mxf=mxc=0; }
	void add(ll x,ll y,ll z,ll w) {
		val[++m]=z,cs[m]=w,e[x].pb({y,m});
		val[++m]=0,cs[m]=-w,e[y].pb({x,m});
	}
}

有负环费用流

在后面上下界网络流那一块。

基础应用

通常费用流的用法是,用最大流满足题目限制,用最小费用满足答案最优。

P4014 分配问题

二分图最大匹配,但是带权。把之前最大流求二分图最大匹配中,左部点和右部点之间连的边带个费用即可。P4015 就是把容量改一下。

P4016 负载平衡问题

把货物搬运看成流,源点向每个点连容量为 ai 费用为 0 的边,每个点向相邻的两个点连容量为 费用为 1 的边,每个点向汇点连容量为 ain 费用为 0 的边。

……


最小割

水厂的黑心甲方们不高兴了,于是要阻止我们喝水。他们需要通过割断一些水管来使水无法从源点流到汇点。但是水厂的经费也有限,所以他们要使割掉的水管容量和最小。这个容量和叫最小割。

  • 最大流最小割定理:最大流等于最小割

口胡一下。

  • 所有割都不小于最大流:

给一种感性理解。

每割掉一条边最多只会使整张网路的流量减少这条边的容量,即极限情况是这条边被流满了,而且没有其它的边还可流。显然你割这里不可能会使其它地方的流量变少。

那割到最后,割掉的边容量之和肯定不会还没最大流大。

  • 存在一组割等于最大流:

反证,假设达到最大流的时候残量网络还连通,那么还可以继续增广,所以这时求出的不是最大流,矛盾。

回想一下,之前 EK 和 Dinic 判断是否达到最大流的条件就是源汇点是否在残量网络上连通,即还有没有增广路。

求方案

可以根据上面的证明感性理解,被割掉的边一定是被流满的。注意被流满的边不一定被割掉,如果有边权相同的就寄了。

所以从源点出发搜一遍,遇到流满的边就计入答案然后转头就跑,否则走过去继续搜。

基础应用

难绷的是最小割好像没什么简单应用题,就随便拿个凑个数吧。

Gym102341B Bulbasaur

给定一张 n 层每层 k 个点的分层图,每条边一定从 i 层连到 i+1 层。定义 f(i,j) 为选择若干条从第 i 层到第 j 层的路径,并且每条路径上的点和边都不相交,最多选的路径数,求 i=1nj=i+1nf(i,j)

2n4×1041k9,6s,512MB。

发现 f(i,j) 就是每条边容量为 1i 层到 j 层的最大流。最大流不好搞,转成最小割。理论上到这就能做了,但是还能加一步,根据容量为 1 特殊性质转成最小割点数,即最少删多少个点才能使 i,j 两层不连通。

好,网络流部分结束了。后面状压略过,还需要一个状态设计优化。


上下界网络流

见黑心水厂这么黑心,我们果断放弃,换了一个良心水厂。现在我们可以喝水了。

然而良心水厂也有一个要求:如果一条水管里几乎不流水,那么这会使得建筑方觉得它们辛辛苦苦打下的管子白费了,良心水厂觉得这不好,于是要求第 i 条水管至少要流 bi 的水。

然后就可以引出一大堆问题。

无源汇上下界可行流

  • ?我是谁?我在哪?水厂又在哪?

也就是说水厂在水管里放了一点违反了能量守恒定律的水就跑了,这些水在管子里不停的流,也就是说每个点都需要满足流进的水量等于流出的水量,即流守恒。我们需要规划一种流的方案使得它符合水厂的要求,咋办?

网络流算法是基于反悔的,也就是说只要是个合法网络,不管流没流过它都能跑。但前提是它必须是个合法的符合流量守恒的网络。

首先我们钦定每条边的下限都被流满了,然后将新的流量设为上限减去下限,再去考虑咋流。主要问题在于流满下限之后,每个点的流守恒很可能当场就没了,所以我们要做一些处理。

考虑把流不守恒的情况转化掉。虚空建一个超级源点和一个超级汇点,然后对每个点连边。如果一个点多了水就把漏的水量连到汇点上,少了水就让源点连过来负责加对应的水量,正好守恒就不管它。

这个时候再对这张每条边容量已经更换为上界减去下界,而且加上了超源超汇的图跑最大流。如果从源点出发的边存在没流满的,那说明不守恒的问题无法解决(即还没流满就增广不了了),不存在可行流;否则目前原图中的边流的方案即是一组可行流。

有源汇上下界可行流

水厂突然回来放水了。

相比无源汇的变化在于源点和汇点可以不满足流守恒,那就从原图中汇点到源点连一条容量为 的边(不是源点到汇点),把多出的流量匀回来,强制让它变成变成一个无源汇,再按上面的方法跑即可。

流量不是跑出来的最大流,应该是汇点到源点的无限边里的流量(或者反边的容量)。

有源汇上下界最大流

先跑一遍可行流,然后在跑完可行流后,撤掉超级源汇点和无限容量边,从源点到汇点再跑出一个最大流即可。答案是可行流量加上从源点到汇点的新最大流。

换句话说,加上超级源汇和无限边后用超源超汇跑一遍最大流,检查一下有没有流满然后把它们撤掉用原源原汇再跑一次最大流,两次加起来。

这时因为容量已经更换为上界减去下界,我们又用可行流提前规划好了一种可行的流法,那么最大流在上面怎么反悔都不会打破限制,还可以帮你完成一切的撤销工作,确保是最大流。

(Loj116)
namespace D {
	const int N=207,M=2e4+7;
	ll n,m,s,t,mxf;
	ll f[M<<1];
	ll cur[N],dis[N],flw[N];
	vector<pll> e[N];
	bool bfs() {
		memset(dis,20,sizeof(dis)),memset(cur,0,sizeof(cur));
		queue<ll> q;
		q.push(s),dis[s]=0;
		while (!q.empty()) {
			ll p=q.front();
			q.pop();
			if (p==t) return 1;
			for (pll i:e[p]) if (f[i.se]&&dis[i.fi]>dis[p]+1)
				dis[i.fi]=dis[p]+1,q.push(i.fi);
		}
		return 0;
	}
	ll dfs(ll p,ll fl) {
		if (p==t) return fl;
		ll nf=0;
		for (ll ii=cur[p];ii<e[p].size();ii++) { pll i=e[p][ii];
			cur[p]=ii;
			if (f[i.se]&&dis[i.fi]==dis[p]+1) {
				ll cf=dfs(i.fi,min(fl-nf,f[i.se]));
				nf+=cf,f[i.se]-=cf,f[i.se^1]+=cf;
				if (fl==nf) return fl;
			}
		}
		return nf;
	}
	void din() { while (bfs()) mxf+=dfs(s,J); }
	void ini(ll _n,ll _s,ll _t) {
		n=_n,s=_s,t=_t,m=1,mxf=0,memset(flw,0,sizeof(flw));
		for (ll i=0;i<=n+2;i++) e[i].clear();
	}
	void add(ll x,ll y,ll z) { f[++m]=z,e[x].pb({y,m}),f[++m]=0,e[y].pb({x,m}); }
	void add(ll x,ll y,ll a,ll b) { add(x,y,b-a),flw[x]-=a,flw[y]+=a; }
	void mian(ll _s,ll _t) {
		s=++n,t=++n;
		for (ll i=0;i<=n-2;i++) {
			if (flw[i]>0) add(s,i,flw[i]);
			else if (flw[i]<0) add(i,t,-flw[i]);
		}
		add(_t,_s,J),din();
		for (pll i:e[s]) if (f[i.se]) return mxf=-1,void();
		mxf=f[m],f[m-1]=f[m]=0,s=_s,t=_t,din();
	}
}
ll n,m,s,t;
void mian() {
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	D::ini(n,s,t);
	for (ll i=1,x,y,c,d;i<=m;i++) scanf("%lld%lld%lld%lld",&x,&y,&c,&d),D::add(x,y,c,d);
	D::mian(s,t);
	if (~D::mxf) cout<<D::mxf<<"\n";
	else cout<<"please go home to sleep";
}

有源汇上下界最小流

  • 最小流不是 0 吗?……哦。

最大流的从源点到汇点再跑一次最大流是看还有什么流量还可以加。

那我们是不是从汇点到源点跑一次看还有什么流量可以减就行了?

所以就是可行流量减去从汇点到源点的新最大流

这里有一个定义问题:在某些神秘情况中,你可能会求出最小流是负的。其实根据网络流的形式化定义,源点的流量为流出流量减去流入流量。实际上流出流量小于流入流量也是可行的。但是题目限制可能会让这种情况不合法,所以需要针对各个题目进行修改。感觉这或许也是最小流的题好像没几个的原因……?

有源汇上下界费用流

一样地换成 spfa 啦。注意一开始找可行流要的费用也得加上。

基础应用

P7173 【模板】有负圈的费用流

你会发现你的 SPFA 或者 Dijkstra 跑着跑着就死了,所以我们要开点外挂。

考虑直接把网上的负边干掉!

还是一样,跑费用流之前先把每条负费用边流满是没有关系的:把它们的反边容量一样进行增加费用是原边的相反数。然后图上就没有负边了,要改就等着后面流过来的时候反悔掉。

然后为了保住流量守恒,所以用回有源汇上下界网络流的技术,建超源超汇和无限边,少流量了用超源补,多流量了补给超汇,确保没有负权边后先跑一遍费用流。让网络暂时变得流量守恒,接下来把超源超汇撤掉,一切交给第二次费用流。

注意一开始处理满流的时候依然要计算费用。

P5192 Zoj3229 Shoot the Bullet|东方文花帖|【模板】有源汇上下界最大流

建模是好想的。

  • 照片是流。
  • 源点向每个少女连容量为 [Gx,inf] 的边。
  • 每天可以拍的少女向这一天连容量为 [Lki,Rki] 的边。
  • 每天向汇点连容量为 [0,Di] 的边。

跑一遍上下界。

……


网络流的那些 trick

下面是一些网络流的常用模型。

值得注意的是,虽然这些模型用得多,但是大多数情况下是无法直接套用的,通常需要对这些模型有着深刻理解,并充分发挥人类智慧进行魔改才能做。

拆点大法

网络流针对的是边。如果有时建出的图对点里的流量/费用也有限制要怎么办呢?

把点拆成入点和出点,入点和出点中间连我们需要的流量/费用即可。

当然有时候限制的可能不是流量费用而是流的方式,也许可以多拆几个入点出点解决。

P4013 数字梯形问题

路径看成流,答案看成费用,尝试最大费用最大流解决。

把每个数字拆成入点和出点,连接出入点的边的容量和费用分别设置为每一问需要的数值即可。

P1251 餐巾计划问题

你发现瓶颈大部分在于怎么区分干净毛巾和脏毛巾。那就把每天拆成早上和晚上,流出早上的是干净毛巾,流进晚上的是脏毛巾。那这些干净毛巾给谁,脏毛巾从哪来呢?你发现还有源汇点没有用,于是接上去就好了。

具体的,以下是离谱的建模方法:

  • 流即为毛巾。
  • 源点提供各种毛巾,汇点收集干净毛巾然后变成脏毛巾丢给源点,由于流量守恒,这样不会有毛巾被弄丢,可以理解成是毛巾通过汇点流回了源点。
  • 每天拆成早上和晚上两个点,以区分干净和脏毛巾。
  • 规定每条毛巾在洗好之后立刻开始用,如果要晚点用就等完再洗。
  • 提供所需干净毛巾:每天早上向汇点连边,容量为当天 ri,费用为 0
  • 生产脏毛巾:源点向每天晚上连边,容量为当天 ri,费用为 0
  • 留着脏毛巾之后再处理:每天晚上向下一个晚上连边,容量为 inf,费用为 0
  • 买毛巾:源点向每一天早上连边,容量为 inf,费用为 p
  • 慢洗部:每天晚上向 m 天后的早上连边,容量为 inf,费用为 f
  • 快洗部:每天晚上向 n 天后的早上连边,容量为 inf,费用为 s
  • 由于只有前两类边有容量限制,所以跑最大流肯定能把这两类边全部流满,也就是保证每天的毛巾都够用。在此基础上再去保证费用最小。
  • 也就是在这个网络上跑费用流。

最小费用满足费用最小,最大流满足每天毛巾够用了。这个建模手法感觉还挺人类智慧的,膜拜发明者一秒。利用流量守恒表示物品个数不变应该算个套路罢。

最小路径覆盖

一张 DAG,请你用若干条没有公共点的简单路径覆盖上面的所有点。求最小路径数。

你或许可以每个点跟源点和汇点连容量为 [0,1] 的边,再拆点然后在入点和出点之间连容量为 [1,1] 的边然后上下界做它,理论上有环的一般图也能搞。但是它太慢了。

根据点做看起来没什么前途了,考虑根据边做。你发现路径数最少其实就等价于没被覆盖的边数最少,因为每条路径覆盖的边数就是点数 1。而在 DAG 上,每条边被覆盖,要求它连接的两个点没有被其它出边/入边覆盖。

你想起了二分图匹配。每条边 (ui,vi) 从左部点的 ui 连向右部点的 vi,那么相当于每个点被拆成出入点,左部点代表出点,右部点代表入点,每个出点/入点只能被一条边占用。跑一遍的复杂度是二分图匹配的 O(mn)

很明显在一般图上这么做的话它会把环算进去,那么边数最少就不等价于路径最少了,因为会多出一条,可能另有他用。

P2764 最小路径覆盖问题

模板。输出方案跟二分图一样。

P2765 魔术球问题

你大胆猜测答案不会太大。那么对于每一对和为完全平方数的 x,y(x<y),我们连一条 xy 的边,那么问题就变为这个 DAG 的最小路径覆盖。做就行了。

然后你发现 n=55 时答案是 1567,确实非常小呢。实际上答案上界较小是可以证明的。

P8291 [省选联考 2022] 学术社区

重量级选手来了。

考虑处理出每两条信息接在一起时会带来多少贡献。发现贡献可能有 0(学术信息/louxia+loushang),1(louxia+?/?+loushang),2(loushang+louxia),而且很绝妙的是它们不冲突。称接在一起的关系为一条边。

那么现在问题就变为我们要在这个 m 个点的有向完全图上找一条点不重复的路径,使得路径上的边权之和最大。

考虑没有 2 的边(C 性质)怎么做。0 的边可以删掉,这时你选的路径断成了互不相交的若干段,然后你发现这就是一个最小路径覆盖问题,因为你要让 1 边尽量多,也就是断的段数尽量少。跑一遍求一下方案就可以了……吗?坏了,这回有环。

你发现没有太大的问题,因为题目有一个反复强调的性质就是每个人都发过至少一条学术消息!因为环肯定全是 louxia 或全是 loushang,我们可以随便挑环上的一个位置劈开,然后把断开位置的那个人发的学术消息接到前面/后面,相当于贡献并没有减少,而整个断开的环加上学术消息就相当于又变成了同一个人发的超大学术消息,可以继续用。

然后把 2 的边解决了。发现有贪心结论:优先把 2 的边全接了一定是不劣的。把这个结论扔到完全图上去看可以感性理解。发现 2 边形如 a b loushangb a louxia,可以等效为一条从前面看是 a 发的,从后面看是 b 发的一条学术消息。你正好发现这在最小路径覆盖时是没有什么影响的。

但是边数达到了惊人的 O(m2)。发现 loushang 或 louxia 是否能连接上是根据被 @ 者/发出者来定的,只要这两个信息一样就没有本质区别。所以可以开两排辅助点分别表示被 @ 者/发出者,连边的时候往它们上面连,于是边数变为了 O(m) 级别。时间复杂度还是二分图匹配的 O(mm)

输出方案的话就各显神通吧。

集合划分

n 个元素,你要将它们分到两个集合 A,B 中,已知第 i 个元素分配到 A 集合的代价是 aiB 集合的代价是 bi,又有 m 组关系 (ui,vi)ui,vi 不在同一个集合中的代价是 ci。问最小划分代价。

考虑最小割。

  • 源点向每个元素连容量为 bi 的边。
  • 每个元素向汇点连容量为 ai 的边。
  • 每对 ui,vi 间连容量为 ci双向边。
  • 跑一遍最小割(最大流)即为答案。其中每个元素如果连源点就代表被分到 A 集合,连汇点就代表被分到 B 集合。注意前面容量是反的。

不难理解。其中每对关系可以实现是因为如果 ui,vi 不在同一个集合,那么它们被割的边就不在同一侧,就需要把关系边也割掉才能保证不连通。

几个变形:

  • ai,bi 中有非正数:把 ai,bi 在图上对应的容量同时加上一个大常数,使得你每个元素不管分到哪个集合都得选这个大常数的代价,最后去掉 n 倍的这个常数即可。
  • 关系不只是 ui,vi 在不同集合,而是确定 uiA 集合,viB 集合之类:把双向边改成单向边。
  • 关系限制变为 ui,vi 在同一个集合中的代价:这个不属于集合划分,看后面最大独立集。

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

模板。

Gym100729F Pool construction

草地和洞是两个集合。处理出每一块变草地/变洞的代价,然后相邻的块存在上面说的关系。注意外围必须是草地,所以如果原本是洞的话保持洞的代价是 +

最大独立集

这个应该归到二分图里的。

一张二分图,每个点带权,求它的最大独立集。

  • 源点向每个左部点,每个右部点向汇点连容量为权值的边。
  • 原图中的边容量为 +
  • 跑一遍。

由于有结论最大独立集=点数-最大匹配,这个并不难理解。

其实从最小割的角度看的话这个结论也可以得到证明。如果一条边连接的两个点没有被割掉,那么我们就需要割掉这条边,而它的容量是巨大的,这不好。

拓展:

一张二分图,每个点带权,每条边也带权,你要选一些点,同时也会选两个端点都被选的边,求最大权值。

把上面的原图边容量改成边权就行了。结合最小割也不难理解。

  • 为什么不是二分图就没法做?

一般图的最大独立集问题已被证明是 NP 问题。而且你似乎也想不到不是二分图的时候该怎么建图啊。

P2774 方格取数问题

你发现这是一个二分图,考虑最大独立集。

  • 将网格黑白染色。
  • 把黑点和白点分别拎出来排成两排。
  • 源点向黑点,白点向汇点连容量为对应 ai,j 的边。
  • 网格上相邻的黑白点之间连容量为 的边。
  • 跑最小割。ai,j 减去结果即为答案。

P5030 长脖子鹿放置

也许你会在做完方格取数问题之后满怀自信地点开这题,然后……不会了。

感觉网络流题确实非常需要避免思维固化,实际上这题把上一个题的黑白染色换成按横坐标奇偶性染色就可以了。

Gym102428A Swap Free

根据交换关系建出图,发现是一个最大独立集问题,如果是一般图就寄了,所以肯定要找点性质才能做。

你发现这个图上一定没有奇环,于是就可以做了。

最大闭合子图

  • 给定一张有向图,每个点带权。请你求一个子图,使得图上所有点的出边都在子图中,并使子图内的点的点权之和最大。

考虑最小割。

  • 源点向所有非负权点连容量为对应权值的边,所有负权点向汇点连容量为对应权值的相反数的边。
  • 保留原图的所有边,容量为 +
  • 跑一遍最小割(最大流),所有正权的和减去最小割结果即为答案。没被割掉的边连接的点就在子图中。

这里原图的边实在太大,所以割的肯定全是附加边。而对于源点连接的没被割掉的边,可以发现从它们连的点往外流,流到不能流的时候就是一个闭合子图,这时再把负权点连的边割掉就可以不连通了。可以发现这个最小割和最后的减去答案很巧妙地处理了正权/负权边以及答案最优。

P4174 [NOI2006] 最大获利

模板。

P2762 太空飞行计划问题

还是模板。

最大密度子图

  • 求无向图上的一个导出子图,使得子图中边与点的比值最大。

设原图为 (V,E),子图为 (V,E)

比值问题先二分答案试试。设答案为 g,那么有 |E||V|g|E|g|V|0

简单但复杂度没那么优做法

考虑子图有什么限制。也就是要选一条边,那么就必须要选它连接的两个点。如果把每条边也看成点的话,我们惊喜地发现这就是一个最大闭合子图问题。

  • 每条原图中的边代表的点向它连接的两个点连一条边。
  • 每条原图的边代表的点有 1 的点权,每个原图的点有 g 的点权。
  • 直接跑最大闭合子图。

非常好理解,但是点数是 O(n+m) 的,遇到稠密图可能会死掉。

复杂度优但没那么简单做法

考虑能不能把每条边的选取情况用点的信息描述。你惊喜地发现有一个东西叫做度数!

设第 i 个点在原图中的度数为 di。如果没有连向外面的边,边数就是 di2。而现在出现了两端不一定在同一个子图里的边,考虑设这一部分的度数为 f(V,VV)。那么对于子图 V|E|g|V|=uVdu2f(V,VV)2g|V|=12(uV(du2g)f(V,VV))0。你发现这好像这变成了一个……集合划分?

不完全是。集合划分求的是最小值,这玩意要最大值 0,而且里面还有负数。那我们两边取负数变成 f(V,VV)+uV(2gdu) 的最小值取负……吗?不对里面那个 2gdu 可能也是负的。这怎么办。

考虑魔改集合划分。

  • 源点向每个点连容量为 K 的边。
  • 每个点向汇点连容量为 2gdu+K 的边。
  • 每对原图中的无向边的容量改为 1。(这个是 f(V,VV)

K 是一个大常数,这里可以取 m。这里其实就用了上面讲的去除负数的方法。因为每个点不管分没分进 V,它们的代价里都得有一个 K。所以最后二分条件就是 (|E|g|V|)max=2(nK(集合划分结果))0,即 nm最大流0

UVA1389 Hard Life

模板。输出方案就是输出集合划分的方案也就是输出最小割的方案。

(to be continued)

posted @   CarroT1212  阅读(64)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示