网络流小结

废话

算上代码好歹有个一万字,但是不敢说这是详解,因为网上的大神们写的相当精彩……

应该看完多少会有点收获吧,个人感觉挺适合入门选手的,看完应该不至于入土。

最大流

先把例题摆出来: c l i c k   h e r e click~here click here

考虑一种暴力做法:从点 1 1 1 往外 d f s dfs dfs,找到一条能往 n n n 流流量的路时,就暴力流过去,能流多少流多少,而经过的边的流量上限自然相应减少。不停地找这样的路径直到找不到为止。

找到的这条路,在网络流中,我们称之为增广路,而最后能流过去的最大流量,称为最大流

显然上面的爆搜很可能会做出不优秀的决策,最经典的就是这张图了:
在这里插入图片描述
噢,顺便说一下,图这种东西,在网络流里面,我们称之为网络qwq。

显然,当增广路为 1 → 2 → 4 1\to 2\to 4 124 1 → 3 → 4 1\to 3\to 4 134 时,能得到最大流 2 2 2,但是如果爆搜搜到了 1 → 2 → 3 → 4 1\to 2\to 3\to 4 1234 这样的憨憨增广路,那么流量只有 1 1 1 了。

为了避免这种情况,我们需要引入一个优化:反向弧,也就是对于每条边,建一条反向边,一开始的流量为 0 0 0,当正向边流量减少时,反向边流量相应增加,反向边流量减少时,正向边亦然。

这有什么用呢?来看看有了反向边后,假如找到了 1 → 2 → 3 → 4 1\to 2\to 3\to 4 1234 这样的增广路会怎么样:
在这里插入图片描述
又要来补充一些概念了……这种被跑过的图,我们称之为残余网络

此时流量变成了这样,然后发现,我们又可以找到一条增广路: 1 → 3 → 2 → 4 1\to 3\to 2 \to 4 1324

没错, 2 → 3 2\to 3 23 的反向边发挥了作用,先走了 2 → 3 2\to 3 23,再走了它的反向边,那么这条边相当于没走,所以上面走的两条增广路其实就相当于 1 → 2 → 4 1\to 2\to 4 124 1 → 3 → 4 1\to 3\to 4 134 这两条。

反向边的作用浮出水面:提供反悔的机会。

要说具体一点的话,就是假如有这样一条不优秀的 S → . . . → a → b → d → . . . → T S\to ...\to a\to b \to d\to ...\to T S...abd...T,优秀的走法应该是: S → . . . → a → S\to ...\to a\to S...a c c c → . . . → T \to ...\to T ...T,那么反向边就会提供一条这样的路径: S → . . . → b → a → c → . . . → T S\to ...\to b\to a\to c\to ...\to T S...bac...T a → b a\to b ab 这条边的正向边和反向边都被走了一次,相当于没走,所以事实上我们得到的两条增广路为 S → . . . → a → c → . . . → T S\to ...\to a\to c\to ...\to T S...ac...T S → . . . → b → d → . . . → T S\to ...\to b\to d\to ...\to T S...bd...T

(不明白可以画个图手玩一下qwq)

加上反向边后,这种最暴力的算法就是EK算法

但是这样的时间复杂度依然不能让人满意,所以还有几个优化:

  1. 发现如果要求最大流的话,就要让增广路经过的边数尽可能少,这样每次影响的边数也尽可能少,能找到的增广路就会尽可能多,可以减少反向边的使用次数,尽可能每次都能找到优秀的增广路。
    要做到这一点的话,就在每次求增广路前预处理一个 h h h 数组, h [ i ] h[i] h[i] 表示从源点到点 i i i 至少经过多少个点(边也行,差不多),然后在求增广路时,只有满足了 h [ y ] = h [ x ] + 1 h[y]=h[x]+1 h[y]=h[x]+1 时, x x x 才往 y y y 那里搜。即对图进行分层。
  2. 每次找到一条增广路就重新来这样的做法太慢了,我们可以去找尽可能多的增广路,直到找不到位为止,然后重新求一次 h h h 数组再来。
  3. 当前弧优化(建议结合代码理解):比如说,上一次搜索到点 x x x 时,枚举到第 k k k 个儿子就没有流量了,此时我们选择退出循环,那么下一次来点 x x x 时,直接从第 k k k 个儿子开始往下枚举即可,不需要再从第一个儿子开始了。

有了这些优化后,你就得到了一个优秀的网络流算法:dinic算法。

代码如下(上面模板题的 A C AC AC 代码):

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 210

int n,m;
struct edge{int y,z,next;};
edge e[maxn<<1];
int first[maxn],cur[maxn],len=1;//注意len是1,这样方便找反向边
void buildroad(int x,int y,int z)
{
	e[++len]=(edge){y,z,first[x]};
	first[x]=len;
}
int h[maxn],q[maxn],st,ed;
bool bfs()//求h数组
{
	memset(h,0,sizeof(h));
	for(int i=1;i<=n;i++)cur[i]=first[i];//cur记录当前弧,一开始初始化为first
	st=ed=1;q[st]=1;h[1]=1;
	while(st<=ed)
	{
		int x=q[st++];
		for(int i=first[x];i;i=e[i].next)
		//假如e[i].y是第一次来,并且这条边还有流量,才去更新他,没有流量的边不考虑
		if(h[e[i].y]==0&&e[i].z>0)h[q[++ed]=e[i].y]=h[x]+1;
	}
	return h[n]>0;
}
int dfs(int x,int flow)//flow表示此时流到x的流量,dfs的返回值表示有多少流量流到了汇点
{
	if(x==n)return flow;//假如流到了汇点,那么汇点照单全收
	int tt=0;//记录流到汇点的流量
	for(int i=cur[x];i;i=e[i].next)//从当前弧开始
	{
		int y=e[i].y;cur[x]=i;//别忘了更新
		if(h[y]==h[x]+1&&e[i].z)
		//第一个条件正如上面所说,然后还要求这条边有流量
		{
			int p=dfs(y,min(flow-tt,e[i].z)); tt+=p;
			e[i].z-=p;e[i^1].z+=p;//正向边减去,反向边加上
			if(tt==flow)break;//没流量了就不用循环了
		}
	}
	if(!tt)h[x]=0;//假如这次一滴流量都没流过去,那么以后就没必要来x了
	return tt;
}

int main()
{
	scanf("%d %d",&m,&n);
	for(int i=1,x,y,z;i<=m;i++)
	scanf("%d %d %d",&x,&y,&z),
	buildroad(x,y,z),buildroad(y,x,0);//别忘了建反向边
	int ans=0;while(bfs())ans+=dfs(1,2147483640);
	printf("%d",ans);
}

最坏时间复杂度为 O ( n 2 m ) O(n^2m) O(n2m),但是网络流的时间复杂度一般跑不满。

简单应用1

二分图最大匹配

比如说一个班级里将男女生配对。

网络流的题做起来一般分为两步:建模型,套板子。所有题目的难点都是第一步。(废话,第二步有什么难的= =)

这题的模型是超级好建的那种,创造一个超级源点和一个超级汇点,源点向所有男生连边,所有女生向汇点连边,然后每对关系中男生向女生连边,所有边的流量都是 1 1 1,最后跑一发最大流就是答案。

简单分析一下这些边的意义:源点向男生连流量为 1 1 1 的边,代表每个男生最多得到 1 1 1 的流量,那么这限制了每个男生只能找 1 1 1 个女生,而女生向汇点连流量为 1 1 1 的边,代表每个女生只能被 1 1 1 个男生选择,正好对应了上面的要求。而中间根据关系连的边,流量其实无所谓,大于 0 0 0 即可。

匈牙利跑的话复杂度是 O ( n m ) O(nm) O(nm) 的,但是dinic只需要 O ( m n ) O(m\sqrt n) O(mn ),证明我不会,但似乎不是很难,可以自行百度学习……

拆点

这是网络流的一个经典技巧。

例题: 给出一张有向图,找出尽可能多的从起点到终点的路径,这些路径不能经过相同的点(即每个点只能存在于至多 1 1 1 条路径),除了起点和终点。

拆点的作用一般就是是将点的限制或贡献什么的放到边上,这里把每个点拆成入点和出点,顾名思义,入边连入点,出边连出点,两点之间再连一条流量为 1 1 1 的边即可。这样每个点就只会被走一次。

上面是只有将限制放到边上,一类将贡献放到边上的就比如, 1 1 1 单位流量经过这个点会获得多少贡献这样子,然后就可以拆点跑费用流。

集合划分模型

n n n 个球,你要将他们分成 S , T S,T S,T 两个集合,第 i i i 个球在 S S S 内的代价为 a i a_i ai,在 T T T 内的代价为 b i b_i bi,若 x , y x,y x,y 两个球不在同一个集合内那么有 c x , y c_{x,y} cx,y 的额外代价,求最小代价的划分。

网络流有一个经典的常用定理:最大流最小割定理(下面有解释),即最大流等于最小割,这意味着我们常常可以用最小割的思想建图,然后用最大流算法求解。

考虑建立超级源 S S S 超级汇 T T T,令 S S S i i i 连边流量为 b i b_i bi i i i T T T 连边流量为 a i a_i ai,令 i , j i,j i,j 间连双向边流量为 c x , y c_{x,y} cx,y

( S , i ) (S,i) (S,i) 这条边被割了,那么意味着 i i i 点放到 T T T 集合内,若 ( i , T ) (i,T) (i,T) 被割了,那么就是放到 S S S 集合内。假如 ( i , j ) (i,j) (i,j) 这条边被割了,则意味着 i , j i,j i,j 不在同一个集合内。

我个人惯用的一种考虑最小割模型的方式是:对于源点到汇点的一条路径,路径上会被割掉至少一条边,考虑每条边被割掉时的意义是否和题目限制对得上。


例题: 文理分科

这题是五个人分在同一个集合内才有额外贡献,而不是两个人,这是难以处理的地方。

首先最大贡献可以转化为总贡献减去最小贡献,最小贡献就可以用最大流求。

建图还是 S → i , i → T S\to i,i\to T Si,iT,流量对应选文科和选理科的贡献。对于每五个点的一组,建两个新点 A i , B i A_i,B_i Ai,Bi,建边 S → A i , B i → T S\to A_i,B_i\to T SAi,BiT,流量对应全文和全理的贡献。然后 A i → j , j → B i A_i\to j,j\to B_i Aij,jBi,流量无限, j j j 是组内的点。

割去 S → i , i → T S\to i,i\to T Si,iT 的意义和上面一样,如果一组内要全文,也就是 S → A i S\to A_i SAi 不能被割,由于存在 S → A i → j → T S\to A_i\to j\to T SAijT 这样的路径, A i → j A_i\to j Aij 不可能被割,那么五条 j → T j\to T jT 都要被割掉,也就是全都不能选理科,意义对应上了;全理是类似的。

练习题

最大权闭合子图

最大权闭合子图

各种性质&定理

最大流最小割定理

即一张图的最大流容量等于最小割的容量。

补充一下割的定义:割是图中的一些边组成的集合,如果去掉这些边,那么 S S S 不能到达 T T T。一个割的容量就是去掉的边的流量之和。

证明的话,注意到,对于一个割而言,在图中去掉这个割之后,是不可能找到 S S S T T T 的增广路的。这意味着,割其实就是对流的上界的约束,那么最小割就是最紧的约束,所以最大流等于最小割。

König定理

这是个在二分图上的定理,即最大匹配数等于最小点覆盖数。

最小点覆盖:假设标记一个点后与它相连的所有边都会被覆盖,那么最小点覆盖就是要标记最少的点使得所有边都被覆盖。

假设最大匹配数为 K K K,那么显然对于任意点覆盖,需要的点数都 ≥ K \geq K K,因为不存在一个点可以同时覆盖两条匹配边。

所以如果能用 K K K 个点覆盖所有边,那么一定是最小的点覆盖。下面就给出一种构造方法,使得用 K K K 个点就能覆盖所有边。

先使用匈牙利算法求出一个最大匹配。对于所有 右部的 未匹配的 点,尝试从他往外找增广路——像匈牙利那样,右边点只走为匹配边,左边点只走匹配边——但显然是找不到的,最后会走到一个 右部的 已匹配的 点。

对于上面走出来的伪·增广路,我们将所有这些伪·增广路上的点都标记上,然后就有结论:所有左部的被标记的点和右部的无标记的点,他们就是一个最小点覆盖。

先证明,为什么这是一个点覆盖。

先考虑匹配边,假如一条匹配边 ( u , v ) (u,v) (u,v) 的右部点 v v v 是被标记了的,那么 u u u 也一定被标记了——因为 v v v 就是从 u u u 伪·增广过来的,那么此时 u u u 在点覆盖中;否则 v v v 没有被标记,那么 v v v 在点覆盖中。

再考虑非匹配边,思路是类似的,假如 v v v 是被标记了的,那么 u u u 一定会被标记,因为 u u u 会被 v v v 伪·增广到,此时 u u u 在点覆盖中;否则 v v v 没有被标记,那么 v v v 在点覆盖中。

综上,对于任意边 ( u , v ) (u,v) (u,v),总有 u , v u,v u,v 其中一者在点覆盖中。

接下来证明,为什么是最小的。

不难发现,对于点覆盖中任意一个点,它都是一条匹配边的一端,并且没有两个点在同一条匹配边上,这意味着点覆盖数就等于最大匹配数,所以是最小的。

最大独立集等于最小点覆盖的补集

假设最大独立集点数为 x x x,最小点覆盖点数为 y y y,总点数为 n n n,那么做题时我们常常用到的式子其实是 x = n − y x=n-y x=ny

证明的话,对于最小点覆盖的补集,不可能存在一条边,他的两端都在补集内,因为这意味着最小点覆盖中没有覆盖到这条边。

也就是说,最小点覆盖的补集至少是个合法的独立集,而由于点覆盖是最小的,所以独立集就是最大的。

简单应用2

二分图最小权点覆盖

具体一点,问题是这样的:

给一张二分图,选若干个点使他们覆盖所有边,并且使选出来的点的权值和最小。(权值都是正数,下面都是)

简单应用1中提到了一个最小割的思想,我们拿过来用。

依然是先建一个源点和一个汇点,源点和左部点连边,汇点和右部点连边,不同的是,流量不是 1 1 1,而是这个点的权值。然后把原图中的边连上,变成有向边从左边往右边连,流量无限,那么这张图的最小割就是最小点权和,可以用最大流算法求出。

二分图最大权独立集

由于点覆盖的补集一定是独立集,所以最小权点覆盖取补集就是最大权独立集了。

二分图最大权点覆盖和最小权独立集(

当时脑子一抽,想着把点权取负跑最大流,然而并不能跑,也不能加偏移量变成正数,于是问机房巨佬,然后就很惨烈的被骂了一句:

这tm不是全部点都选上就最大了吗?!

呜呜呜我果然是个傻逼QAQ……

然后同样笨的一个问题——最小权独立集——的答案也是一样的:啥都不选就最小了……

最小/大权最小点覆盖&最小/大权最大独立集

别问我为什么不讲最大点覆盖和最小独立集……

这两个问题我们一样可以用点覆盖的补集一定是独立集这个性质来相互转化,所以只需要知道怎么求最小/大权最小点覆盖即可。

最小权最小点覆盖的话,我们给每个点的权值加上一个 n × v a l m a x n\times val_{max} n×valmax n n n 是点数, v a l m a x val_{max} valmax 是权值上界,然后跑最大流,再将最大流减去 点覆盖的大小乘以 n × v a l m a x n\times val_{max} n×valmax 即可。

这个偏移量的作用是:使得求出来的点覆盖一定是最小的。因为假如多选一个点,在不加偏移量的图中可能会使总权值减小,但是减小的量一定不超过 n × v a l m a x n\times val_{max} n×valmax,而在这个加了偏移量的图中选多一个点就会额外给你加上一个 n × v a l m a x n\times val_{max} n×valmax,一定会使答案更加不优。

当然这个偏移量通过分析可以更紧,但是这类问题本身就冷门(博主本人并没有见过),就算有应该是不会还恶心到卡这个偏移量的。

最大权的话,权值取反就变成了一样的问题了,由于加上了偏移量所以不需要担心负数不能跑最大流的问题。

最小费用最大流

题目没变太多,只是每条边上的流量多了一个费用,在这条边上每流过去 1 1 1 的流量,就要缴纳一次费用,于是问题变成了:在最大流基础上费用最小(大)。

要让费用最大,将费用取相反数跑最小费用流即可,所以下面只看最小费用流。

做法相当暴力,每次用SPFA或dijkstra找到费用最小的路径,然后流尽可能多的流量过去。

没了?

哦,还有一点,反向边的费用是正向边的相反数,没了。

这个做法本质是EK,所以并不快。

模板题

代码(SPFA版):

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 5010

int n,m,S,T;
struct edge{int x,y,z,cost,next;};
edge e[100010];
int first[maxn],len=1;
void buildroad(int x,int y,int z,int cost)
{
	e[++len]=(edge){x,y,z,cost,first[x]};
	first[x]=len;
}
int q[maxn],st,ed,fa[maxn],f[maxn];//f记录最短路,fa记录每个点是从那条边来的
bool v[maxn];
bool SPFA()
{
	memset(fa,0,sizeof(fa));
	memset(f,63,sizeof(f));
	st=1;ed=2;q[st]=S;
	f[S]=0;v[S]=true;
	while(st!=ed)
	{
		int x=q[st++];st=st>n?1:st;//要用循环队列
		for(int i=first[x];i;i=e[i].next)
		{
			int y=e[i].y;
			if(e[i].z>0&&f[x]+e[i].cost<f[y])
			//别忘了判断e[i].z是否大于0,没有流量的边是不能走的
			{
				f[y]=f[x]+e[i].cost;fa[y]=i;
				if(!v[y])v[y]=true,q[ed++]=y,ed=ed>n?1:ed;
			}
		}
		v[x]=false;
	}
	return fa[T];
}

int main()
{
	scanf("%d %d %d %d",&n,&m,&S,&T);
	for(int i=1,x,y,z,cost;i<=m;i++)
	scanf("%d %d %d %d",&x,&y,&z,&cost),
	buildroad(x,y,z,cost),buildroad(y,x,0,-cost);
	int ans_flow=0,ans_cost=0;
	while(SPFA())
	{
		int min_flow=999999999;
		//找到最短路上流量最小的边,它的流量就是这条增广路能流到汇点的最大流量
		for(int i=fa[T];i;i=fa[e[i].x])min_flow=min(min_flow,e[i].z);
		//将这条路上的所有边减去这个流量,然后答案加上相应费用
		for(int i=fa[T];i;i=fa[e[i].x])
		e[i].z-=min_flow,e[i^1].z+=min_flow,
		ans_cost+=e[i].cost*min_flow; ans_flow+=min_flow;
		//注意分号和逗号的区别
	}
	printf("%d %d\n",ans_flow,ans_cost);
}

zkw费用流

zkwの博客

大概是借用了一下KM算法的思想,注意到上面的SPFA相当于每次维护出一个 D i D_i Di 表示源点到 i i i 的最短路,然后沿着 D i + c o s t ( i , j ) = D j D_i+cost(i,j)=D_j Di+cost(i,j)=Dj 的边增广一次。

一次增广完后,由于边上流量的变动,在不改变 D D D 的情况下可能就不存在增广路了,改进的EK(就是上面那种)会选择再次SPFA,而zkw选择稍微修改 D D D

先找到所有满足 u ∈ V , v ∉ V u\in V,v\not \in V uV,vV 的边 ( u , v ) (u,v) (u,v),其中 V V V 是上次增广时经过的点集(也就是从源点沿着 D i + c o s t ( i , j ) = D j D_i+cost(i,j)=D_j Di+cost(i,j)=Dj 的边能走到的点),此时一定有 D u − D v + c o s t ( u , v ) ≥ 0 D_u-D_v+cost(u,v)\geq 0 DuDv+cost(u,v)0,类似KM算法,令 c = min ⁡ { D u − D v + c o s t ( u , v ) ∣ u ∈ V , v ∉ V } c=\min\{D_u-D_v+cost(u,v)|u\in V,v\not \in V\} c=min{DuDv+cost(u,v)uV,vV},然后 ∀ i ∈ V , D i − = c \forall i\in V,D_i-=c iV,Di=c

这样操作过后,至少会多一条边可以增广,反复操作就能找到增广路,当不能增广又找不到这种边时就停止。

然后顺便说一句,不难发现对于一条增广路,他的费用就是 − D S -D_S DS

代码:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 5010

int n,m,S,T,ans_flow=0,ans_cost=0;
struct edge{int y,z,cost,next;}e[100010];
int first[maxn],len=1;
void buildroad(int x,int y,int z,int cost){e[++len]=(edge){y,z,cost,first[x]};first[x]=len;}
void ins(int x,int y,int z,int cost){buildroad(x,y,z,cost);buildroad(y,x,0,-cost);}
int D[maxn];
bool v[maxn];
int aug(int x,int flow){
	v[x]=true;if(x==T)return ans_flow+=flow,ans_cost+=-D[S]*flow,flow;
	for(int i=first[x],p;i;i=e[i].next)
		if(e[i].z&&!v[e[i].y]&&D[x]-D[e[i].y]+e[i].cost==0)
			if(p=aug(e[i].y,min(e[i].z,flow)))return e[i].z-=p,e[i^1].z+=p,p;
	return 0;
}
bool mdf(){
	if(v[T])return true;
	int mi=2147483647;
	for(int i=2;i<=len;i++)
		if(e[i].z&&v[e[i^1].y]&&!v[e[i].y])
			mi=min(mi,D[e[i^1].y]-D[e[i].y]+e[i].cost);
	if(mi==2147483647)return false;
	for(int i=1;i<=n;i++)if(v[i])D[i]-=mi;
	return true;
}

int main()
{
	scanf("%d %d %d %d",&n,&m,&S,&T);
	for(int i=1,x,y,z,cost;i<=m;i++)
		scanf("%d %d %d %d",&x,&y,&z,&cost),ins(x,y,z,cost);
	memset(D,0,sizeof(D));
	do memset(v,false,sizeof(v)),aug(S,2147483647); while(mdf());
	printf("%d %d",ans_flow,ans_cost);
}

上下界网络流

无源汇有上下界可行流

模板题传送门

没有源点和汇点看起来比较诡异,但事实上这个东西是很简单的。

首先当然是新建两个点,超级源和超级汇,没有源汇跑个锤子的网络流。

先考虑满足每条边的下界,也就是强制先流下界这么多的流量,此时对于每个点,统计出从入边得到的流量之和减去从出边流走的流量之和

注意到最后每个点流入的流量和流出的流量是相等的。如果这个差值是正数,表示他得到了一些额外的流量,那么就需要流走这些流量,于是从超级源往这个点连边,流量就是这个差值;如果是负数,那么表示他需要得到一些流量,从这个点往超级汇连边,流量是这个差值的负数。

而对于原图中的边,照样连上就好,流量为上下界的差值。

然后跑一遍最大流,看看源点的出边是否全部满流,如果是,那么就求出了一种可行的流量,每条边的流量加上他的下界就是他的真实流量;如果不是,那么就无解。

代码如下(去掉了网络流板子):

int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1,x,y,l,r;i<=m;i++){
		scanf("%d %d %d %d",&x,&y,&l,&r);
		ins(x,y,r-l);d[y]+=l;d[x]-=l;L[i]=l;//d直接维护差值,L记录边的流量下界
	}
	S=n+1,T=S+1;int ans=0;
	for(int i=1;i<=n;i++){
		if(d[i]>0)ins(S,i,d[i]),ans+=d[i];
		if(d[i]<0)ins(i,T,-d[i]);
	}
	while(bfs())ans-=dfs(S,1e9);
	if(ans)puts("NO");
	else{
		puts("YES");
		for(int i=1;i<=m;i++)
			printf("%d\n",L[i]+e[i<<1|1].z);
	}
}

有源汇有上下界可行流

似乎因为过于没有必要而没有模板题。

就是从汇点往源点连一条下界为 0 0 0,上界为无穷大的边,然后跑无源汇有上下界可行流即可。

有上下界可行流,如果要加上费用的话,其实就是把最大流板子换成费用流板子,没什么特别的。这里就有一个有源汇有上下界最小费用可行流的差不多是板子的题——80人环游世界

连边比较简单,因为经过一个城市有次数限制,考虑拆点,然后中间连一条上下界都为 V i V_i Vi,费用为 0 0 0 的边,然后城市间的边就从出点往入点连,下界为 0 0 0 上界为 ∞ \infty ,然后源点往所有入点连边,所有出点往汇点连边即可。

然后还要满足恰好有 M M M 个人,再建一个超超级源点往源点连一条流量上下界都为 M M M,费用为 0 0 0 的边就好。

代码:

int main()
{
	scanf("%d %d",&n,&m);S1=2*n+1;S2=S1+1;T1=S2+1;S=T1+1;T=S+1;
	for(int i=1,x;i<=n;i++)
		scanf("%d",&x),add_edge(i,i+n,x,x,0);
	//add_edge(x,y,l,r,c)表示 x 向 y 连一条下界为 l 上界为 r 费用为 c 的边
	for(int i=1,x;i<=n;i++)
		for(int j=i+1;j<=n;j++){
			scanf("%d",&x);
			if(x!=-1)add_edge(i+n,j,0,m,x);
		}
	add_edge(S1,S2,m,m,0);add_edge(T1,S1,0,m,0);
	for(int i=1;i<=n;i++)
		add_edge(S2,i,0,m,0),add_edge(i+n,T1,0,m,0);
	for(int i=1;i<S;i++){
		if(d[i]>0)ins(S,i,d[i],0);
		if(d[i]<0)ins(i,T,-d[i],0);
	}
	while(SPFA()){
		int min_flow=1e9;
		for(int i=fa[T];i;i=fa[e[i].x])min_flow=min(min_flow,e[i].z);
		for(int i=fa[T];i;i=fa[e[i].x])
			e[i].z-=min_flow,e[i^1].z+=min_flow,ans+=min_flow*e[i].cost;
	}
	printf("%d",ans);
}

有源汇有上下界最大流

模板题传送门

先建出超级源超级汇跑出可行流,然后在此基础上以题目给的源汇跑一遍最大流即可。

代码:

int main()
{
	scanf("%d %d %d %d",&n,&m,&pS,&pT);
	for(int i=1,x,y,l,r;i<=m;i++){
		scanf("%d %d %d %d",&x,&y,&l,&r);
		ins(x,y,r-l);d[y]+=l;;d[x]-=l;
	}
	ins(pT,pS,1e13);S=pT+1,T=S+1;
	for(int i=1;i<=pT;i++){
		if(d[i]>0)ins(S,i,d[i]),sum+=d[i];
		if(d[i]<0)ins(i,T,-d[i]);
	}
	while(bfs())sum-=dfs(S,1e18);
	if(sum)return puts("please go home to sleep"),0;
	//和可行流是差不多的,就多跑一次最大流
	S=pS;T=pT;ll ans=0;while(bfs())ans+=dfs(S,1e18);
	printf("%lld",ans);
}

有源汇有上下界最小流

和最大流类似,先跑出可行流,然后由于要流量最小,所以接下来从汇点往源点跑最大流,尽可能流回去,假设流了 c c c 回去,跑可行流时我们从汇点往源点连了一条流量为 i n f inf inf 的边,用 i n f − c inf-c infc 就是答案。

代码:

int main()
{
	scanf("%d %d %d %d",&n,&m,&pS,&pT);
	for(int i=1,x,y,l,r;i<=m;i++){
		scanf("%d %d %d %d",&x,&y,&l,&r);
		ins(x,y,r-l);d[y]+=l;;d[x]-=l;
	}
	ins(pT,pS,1e13);S=pT+1,T=S+1;
	for(int i=1;i<=pT;i++){
		if(d[i]>0)ins(S,i,d[i]),sum+=d[i];
		if(d[i]<0)ins(i,T,-d[i]);
	}
	while(bfs())sum-=dfs(S,1e18);
	if(sum)return puts("please go home to sleep"),0;
	//几乎和最大流是一样的,就是将下面的 pS 和 pT 换个位置,然后改改输出而已
	S=pT;T=pS;ll ans=0;while(bfs())ans+=dfs(S,1e18);
	printf("%lld",(ll)1e13-ans);
}

一个例题

首先当然是建源点和汇点,源点向每个点连边,下界为 0 0 0 上界为 ∞ \infty ,每个点向汇点连边,下界为 0 0 0 上界为 ∞ \infty 。然后如果 x x x y y y 有边,那么这条边就要被走至少一次,所以连边流量下界为 1 1 1 上界为 ∞ \infty 。然后跑有源汇有上下界最小流即可。

代码:

int main()
{
	scanf("%d",&n);pS=n+1,pT=n+2;
	for(int i=1,m,x;i<=n;i++){
		scanf("%d",&m);while(m--){
			scanf("%d",&x);
			ins(i,x,2e9-1);d[x]++;d[i]--;
		}
		ins(pS,i,2e9);ins(i,pT,2e9);
	}
	ins(pT,pS,1e13);S=pT+1,T=S+1;
	for(int i=1;i<=pT;i++){
		if(d[i]>0)ins(S,i,d[i]);
		if(d[i]<0)ins(i,T,-d[i]);
	}
	while(bfs())dfs(S,1e18);
	S=pT;T=pS;ll ans=0;while(bfs())ans+=dfs(S,1e18);
	printf("%lld",(ll)1e13-ans);
}

一些乱七八糟的题

虽然想写很多题在这里,嘛……然而是个最近才想搞的东西,事实上并没有很多题(也有许多掺杂在上面的讲解里了,也都是些好题)。

  • 切糕
    先不考虑 D D D 的限制,那么就设超级源 S S S 为第 0 0 0 层,超级汇 T T T 为第 R + 1 R+1 R+1 层, ( x , y , z ) (x,y,z) (x,y,z) ( x , y , z + 1 ) (x,y,z+1) (x,y,z+1) 连边(若 z = n z=n z=n 那么令 z + 1 z+1 z+1 T T T),流量为 v ( x , y , z ) v(x,y,z) v(x,y,z),那么最小割就是答案(每个纵轴一定割掉最小的边)。
    再考虑 D D D 的限制,考虑 ( x , y , z ) (x,y,z) (x,y,z) ( i , j , z − D ) (i,j,z-D) (i,j,zD) 连边,流量无限,其中 ( i , j ) (i,j) (i,j) ( x , y ) (x,y) (x,y) 相邻。这样如果选了 ( x , y , z ) (x,y,z) (x,y,z) 这个点,也就是割了 ( ( x , y , z ) , ( x , y , z + 1 ) ) ((x,y,z),(x,y,z+1)) ((x,y,z),(x,y,z+1)) 这条边,那么就会存在一条 S → ( x , y , z ) → ( i , j , z − D ) → ( i , j , z + D + 1 ) → ( x , y , z + 1 ) → T S\to (x,y,z)\to (i,j,z-D)\to (i,j,z+D+1)\to (x,y,z+1)\to T S(x,y,z)(i,j,zD)(i,j,z+D+1)(x,y,z+1)T 的增广路,那么就必须要再在 ( i , j , z − D ) (i,j,z-D) (i,j,zD) ( i , j , z + D + 1 ) (i,j,z+D+1) (i,j,z+D+1) 间割一条边,这样就满足了限制。
  • SDOI 2014 LIS
  • NOI 2009 植物大战僵尸
    先将植物间连边,若 x x x 可以保护 y y y 那么 x x x y y y 连边,每个植物还要向左边的植物连边,因为不吃掉自己就不可能吃掉自己左边的,那么这也是一种保护。
    然后跑一次拓扑,这样可以去掉那些环上以及环能到达的点,这些植物是不可能被吃掉的,将他们去掉,后面不考虑。
    将剩下的边反过来,那么一条 x x x y y y 的边意味着要吃掉 x x x 必须吃掉 y y y。那么剩下的问题就是最大权闭合子图了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值