网络流

网络流

定义

网络是指一个特殊的有向图 G=(V,E),其与一般有向图的不同之处在于有容量和源汇点。

  • E 中的每条边 (u,v) 都有一个被称为容量的权值,记作 c(u,v)。当 (u,v)E 时,可以假定 c(u,v)=0

    对于网络中的反向边和不在网络中的边,容量应为 0。(但也不是唯一,如果题目中的边是双向的话,那么正向边和反向边的容量都是给定的权值。)

  • V 中有两个特殊的点:源点 s汇点 tst)。

对于网络 G=(V,E)是一个从边集 E 到整数集或实数集的函数,其满足以下性质。

  1. 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即 0f(u,v)c(u,v)

    不然一条边就爆炸了。

  2. 流守恒性:除源汇点外,任意结点 u净流量0。其中,我们定义 u 的净流量为 f(u)=xVf(u,x)xVf(x,u)

    流入多少,也应该流出多少,不然一个结点就爆炸了。

  3. 斜对称性:对于每条边,f(u,v)=f(v,u)

    这是后文建反悔边需要用到的。

对于网络 G=(V,E) 和其上的流 f,我们定义 f流量 |f|s 的净流量 f(s)。作为流守恒性的推论,这也等于 t 的净流量的相反数 f(t)

源点流出多少,汇点就流入多少。事实上,f 是可以为负的。

对于网络 G=(V,E),如果 {S,T}V划分(即 ST=VST=),且满足 sS,tT,则我们称 {S,T}G 的一个 s-t 割(cut)。我们定义 s-t{S,T} 的容量为 S,T=uSvTc(u,v)

就是把一张网络劈成两部分,割就是被劈断的边的容量和。

还有一些定义:

  • 流量网络:每条边都给出了流量的网络。
  • 边的残留容量:残留容量 = 容量 - 流量。
  • 残量网络:由每条边的残留容量构成的网络。

最大流

最大流问题:顾名思义,在一张网络上最大化 f

FF 方法(增广路方法)

FF 方法是后面所有最大流算法的基础,之后的所有算法都是对它的优化。

FF 方法的思路简单粗暴,每次找到一条增广路进行增广,直到原图中不存在增广路。

增广路方法基于增广路定理:流量网络达到最大流当且仅当残量网络中没有增广路。

但我们需要考虑到一个问题:我们可能增广了事实上不在最大流的边。解决方法是,我们给每条增广过的边都建一条反悔边,容量为正边的相反数。这样我们在下一次增广的时候就有了重新找最大流的机会。而这条错误的边最后不会给答案做贡献。

这就是 FF 方法。

EK 算法

是 FF 方法的一个具体实现。每次 BFS 找到一条最短的增广路。总复杂度为 O(nm2)

EK 算法慢的原因是每次遍历一遍残量网络只能找到一条增广路。Dinic 算法就是对它的优化。

Dinic 算法

是求解最大流问题的最常用的算法。理论复杂度 O(n2m),但实际上远远跑不满,可以视为 O(玄学)

Dinic 算法一次找到多条增广路经:对于网络中的每个点,如果它有多个分支,则对所有分支都进行一次增广路搜索,而不是重新从源点开始增广。Dinic 算法是 BFS 和 DFS 的结合。

算法思想:

  1. BFS 分层,即根据结点 u 到源点 s 的距离 dep(u) 做分层,暂时删除 dep(v)<dep(u) 的边 (u,v)。分层图的作用是限制 DFS 的搜索范围,在分层图中的任意路径都是边数最少的最短路径。

  2. DFS 增广。一次 DFS,多次增广。在分层图上对当前点的每一个分支 DFS,因为是在分层图上 DFS 的,所以 DFS 只能一层一层往后找到汇点,不会绕路。每次找到一条增广路时就更新残量网络。

  3. 重复上述流程直到源汇点不再连通。

优化

  • 无用节点删除:若从一个点 u 出发无法继续增广,就令 dep(u)=,相当于变相删除了 u
  • 当前弧优化:从一个点 u 增广后可能有很多点 vc(u,v)=0,显然这些点在后续中无用,所以我们标记每个节点的 cur(u) 表示 u 点之后的第一个有用的点,下次增广时从它开始遍历即可。

实现

在网络流中,我们经常需要访问一条边的反向边,所以初始 tot=1

// 背板!
int cur[MAXN],dep[MAXN];
bool bfs(){
	memset(dep,-1,(n+1)<<2);
	dep[s]=0;
	cur[s]=head[s];
	queue<int>q;
	q.emplace(s);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		for(int i=head[u],v;i;i=e[i].to)
			if(e[i].w>0&&!~dep[v=e[i].v]){
				q.emplace(v);
				cur[v]=head[v];
				dep[v]=dep[u]+1;
				if(v==t) return 1;
			}
	}
	return 0;
}
ll dfs(int u,ll sum){
	if(u==t) return sum;
	ll flow=0;
	for(int i=cur[u],v;i&&sum;i=e[i].to){
		cur[u]=i;
		if(e[i].w>0&&dep[v=e[i].v]==dep[u]+1){
			ll k=dfs(v,min(sum,(ll)e[i].w));
			if(!k) dep[v]=-1;
			e[i].w-=k;
			e[i^1].w+=k;
			flow+=k;
			sum-=k;
		}
	}
	return flow;
}
ll dinic(){
	ll res=0;
	while(bfs()) res+=dfs(s,INF);
	return res;
}

最大流建模

网络流的精髓在于建模。需要掌握一些常见的模型。

主要的套路就是拆点、转化为二分图、二分答案

拆点

拆点本质上是把点的限制转化为边的限制的一种方法。主要有两种:一拆二、一拆多。

  • 一拆二:可以解决点上的限制,比如一个点最多只能经过几次之类。
  • 一拆多:主要用于不同的流经过点所带来的贡献不同,或者每个点在不同时刻的贡献不同。这时候就需要一拆多,每个点代表一种情况。

二分图

当题目没有给出明显的超源和超汇,也难以判断超源和超汇分别连接哪些点时,可以考虑二分图。即,把题目所给的点分成两类(记作 S,T),保证边的限制只存在于 S,T 之间,也就是形成一个二分图。然后超源连接 S 中的所有点,然后 T 中的所有点连接超汇。与超源或超汇相连的边的容量通常是题目给的贡献或限制。然后 S,T 之间按照题目要求连边,容量通常是

典题讲解

  • P2472 [SCOI2007] 蜥蜴

    拆点,把一个点拆成入点和出点,入点向出点连边。对于能跳出去的点,从当前点的出点像汇点连边。这两种边边权都是该点的权值。如果该点有蜥蜴,就连源点到该点入点,边权为 1 的边(只有一条蜥蜴)。答案就是总蜥蜴数减去最大流。

  • P3324 [SDOI2015] 星际战争

    二分图 + 二分答案。首先发现单调性:加入答案为 x,则所有时间大于 x 的情况一定是合法的,于是二分答案。然后考虑建模,将每一个武器向对应的机器人连边权为 的边,超级源点向武器连边权为用时 × 激光武器伤害的边,机器人向超级汇点连边权为机器人血量的边,判断最大流是否等于机器人血量之和。

  • P4311 士兵占领

    最小值转化最大流。看到方格问题带行和列的限制,连从行到列的边。把最小值转化为求空出格子数量的最大值,然后从超级源点向每行连边权为当前行空出格子数量的边,每列向超级汇点连边权为空出格子数量的边,同时对于空格子连从行到列边权为 1 的边。跑最大流,最后用 nmk 减去就是答案。

  • P3191 [HNOI2007] 紧急疏散EVACUATE

    按时间拆点。首先根据单调性,直接二分时间。注意到一个单位时间内一扇门只能走一个人,考虑把门按照不同时间拆点。把这些点向超级汇点连边权为 1 的边,每个人向到达门所用时间的点连边权为 1 的边。又因为时间是在流逝的人到达一个门还可以等,所以同一个门的不同时间的点之间需要连流量为 的边。

  • P5038 [SCOI2012] 奇怪的游戏

    二分图 + 二分答案。这道题出现的一个 trick 是:见到两个相邻格子之类话语,考虑黑白染色,转化为二分图。然后考虑二分最后变成的数,用网络流来 check 即可。

最小割

定义

前文已经提到过,把一张网络删去一些边使得 s,t 不再连通,这就是一个割,其中割的容量为删去边的容量和。最小割就是求容量最小的割。

最大流最小割定理

定理内容很简单:最小割容量等于最大流。

口胡证明如下:

引理:对于一张网络,其任意一个割均大于等于最大流。

证明:假设该网络有一个割小于最大流,那么当这个割满流的时候网络不再连通。但根据最大流的定义,此时取到最大流。矛盾,故原命题成立。

然后证明最小割能取到最大流。

当原图跑到最大流的时候,图中所有满流的边的流量总和就是最大流,但同时这些边可以组成一个最小割。

证毕。

OI Wiki 上有更严谨的公式证明。

|f|=f(s)=uSf(u)=uS(vVf(u,v)vVf(v,u))=uS(vTf(u,v)+vSf(u,v)vTf(v,u)vSf(v,u))=uS(vTf(u,v)vTf(v,u))+uSvSf(u,v)uSvSf(v,u)=uS(vTf(u,v)vTf(v,u))uSvTf(u,v)uSvTc(u,v)=S,T

总之,有了这个定理,当我们想求出最小割时,只需跑一遍 Dinic 求出最大流即可。

最小割建模

文理分科模型

名称来源于 P1646 [国家集训队] happiness

这类模型一般指,有 n 个物品和两个集合 A,B,如果一个物品放入 A 集合会得到 ai,放入 B 集合会得到 bi;还有若干个形如 ui,vi,wi 限制条件,表示如果 uivi 同时在一个集合会得到 wi。每个物品必须且只能属于一个集合,求最大贡献。

这类模型一般具有二者选其一的字眼。令源点和汇点分别表示选 A 和选 B,对于每个点 i 先连 (s,i,ai) 的边和 (i,t,bi) 的边,因为二者只能选其一,所以选择一边就要断掉另外一条边,这就是割。对于同时在这一类的限制,我们建立一个虚点 x,连接 (s,x,wi) 的边,然后再连接 (x,ui,)(x,vi,)(对于同时位于另外一个集合同理)。

这种方法可以有效地限制与结构,因为我们要么将 ui,vi 连向汇点的边断掉,要么断掉源点连向虚点的边。

最后统计答案一般是先把所有的贡献加起来,再减去最小割。

P3973 [TJOI2015] 线性代数

先化简式子为:

D=i=1nj=1naiajbi,ji=1naici

此时有两种建模方法。

第一种较为常见,以 bi,j 的视角:如果不选 bi,j(等价于 ai,aj 中至少一个为 0)损失 bi,j 的贡献,否则损失 ci+cj 的贡献。所以构建二分网络,左边有 n2 个点(bi,j),源点向每个 bi,j 连边权为 bi,j 的边;右边有 n 个点(ci),每个 ci 向汇点连边权为 ci 的边;最后每个 bi,jcicj 连边权为 的边,这一步利用了用容量为 的边表示冲突的套路。最后答案就是 bi,j 减去最小割。

第二种我只见到了 dyc 用。以 ai 的视角:如果选 ai 就会有 bi,i 的贡献,如果不选 ai 就会有 ci 的贡献。然后对于一组 ai,aj,如果同时选就会有 bi,j+bj,i 的贡献,于是建立虚点 x,连接每个 (s,x,bi,j+bj,i) 以及 (x,i,)(x,j,),做到了限制与结构。最终答案也是 bi,j 减去最小割。

这两种建模本质上应该是不一样的……第二种更快一些,因为点数更少。

P1791 [国家集训队] 人员雇佣

这题与上面的不同在于,两个人都选可以得到 Ei,j 的贡献,但如果其中一个不选不但不会得到贡献,还会损失贡献,但同时选两个不会有额外影响。这是异或结构,连接 (i,j,2×Ei,j),就可以表示出边权差。

和上面的建边是不同的。

切糕模型

名称来源于 P3227 [HNOI2013] 切糕

这类模型一般指,给定 n 个整数变量 xi,每个 xi 可以取 [1,m] 中的任意一个数,其中取 j 需要 vi,j 的代价。同时有若干个约束,形如 xuixviwixuixviwi。求最小总代价。

建模方法为:把每个整数变量拆成 m+1 个点 pi,1,pi,2,,pi,m+1,连 n 条链 spi,1pi,2pi,mpi,m+1t,其中 spi,1pi,m+1t 的容量是 (代表不能割这条边),pi,jpi,j+1 的容量为 vi,j。对于每一个限制,连接 pvi,j+wipui,j 容量为 的边,用容量为 的边表示冲突

然后跑 Dinic 求出最小割即可。

最大权闭合子图

什么是最大权闭合子图?在一个有向图上,点有点权,求点权和最大的一个子图,满足对于任意有向边 (u,v),若 u 被选 v 一定被选。此时这个子图就是最大权闭合子图。

这个问题可以用最小割求解。方法是建立超级源点 s 和超级汇点 t,枚举节点 u

  • 若节点 u 权值为正,则连一条 (s,u,valu) 的边;
  • 若节点 u 权值为负,则连一条 (u,t,valu) 的边。

最后将原图上所有边的边权都设置成 ,然后跑 Dinic,用所有正权值之和减去最小割就是最大权闭合子图。

原理可以口胡:对于权值为正的点若不选择,相当于损失 valu,相当于和源点断开,于是连源点;对于权值为负的点若选择,相当于损失 valu,相当于和汇点断开,于是连汇点。原图上的边不能断,于是边权设为

模型掌握即可,不要求证明。

最大权闭合子图的使用场景常见于题目中出现了负权或有明显的限制条件的情况下。

遇到这类模型的题目,一般是将题目中所给的各种限制转化为 “依赖” 关系(可以加点,每个点赋点权),把题目所给的贡献算到这些点的点权上,尝试把问题转化为最大权闭合子图问题。一旦转化成功就直接套模型得到答案。

一些性质/推论

将跑完最大流的残量网络进行缩点,排除流量为 0 的边,得到一个 DAG。这个 DAG 的性质有下:

  • s,t 必然不连通;

  • u,v 不在同一 SCC 且边 (u,v) 满流,则边 (u,v) 有可能在原图最小割中;

    首先,如果不满流,绝对不可能;其次,假设 u,v 在同一 SCC 则断开 (u,v) 没有意义,最小割里一定不会有 (u,v)。所以反之成立。

  • s,uv,t 都位于同一 SCC 且 (u,v) 满流,则边 (u,v) 必然在原图最小割中;

    因为 s,uv,t 都连通,所以如果不断掉 (u,v) 就没法把图割断了,所以 (u,v) 必然在最小割中。

最小割树

当我们需要求出任意两点的最小割时,暴力的复杂度是恐怖的 O(n4m),利用最小割树可以做到 O(n3m)。方法如下:

  1. 选择任意两个节点 u,v,求出最小割,并在新图中连接一条边权为最小割的边;
  2. 将整张图的节点分为两部分,使得割后一部分与 u 连通,一部分与 v 连通;
  3. 递归处理两个部分。

得到性质:任意两点的最小割就是它们在最小割树上的简单路径的最小值。

这样子优化后,我们只跑了 n 遍 Dinic,所以复杂度就降下来了。

实际在代码实现上,我们不需要将整棵树建出。在递归时,我们可以将与 u 连通的点放在整个序列的左端,与 v 连通的点放在整个序列的右端,这样子就可以直接分两段调用。然后我们发现对于分别处于两个部分的两点 a,bcut(a,b)=min{cut(a,u),cut(u,v),cut(v,b)},因为 cut(a,u)cut(v,b) 我们通过递归可得,而 cut(u,v) 是我们在当前函数中通过跑 Dinic 得到的,所以可以直接将两点的最小割存在数组中,做到 O(1) 查询。复杂度是 O(n3m+n2+q) 的。

也可以把树建出来,然后用树上算法,复杂度是 O(n3m+nlogn+qlogn) 的。

实现:(P4897 【模板】最小割树

#include<bits/stdc++.h>
using namespace std;

constexpr int MAXN=855,MAXM=8505,INF=0x3f3f3f3f;
int n,m,head[MAXN],tot=1;
struct{
	int v,to,w,ow;
}e[MAXM<<1];
void addedge(int u,int v,int w){
	e[++tot]={v,head[u],w,w};
	head[u]=tot;
	e[++tot]={u,head[v],w,w}; // 这里不一样
	head[v]=tot;
}
struct{
	int n,s,t;
	int cur[MAXN],dep[MAXN];
	bool bfs(){
		memset(dep,-1,(n+1)<<2);
		dep[s]=0;
		cur[s]=head[s];
		queue<int>q;
		q.emplace(s);
		while(!q.empty()){
			int u=q.front();
			q.pop();
			for(int i=head[u],v;i;i=e[i].to)
				if(e[i].w>0&&!~dep[v=e[i].v]){
					q.emplace(v);
					cur[v]=head[v];
					dep[v]=dep[u]+1;
					if(v==t) return 1;
				}
		}
		return 0;
	}
	int dfs(int u,int sum){
		if(u==t) return sum;
		int flow=0;
		for(int i=cur[u],v;i&&sum;i=e[i].to){
			cur[u]=i;
			if(e[i].w>0&&dep[v=e[i].v]==dep[u]+1){
				int k=dfs(v,min(sum,e[i].w));
				if(!k) dep[v]=-1;
				e[i].w-=k;
				e[i^1].w+=k;
				flow+=k;
				sum-=k;
			}
		}
		return flow;
	}
	int dinic(){
		for(int i=2;i<=tot;++i) e[i].w=e[i].ow; // 注意每次都要恢复原边权
        // 因为原图本质上没有变,所以 cur 可以不清空
		int res=0;
		while(bfs()) res+=dfs(s,INF);
		return res;
	}
}D;

// 背板!
int node[MAXN],tmp[MAXN],cut[MAXN][MAXN];
void solve(int l,int r){
	if(l==r) return;
	int s=node[l],t=node[r];
	D.s=s,D.t=t;
	int k=D.dinic();
	cut[s][t]=cut[t][s]=k;
	int L=l,R=r;
	for(int i=l;i<=r;++i)
		if(~D.dep[node[i]]) tmp[L++]=node[i];
		else tmp[R--]=node[i];
	memcpy(node+l,tmp+l,(r-l+1)<<2);
	solve(l,L-1),solve(L,r);
	for(int i=l;i<L;++i)
		for(int j=L;j<=r;++j)
			cut[node[i]][node[j]]=cut[node[j]][node[i]]=min({cut[node[i]][s],cut[node[j]][t],k});
}

int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin>>n>>m;
	D.n=++n;
	for(int i=1,u,v,w;i<=m;++i){
		cin>>u>>v>>w;
		addedge(u+1,v+1,w);
	}
	for(int i=1;i<=n;++i) node[i]=i,cut[i][i]=INF;
	solve(1,n);
	int Q;
	cin>>Q;
	while(Q--){
		int u,v;
		cin>>u>>v;
		cout<<cut[u+1][v+1]<<'\n';
	}
	return 0;
}

平面图最小割(对偶图最短路)

定义

何谓平面图?平面图指对于一张图,存在一种点的位置,使得在不弯曲边的情况下,所有边在二维平面上不会交叉。常见的平面图有环、棋盘等。

对偶图相对于平面图存在,每张平面图都对应着一张对偶图。由上文知,平面图把平面分成了若干个部分,那么对偶图就是把每个部分看作一个点,把相邻的部分连边,边权就是相邻的边的边权。特殊地,整张图外侧的区域也应当算作一个部分,对于这个部分一般会拆两个点。

给出一张图示:(黑色的是原图,绿色的是对偶图)

关联

事实上,平面图最小割等于对偶图最短路。以狼抓兔子一题为例,这道题要求一张平面图最外侧两个顶点的最小割。

我们仿照上面那张图的建模方式建出对偶图。容易发现,对偶图的一条路径 st 相当于原图的一个割。那么原图的最小割显然就是对偶图的最短路,可以用 SPFA 或 Dijkstra 求解。

实际上在这道题中,即使是 Dinic 也因为其 O(玄学) 的复杂度顺利地通过了。但是理论上还是 Dijkstra 最优,并且谷上的一些学长告诫我们:遇到平面图最小割,一定要打对偶图最短路。所以还是尽量不要踩这个坑。

建图

最后是建图问题。实际上建立一张平面图的对偶图是非常简单的,只需要把每条边按照固定的方向旋转 90 即可。

费用流

当最大流的每条边的单位流量带上一定费用,要求求出在最大流前提下使得总费用最小,这就是费用流。

那么在费用流中如何建立一条边?换句话说,考虑反向边的建立方法。显然,在撤回流量费用时要减去,所以反向边的费用是正向边的相反数

然后考虑怎么求费用流。显然,要么是先求出一个最大流,然后不断优化得到最小费用流;要么是每次增广一条最小费用的流,直到无法增广。在代码实现中,一般采用后者。那么我们就需要一个最短路来求出最小费用。只能使用 SPFA,因为网络流的反向边必定出现负权,除非你愿意把边权统一加上一个大数全变为正然后跑 Dijkstra。

然后,用 EK 算法求解增广路。至于为什么不用 Dinic,因为它代码多、复杂度优化不明显(SPFA 的 O(nm) 复杂度上界)。最重要的是,费用流的题目没人敢卡 EK+SPFA(江湖规矩)。所以用 EK+SPFA 即可。

复杂度方面,SPFA 上界是 O(nm) 的,最差会跑 nm 次 SPFA,所以复杂度上界是 O(n2m2)。当然,这只是上界。众所周知,在网络流中,上界和实际是两码事。

处理完之后依旧需要修改每条边的流,并且 EK+SPFA 能够同时求解出最大流。

// 背板!
int n,s,t,pre[MAXN];
int dis[MAXN],inr[MAXN];
bool vis[MAXN];
bool spfa(){
	memset(dis,0x3f,(n+1)<<2);
	memset(vis,0,n+1);
	queue<int>q;
	q.emplace(s);
	dis[s]=0,inr[s]=INF;
	while(!q.empty()){
		int u=q.front();
		q.pop();
		vis[u]=0;
		for(int i=head[u],v;i;i=e[i].to)
			if(e[i].w>0&&dis[v=e[i].v]>dis[u]+e[i].val){
				dis[v]=dis[u]+e[i].val;
				inr[v]=min(inr[u],e[i].w);
				pre[v]=i;
				if(!vis[v]) vis[v]=1,q.emplace(v);
			}
	}
	return dis[t]<INF;
}
void mcmf(){
	while(spfa()){
		maxf+=inr[t];
		minc+=dis[t]*inr[t];
		for(int u=t,i;u!=s;u=e[i^1].v){
			i=pre[u];
			e[i].w-=inr[t];
			e[i^1].w+=inr[t];
		}
	}
}

费用流建模

和最大流差不多,依然是拆点、二分图那些。讲一些比较特别的题目。

这道题的弱化版是 P2053 [SCOI2007] 修车。显然在这道题如果用弱化版的思路,直接按时间拆点,边数会达到接近 107,这显然对于网络流来说是不可接受的。发现很多点和边实际上是没用的,因为每次增广出的流一定是 1,也就是顶多会多占领一个厨师在一个时间的位置,而我们完全可以等到占领它的时候再将这个点和所连的边建出来。可以证明,这样子做不影响答案,因为对应厨师的前一个点的边权一定比新增点的边权小,所以最后所作出的贡献的顺序是正确的。

#include<bits/stdc++.h>
#define int long long
using namespace std;

constexpr int MAXN=1e5+5,MAXM=8e6+5,INF=0x3f3f3f3f3f3f3f3fll;
int N,m,p[45],T[45][105],head[MAXN],tot=1;
int maxf,minc,typ[MAXN],siz[MAXN];
struct{
	int v,to,w,val;
}e[MAXM];
void addedge(int u,int v,int w,int val){
	e[++tot]={v,head[u],w,val};
	head[u]=tot;
	e[++tot]={u,head[v],0,-val};
	head[v]=tot;
}
struct{
	int n,s,t,pre[MAXN];
	int dis[MAXN],flow[MAXN];
	bool vis[MAXN];
	bool spfa(){
		memset(dis,0x3f,(n+1)<<2);
		memset(vis,0,n+1);
		queue<int>q;
		q.emplace(s);
		dis[s]=0,flow[s]=INF;
		while(!q.empty()){
			int u=q.front();
			q.pop();
			vis[u]=0;
			for(int i=head[u],v;i;i=e[i].to)
				if(e[i].w>0&&dis[v=e[i].v]>dis[u]+e[i].val){
					dis[v]=dis[u]+e[i].val;
					flow[v]=min(flow[u],e[i].w);
					pre[v]=i;
					if(!vis[v]) vis[v]=1,q.emplace(v);
				}
		}
		return dis[t]<INF;
	}
	void mcmf(){
		while(spfa()){
			maxf+=flow[t];
			minc+=dis[t]*flow[t];
			for(int u=t,i;u!=s;u=e[i^1].v){
				i=pre[u];
				e[i].w-=flow[t];
				e[i^1].w+=flow[t];
			}
			// 动态加点 
			int x=e[pre[t]^1].v;
			siz[typ[x]]++;
			typ[++n]=typ[x];
			addedge(n,t,1,0);
			for(int i=1;i<=N;i++)
				addedge(i,n,1,T[i][typ[n]]*siz[typ[n]]);
		}
	}
}E;

signed main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin>>N>>m;
	for(int i=1;i<=N;++i) cin>>p[i];
	for(int i=1;i<=N;i++)
		for(int j=1;j<=m;j++){
			cin>>T[i][j];
			addedge(i,j+N,1,T[i][j]);
		}
	int s=0,t=N+m+1;
	for(int i=1;i<=N;i++) addedge(s,i,p[i],0);
	for(int i=1;i<=m;i++){
		siz[i]=1;
		addedge(i+N,t,1,0);
		typ[i+N]=i;
	}
	E.s=s,E.n=E.t=t,E.mcmf();
	cout<<minc<<'\n';
	return 0;
}

上下界网络流

当我们指定一条边的流量有上下界时,这就是上下界网络流问题。显然,最大流问题就是每条边上界为 ci 下界为 0 的上下界网络流。

无源汇上下界可行流

没有源点和汇点,只给定一些点和一些边,每条边的流量指定上下界,求是否存在循环流使得每个点流量守恒。

解决这类问题的方法是,先将每条边的流量设置为其下界。设 (u,v) 这条边的下界是 b(u,v),上界为 c(u,v),然后建立新图,在新图上将这条边的容量设置为 c(u,v)b(u,v)。因为最终的可行流一定是在每条边的流量达到其下界的基础上增大一些边的流量使得所有点满足流量守恒,同时流量不能超过上界。

那么现在,新图上的一点流量代表将原图上该边的流量增加一点。最后将两张图合并,一定是满足流量守恒的网络。我们将原图称为 “初始流”,新图称为 “附加流”。那么对于一个点有三种情况:

  1. 若其在初始流中满足流量守恒,那么在附加流中也满足流量守恒;
  2. 若其在初始流中流入量大于流出量,那么在附加流中流入量小于流出量;
  3. 若其在初始流中流入量小于流出量,那么在附加流中流入量大于流出量。

考虑求这个附加流。但是 Dinic 算法只能求出有源汇且流量平衡的最大流,而我们要求的这个附加流在某些点流量肯定是不平衡的。于是我们需要建立一个源点和一个汇点,对于每个点,如果是情况 1 自然不用考虑;对于上面的情况 2,连一条从源点到它、边权为流出量和流入量之差的边,把它的流入量补足;对于上面的情况 3 同理,连一条从它到汇点、边权为流入量和流出量之差的边,把它的流出量补足。

此时就可以跑 Dinic 了。判定答案的方法是:如果从源点连出去的边全部满流,则说明有解;否则无解。如果题目要求输出最后每条边的流量,只需输出其反向边的边权加上其原本的下界即可。(为什么是反向边?建议回炉重造 Dinic。)

实现(LOJ#115)

// Dinic 板子不再展示
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin>>n>>m;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v>>l[i]>>r[i];
		fl[u]-=l[i],fl[v]+=l[i];
		addedge(u,v,r[i]-l[i]);
	}
	int s=0,t=n+1,ans=0;
	for(int i=1;i<=n;i++)
		if(fl[i]>0) addedge(s,i,fl[i]),ans+=fl[i];
		else if(fl[i]<0) addedge(i,t,-fl[i]);
	D.s=s,D.n=D.t=t;
	if(D.dinic()==ans){
		cout<<"YES\n";
		for(int i=1;i<=m;i++) cout<<l[i]+e[i<<1|1].w<<'\n';
	}else cout<<"NO\n";
	return 0;
}

有源汇上下界可行流

考虑把有源汇转化为无源汇。注意到为了使源汇点满足流量守恒,需要有边连入源点 s 且有边连出汇点 t。可以连一条 (t,s,) 的边,相当于把从 s 流到 t 的流量又流了回来,就转化成无源汇的情况了。

有源汇上下界最大流

先跑出一组可行流。因为可行流已经满足流量守恒,所以只需要在可行流的残量网络基础上再跑最大流,把可行流的流量和再加上新跑出的最大流的流量和就是答案。

注意,我们只能在原图的边上跑最大流。所以需要禁掉新增的边。因为我们建立的虚拟源汇实际上并不连通,所以只需要禁掉我们连的那条 (t,s,) 的边即可。

实现(LOJ#116)

// Dinic 板子不再展示
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin>>n>>m>>s>>t;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v>>l[i]>>r[i];
		fl[u]-=l[i],fl[v]+=l[i];
		addedge(u,v,r[i]-l[i]);
	}
	int S=0,T=n+1,ans=0;
	for(int i=1;i<=n;i++)
		if(fl[i]>0) addedge(S,i,fl[i]),ans+=fl[i];
		else if(fl[i]<0) addedge(i,T,-fl[i]);
	addedge(t,s,INF);
	D.s=S,D.n=D.t=T;
	if(D.dinic()==ans){
		ans=e[tot].w;
		e[tot].v=e[tot].w=e[tot^1].v=e[tot^1].w=0;
		D.s=s,D.t=t;
		cout<<ans+D.dinic()<<'\n';
	}else cout<<"please go home to sleep\n";
	return 0;
}

有源汇上下界最小流

同理,将可行流多余的部分退回,也就是从源汇点到原源点跑一遍最大流,用可行流的流量减去这个最大流即可。

实现(LOJ#117)

// Dinic 板子不再展示
int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin>>n>>m>>s>>t;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v>>l[i]>>r[i];
		fl[u]-=l[i],fl[v]+=l[i];
		addedge(u,v,r[i]-l[i]);
	}
	int S=0,T=n+1,ans=0;
	for(int i=1;i<=n;i++)
		if(fl[i]>0) addedge(S,i,fl[i]),ans+=fl[i];
		else if(fl[i]<0) addedge(i,T,-fl[i]);
	addedge(t,s,INF);
	D.s=S,D.n=D.t=T;
	if(D.dinic()==ans){
		ans=e[tot].w;
		e[tot].v=e[tot].w=e[tot^1].v=e[tot^1].w=0;
		D.s=t,D.t=s;
		cout<<ans-D.dinic()<<'\n';
	}else cout<<"please go home to sleep\n";
	return 0;
}

上下界费用流

不管是有无源汇、最小费用流、最大费用流,都只需要把 Dinic 换成 MCMF 即可。注意到下界的费用是一定需要的,需要提前加入到答案里。

实现(P4043 [AHOI2014/JSOI2014] 支线剧情)

#include<bits/stdc++.h>
using namespace std;

constexpr int MAXN=1005,MAXM=3e5+5,INF=0x3f3f3f3f;
int n,head[MAXN],tot=1;
struct{
	int v,to,w,val;
}e[MAXM<<1];
int minc,fl[MAXN];
void addedge(int u,int v,int w,int val){
	e[++tot]={v,head[u],w,val};
	head[u]=tot;
	e[++tot]={u,head[v],0,-val};
	head[v]=tot;
}
struct{
	int n,s,t,pre[MAXN];
	int dis[MAXN],inr[MAXN];
	bool vis[MAXN];
	bool spfa(){
		memset(dis,0x3f,sizeof(int)*(n+1));
		memset(vis,0,sizeof(bool)*(n+1));
		queue<int>q;
		q.emplace(s);
		dis[s]=0,inr[s]=INF;
		while(!q.empty()){
			int u=q.front();
			q.pop();
			vis[u]=0;
			for(int i=head[u],v;i;i=e[i].to)
				if(e[i].w>0&&dis[v=e[i].v]>dis[u]+e[i].val){
					dis[v]=dis[u]+e[i].val;
					inr[v]=min(inr[u],e[i].w);
					pre[v]=i;
					if(!vis[v]) vis[v]=1,q.emplace(v);
				}
		}
		return dis[t]<INF;
	}
	void mcmf(){
		while(spfa()){
			minc+=dis[t]*inr[t];
			for(int u=t,i;u!=s;u=e[i^1].v){
				i=pre[u];
				e[i].w-=inr[t];
				e[i^1].w+=inr[t];
			}
		}
	}
}E;

int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin>>n;
	int t=n+1,ans=0;
	for(int i=1,k;i<=n;i++){
		cin>>k;
		for(int j=1,b,t;j<=k;j++){
			cin>>b>>t;
			fl[i]--,fl[b]++;
			addedge(i,b,INF-1,t);
			ans+=t;
		}
		addedge(i,t,INF,0);
	}
	addedge(t,1,INF,0);
	int S=0,T=n+2;
	for(int i=1;i<=n;i++)
		if(fl[i]>0) addedge(S,i,fl[i],0);
		else addedge(i,T,-fl[i],0);
	E.s=S,E.n=E.t=T,E.mcmf();
	cout<<ans+minc<<'\n';
	return 0;
}
posted @   Laoshan_PLUS  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示