文章目录
废话
算上代码好歹有个一万字,但是不敢说这是详解,因为网上的大神们写的相当精彩……
应该看完多少会有点收获吧,个人感觉挺适合入门选手的,看完应该不至于入土。
最大流
先把例题摆出来: 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 1→2→4 和 1 → 3 → 4 1\to 3\to 4 1→3→4 时,能得到最大流 2 2 2,但是如果爆搜搜到了 1 → 2 → 3 → 4 1\to 2\to 3\to 4 1→2→3→4 这样的憨憨增广路,那么流量只有 1 1 1 了。
为了避免这种情况,我们需要引入一个优化:反向弧,也就是对于每条边,建一条反向边,一开始的流量为 0 0 0,当正向边流量减少时,反向边流量相应增加,反向边流量减少时,正向边亦然。
这有什么用呢?来看看有了反向边后,假如找到了
1
→
2
→
3
→
4
1\to 2\to 3\to 4
1→2→3→4 这样的增广路会怎么样:
又要来补充一些概念了……这种被跑过的图,我们称之为残余网络。
此时流量变成了这样,然后发现,我们又可以找到一条增广路: 1 → 3 → 2 → 4 1\to 3\to 2 \to 4 1→3→2→4。
没错, 2 → 3 2\to 3 2→3 的反向边发挥了作用,先走了 2 → 3 2\to 3 2→3,再走了它的反向边,那么这条边相当于没走,所以上面走的两条增广路其实就相当于 1 → 2 → 4 1\to 2\to 4 1→2→4 和 1 → 3 → 4 1\to 3\to 4 1→3→4 这两条。
反向边的作用浮出水面:提供反悔的机会。
要说具体一点的话,就是假如有这样一条不优秀的 S → . . . → a → b → d → . . . → T S\to ...\to a\to b \to d\to ...\to T S→...→a→b→d→...→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→...→b→a→c→...→T, a → b a\to b a→b 这条边的正向边和反向边都被走了一次,相当于没走,所以事实上我们得到的两条增广路为 S → . . . → a → c → . . . → T S\to ...\to a\to c\to ...\to T S→...→a→c→...→T 和 S → . . . → b → d → . . . → T S\to ...\to b\to d\to ...\to T S→...→b→d→...→T。
(不明白可以画个图手玩一下qwq)
加上反向边后,这种最暴力的算法就是EK算法。
但是这样的时间复杂度依然不能让人满意,所以还有几个优化:
- 发现如果要求最大流的话,就要让增广路经过的边数尽可能少,这样每次影响的边数也尽可能少,能找到的增广路就会尽可能多,可以减少反向边的使用次数,尽可能每次都能找到优秀的增广路。
要做到这一点的话,就在每次求增广路前预处理一个 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 那里搜。即对图进行分层。 每次找到一条增广路就重新来
这样的做法太慢了,我们可以去找尽可能多的增广路,直到找不到位为止,然后重新求一次 h h h 数组再来。- 当前弧优化(建议结合代码理解):比如说,上一次搜索到点 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 S→i,i→T,流量对应选文科和选理科的贡献。对于每五个点的一组,建两个新点 A i , B i A_i,B_i Ai,Bi,建边 S → A i , B i → T S\to A_i,B_i\to T S→Ai,Bi→T,流量对应全文和全理的贡献。然后 A i → j , j → B i A_i\to j,j\to B_i Ai→j,j→Bi,流量无限, j j j 是组内的点。
割去 S → i , i → T S\to i,i\to T S→i,i→T 的意义和上面一样,如果一组内要全文,也就是 S → A i S\to A_i S→Ai 不能被割,由于存在 S → A i → j → T S\to A_i\to j\to T S→Ai→j→T 这样的路径, A i → j A_i\to j Ai→j 不可能被割,那么五条 j → T j\to T j→T 都要被割掉,也就是全都不能选理科,意义对应上了;全理是类似的。
练习题。
最大权闭合子图
各种性质&定理
最大流最小割定理
即一张图的最大流容量等于最小割的容量。
补充一下割的定义:割是图中的一些边组成的集合,如果去掉这些边,那么 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=n−y。
证明的话,对于最小点覆盖的补集,不可能存在一条边,他的两端都在补集内,因为这意味着最小点覆盖中没有覆盖到这条边。
也就是说,最小点覆盖的补集至少是个合法的独立集,而由于点覆盖是最小的,所以独立集就是最大的。
简单应用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费用流
大概是借用了一下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 u∈V,v∈V 的边 ( 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 Du−Dv+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{Du−Dv+cost(u,v)∣u∈V,v∈V},然后 ∀ i ∈ V , D i − = c \forall i\in V,D_i-=c ∀i∈V,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 inf−c 就是答案。
代码:
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,z−D) 连边,流量无限,其中 ( 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,z−D)→(i,j,z+D+1)→(x,y,z+1)→T 的增广路,那么就必须要再在 ( i , j , z − D ) (i,j,z-D) (i,j,z−D) 到 ( 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。那么剩下的问题就是最大权闭合子图了。