详解网络流
本蒟蒻第一次学网络流,如果有错误,欢迎各位大佬指出!
基础与定义
流网络
\(G=(V,E)\)。

其中 \(s\) 为源点,\(t\) 为汇点,边上的数字为容量。
可行流
定义
可行流表示为 \(f\),\(c\) 为容量。一个可行流 \(f\) 要满足容量限制和流量守恒两条限制。
- 容量限制:
- 流量守恒:

定义 \(|f|\) 为可行流 \(f\) 的流量值。
举例

这样的就是一个可行流,比较每个点(不包括源点和汇点)流出的流量和流入的流量,发现它们是相等的,且每条边的流量不超过容量。
注意:一个图中可能有很多的可行流。
最大流:一个集合里流量最大的一个可行流。
加减法
一个流网络中两个可行流的加法就是对应边相加,减法类似。
流的分解定理
任意可行流 \(f\) 均可以分解为有限个从源到汇的路径和环的组合。
残留网络
定义:
\(V_f=V,E_f=E+E_{\text{rev}}\)
记作 \(G_f\)。
残留网络是和可行流一一对应的,可行流不同,残留网络也不同。

像这两个流网络,下面的就是上面的残留网络。
\(f+f'\) 也是原网络 \(G\) 的一个可行流。
\(|f+f'|=|f|+|f'|\)

增广路径
定义:在残留网络里面,从源点出发,沿着容量大于 \(0\) 的边,如果能够走到汇点的话,那么这条路径就被称为增广路径。
若流网络 \(G\) 的一条可行流 \(f\) 的残留网络 \(G_f\) 中不存在增广路径的话,那么 \(f\) 就是 \(G\) 的最大流。
割
定义
将点集 \(V\) 分成两个集合 \(S,T\),使得 \(S\cup T=V,S\cap T=\varnothing\),且 \(s\in S,t\in T\)。
容量
所有从 \(S\) 指向 \(T\) 的边的容量之和。
注意:只算从 \(S\) 指向 \(T\) 的边。
最小割:割的容量最小的那一种分割方案。
分割方案数:\(2^{n-2}\) 种。
流量
性质:\(\forall[S,T]\forall f\)
-
\(c(S,T)\ge f(S,T)\)。
-
割的流量 \(f(S,T)=\) 可行流的流量 \(|f|\)。
所以最大流 \(\le\) 最小割。
最大流最小割定理
对于某一个流网络 \(G(V,E)\),以下三个条件是等价的:
-
可行流 \(f\) 是最大流;
-
可行流 \(f\) 的残留网络中不存在增广路径;
-
存在某个割 \([S,T]\),使得 \(|f|=c(S,T)\)。
最大流
算法模板
EK 算法
while( ){
1.找增广路径(BFS)
2.更新残留网络
}
对于更新残留网络:

时间复杂度:\(O(nm^2)\)。
但是
yxc:网络流算法的时间复杂度跟放屁一样,它的实际效率是非常高的。
存图:第 \(0\) 条边是正向边,第 \(1\) 条边是第 \(0\) 条边的反向边,以此类推。

如果要找 \(i\) 这条边的反向边就是 \(i\oplus 1\)。
唐氏马蜂,请勿吐槽。
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=1010,M=20010,inf=0x3f3f3f3f;//这里的inf只要比10000大就行
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;//f为容量
int q[N],d[N],pre[N];//d为从起点走到当前点的所有边上的容量最小值,pre为前驱边
bool st[N];
void add(int a,int b,int c){
//建正向边
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
//建反向边
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(st,0);
q[0]=S;
st[S]=true;
d[S]=inf;
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i/*等价于i!=-1*/;i=ne[i]){
int ver=e[i];
if(!st[ver] && f[i]>0){
st[ver]=true;
d[ver]=min(d[t],f[i]);
pre[ver]=i;
if(ver==T) return true;//找到了增广路径
q[++tt]=ver;
}
}
}
return false;
}
int EK(){
int r=0;
while(bfs()){
r+=d[T];
for(int i=T;i!=S;i=e[pre[i]^1]/*这条边的编号为pre[i],反向边就是pre[i]^1,反向边指向的点的编号就是e[pre[i]^1]*/){
f[pre[i]]-=d[T];
f[pre[i]^1]+=d[T];
}
}
return r;
}
int main(){
cin>>n>>m>>S>>T;
mems(h,-1);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
cout<<EK();
return 0;
}
注:如果你想在洛谷上 AC 此题,请开 long long,并扩大 inf。
dinic 算法
EK 算法每次只会搜索流网络中的一条增广路径,但是一个流网络中可能有很多增广路径,dinic 算法的优化思路就是每次将所有增广路径全部搜索一遍。
但是如果有环的话,dinic 算法就会死循环,所以我们引入了分层图的概念。
也就是说,我们把一张图做成一层一层的,并严格要求每次在寻找增广路径的时候,从起点开始,只能从前一层走到后一层,直到走到终点,这样就会保证一定没有环路。
这个点的层数我们定义为到起点的最短距离。
所以 dinic 算法的思路就是:
-
BFS,建立分层图;
-
DFS,在分层图中搜索全部增广路径。
当前弧优化:
假设现在已经建立完分层图,那么我们维护从起点到这个点可流过流量的最大值,也就是容量的最小值,当这个点继续往后搜的时候,再看一下从这个点到终点的所有路径中,最多可以流过多少流量,再比较它的一条临边到终点能流过的流量,如果这条边能流过的流量已经满了的话,那么以后枚举到这个点的时候就不需要再枚举这条临边了。
我们用水管来理解,比如这个节点相邻的一条水管已经满了的话,那么考虑方案的时候就不用再考虑这条水管了。
注:dinic 对优化是极其敏感的,如果一个优化写错了,或者少写一个优化都有可能 TLE。
参考代码:
//与EK算法相同的地方不再写注释
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=10010,M=200010,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];//d为分层图层数,cur为当前弧优化,表示当前这个点的第一个没有满的临边
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];//当前弧初始化成第一条边
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;//更新最短距离,be like SPFA
cur[ver]=h[ver];//更新这个点的当前弧
if(ver==T) return true;//找到了增广路径
q[++tt]=ver;
}
}
}
return false;
}
int find(int u/*从u这个点开始搜*/,int limit/*从起点能够流向u这个点的最大流量*/){
if(u==T) return limit;
int flow=0;//表示从u这个点向后流过最大的流量
for(int i=cur[u];~i && flow<limit/*后面流的最大流量要保证小于前面流过的最大流量,如果等于的话,也就没有必要再搜索了*/;i=ne[i]){
cur[u]=i;//将当前弧设为i,因为i之前的边已经满了,否则不会搜到i这条边
int ver=e[i];
if(d[ver]==d[u]+1/*按分层图的顺序*/ && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;//如果当前这个点不可用(可流过的流量为0),就把这个点删掉
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())//bfs函数即可以建立分层图,还可以返回有没有增广路径
while(flow=find(S,inf)) r+=flow;//如果有增广路径,就在当前的分层图中累加全部增广路径的流量,如果不套while的话,有时会因为inf太小榨不干
return r;
}
int main(){
cin>>n>>m>>S>>T;
mems(h,-1);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
cout<<dinic();
return 0;
}
放屁时间复杂度:\(O(n^2m)\)。
应用概括
问:给我们一个问题 \(P\),该如何把它转化为一个流网络 \(G\) 呢?
我们要证明下面两个条件:
-
问题 \(P\) 中任意一个可行解都可以在流网络 \(G\) 中对应一条可行流。
-
流网络 \(G\) 中的任意一条可行流都可以在问题 \(P\) 中对应一个可行解。

证明完这两个条件后,问题 \(P\) 中可行解的最大值就可以转化成流网络 \(G\) 中的最大流。
如果要求最小解,就可以对应到最小割。
二分图
相比较我们之前学过的二分图最大匹配算法,最大流算法会更快。
比如说比较匈牙利算法和 dinic 算法。
匈牙利算法时间复杂度:\(O(nm)\)。
dinic 算法时间复杂度:\(O(m\sqrt{n})\)。
那么问题来了:用网络流解决二分图最大匹配,要怎么建图呢?
比如给了我们两个点集 \(V_1\) 和 \(V_2\),就可以这样建图:
-
建立一个虚拟源点 \(S\),并向点集 \(V_1\) 的每一个点连一条容量为 \(1\) 的边。
-
建立一个虚拟汇点 \(T\),并将点集 \(V_2\) 的每一个点向 \(T\) 连一条容量为 \(1\) 的边。
-
对于两个点集之间的边,就连一条容量为 \(1\) 的边。
注意:这里的前提是流量为整数,否则可行流不能对应到可行解。
但是我们要求的整数值最大流的集合,是在整个最大流集合内的,也就是说,我们要求的答案是算法给出来的答案的子集。
对于这种情况,我们自习观察一下我们的 dinic 算法,发现我们用过的变量都是整数值变量,所以我们求的可行流,也就都是整数值可行流(这很显然,各位大佬应该不用想就知道)。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=110,M=5210,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>m>>n;
S=0,T=n+1;
mems(h,-1);
for(int i=1;i<=m;i++) add(S,i,1);
for(int i=m+1;i<=n;i++) add(i,T,1);
int a,b;
while(scanf("%d%d",&a,&b) && a!=-1) add(a,b,1);
cout<<dinic()<<endl;
for(int i=0;i<idx;i+=2)//枚举所有正向边
if(e[i]>m && e[i]<=n && f[i]==0) printf("%d %d\n",e[i^1],e[i]);//这条边在两个点集中间,且正向边的流量为0(被选了)
return 0;
}
对于这道题,我们知道 \(m\) 个单位,\(n\) 个圆桌,每个单位的人数分别是 \(r_1,r_2,...,r_m\),每个圆桌的容量分别是 \(c_1,c_2,...,c_n\)。
要求:每张圆桌上,不能有两个人在同一个单位。
这题还是一道二分图的问题,\(m\) 个单位可以画 \(m\) 个点,\(n\) 张圆桌可以画 \(n\) 个点,左边每个点向右边每个点连一条容量为 \(1\) 的边。

但是这题的特殊之处就在于:每个点可以选多次。比如说第一个单位有两个人,那么这个点是可以在两条边里的。也就是说,有些边是可以共用某些点的。这道题就是二分图的多重匹配问题。
我们可以参考上一题的建图方式,建立源点和汇点,源点向左边每个点连一条容量为 \(r_i\) 的边(因为这个点可以用 \(r_i\) 次),右边每个点向汇点连一条容量为 \(c_i\) 的边。
下面是一种情况:

题目要求我们输出一种方案,使得所有代表都有地方坐,也就是从源点出发的边都是满流。
那么问题有没有解,就相当于流网络存不存在一条可行流,使得从源点出发的边都是满流。其实就是看一下流网络的最大流,从源点出发的边是不是都是满流。
也就是计算最大流是不是等于总人数。
输出方案的时候,只要看一下从左边出发的每条边,哪些边是满流的,就说明左边这个单位向右边那张桌子派了一名代表。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=430,M=(150*270+N)*2,inf=0x3f3f3f3f;
int m,n,S,T;
int h[N],e[M],f[M],ne[M],idx;
int q[N],d[N],cur[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>m>>n;
S=0;
T=m+n+1;
mems(h,-1);
int tot=0;//总人数
for(int i=1;i<=m;i++){
int c;
scanf("%d",&c);
add(S,i,c);
tot+=c;
}
for(int i=1;i<=n;i++){
int c;
scanf("%d",&c);
add(i+m,T,c);
}
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++) add(i,j+m,1);
if(dinic()!=tot) puts("0");//无解
else{
puts("1");
for(int i=1;i<=m;i++){
for(int j=h[i];~j;j=ne[j])
if(e[j]>m && e[j]<=m+n && !f[j]) printf("%d ",e[j]-m);
puts("");
}
}
return 0;
}
上下界可行流
无源汇上下界可行流
原先的流量要满足的条件是:\(0\le f(u,v)\le c(u,v)\)。
现在的流量要满足的条件是:\(c(u,v)\le f(u,v)\le d(u,v)\)。
为了方便,下面把 \(c(u,v)\) 叫做 \(c_l(u,v)\),\(d(u,v)\) 叫做 \(c_u(u,v)\)。
设原网络是 \(G\),原网络的一条可行流是 \(f\),我们希望把 \((G,f)\) 变成 \((G',f')\),使得 \(G'\) 符合我们平时见到的流网络。
原网络的条件为:\(c_l(u,v)\le f(u,v)\le c_u(u,v)\)。
将每一项都减去 \(c_l(u,v)\),得:\(0\le f(u,v)-c_l(u,v)\le c_u(u,v)-c_l(u,v)\),这样就是我们平常见到的流网络的流量限制了。
然后我们再将求出来的方案加上 \(c_l(u,v)\),这样得到的流网络满足容量限制,但是不一定满足流量守恒,比如说这样的一个局部:

每条边的容量都加上 \(c_l(u,v)\) 的话,就会将进入的流量多减 \(C_{\text{out}}=\sum_{v\in V} c_l(x,v)\),多加 \(C_{\text{in}}=\sum_{v\in V} c_l(v,x)\),很有可能这个点出去的流量和进入的流量不相等。
其实我们只要从源点或汇点连一条边补上流量就可以了,如果 \(C_{\text{out}}>C_{\text{in}}\),只需要从源点连一条流量为 \(C_{\text{out}}-C_{\text{in}}\) 的边,那么现在的流量就守恒了。
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=210,M=(10200+N)*2,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],f[M],l[M],ne[M],idx;
int q[N],d[N],cur[N],A[N];//A[i]表示这个点进入的容量下界之和-出去的容量下界之和
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=d-c;
l[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(!t) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>n>>m;
S=0;
T=n+1;
mems(h,-1);
for(int i=0;i<m;i++){
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
add(a,b,c,d);
A[a]-=c;
A[b]+=c;
}
int tot=0;//从源点出发到每个点的容量下界之和
for(int i=1;i<=n;i++){
if(A[i]>0){
add(S,i,0,A[i]);//从源点连边
tot+=A[i];
}
else if(A[i]<0)
add(i,T,0,-A[i]);//向汇点连边
}
if(dinic()!=tot) puts("NO");//不是满流,就说明无解
else{
puts("YES");
for(int i=0;i<m*2;i+=2) printf("%d\n",f[i^1]+l[i]);
}
return 0;
}
有源汇上下界最大流
这题规定了源点和汇点,从上一题的所有点都是流量守恒(不算自己增加的点),变成了除了源点和汇点,都是流量守恒。为了解决这个问题,我们可以从汇点向源点连一条容量为 \(+\infty\) 的边。
问:如何求最大流?
我们很容易想到下面的做法:先按上一题的做法转换流网络,并删掉那条刚加的边,再做一遍 dinic,就可以保证 \(s\) 到 \(t\) 没有增广路径,所以就求出了最大流。

但是这个证明是错误的,因为我们求出的残留网络是从 \(S\) 到 \(T\) 的残留网络,而我们要求的是从 \(s\) 到 \(t\) 的最大流,所以不能这样想。
我们选择原图 \(G\) 的一条可行流 \(f_0\),通过之前的变换,成为图 \(G'\) 中特定的一条可行流 \(f_0'\),然后求一下 \(f_0'\) 的残留网络 \(G'_{f_0'}\),那么这个残留网络中 \(S\) 到 \(T\) 的满流就是 \(f_0'\),那么这个可行流里面有一部分的流就是从 \(s\) 到 \(t\) 流过去的,流量就是之前加上的那条从 \(t\) 到 \(s\) 的流量,记为 \(f_{0\ s\rarr t}'\),下面我们从 \(G'_{f_0'}\) 中找到所有 \(s\) 到 \(t\) 的可行流(右),再找到原图的所有可行流(左),我们只需要证明这两个集合中的可行流一一对应。

我们可以这样证明:左集合中任意一个元素可以在右集合中找到,右集合中的任意一个元素也可以再左集合中找到。
右到左:现在从右集合任取一个可行流,记为 \(f'_{s\rarr t}\),那么不难发现 \(f'_{s\rarr t}+f_0'\) 仍然是一个 \(G'\) 的满流,也就是原图的一条可行流,所以说右边集合的任意一个元素可以找到左边集合的一个元素对应。
左到右:首先左集合的一条可行流 \(f\) 通过变换可以得到 \(G'\) 中的一个满流 \(f'\),因为 \(G'\) 中的所有流都是满流,所以 \(f'-f_0'\) 中从 \(S\) 出发的所有边和流向 \(T\) 的所有边的流量都是 \(0\),所以说所有的流量都在中间部分,也就是 \(s\) 到 \(t\) 的流量,那么左集合中的所有元素就可以对应到右集合的所有元素。

结论:如果要求左集合的最大值就相当于在右集合中求最大值。
如果要求最小流的话就是让 \(s\) 到 \(t\) 的流尽可能小,也就是让 \(t\) 到 \(s\) 的流尽可能大,就是求 \(t\) 到 \(s\) 的最大流。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=210,M=(N+10000)*2,inf=0x3f3f3f3f;
int n,m,S,T;//S,T是虚拟的源汇
int h[N],e[M],f[M],ne[M],idx;
int q[N],d[N],cur[N],A[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
int s,t;//题目给定的源汇
cin>>n>>m>>s>>t;
S=0,T=n+1;
mems(h,-1);
while(m--){
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
add(a,b,d-c);
A[a]-=c;
A[b]+=c;
}
int tot=0;
for(int i=1;i<=n;i++){
if(A[i]>0){
add(S,i,A[i]);
tot+=A[i];
}
else if(A[i]<0) add(i,T,-A[i]);
}
add(t,s,inf);
if(dinic()<tot) puts("No Solution");
else{
int ans=f[idx-1];//上面加的那条边的流量(因为满流,所以流量等于容量)
S=s;
T=t;
f[idx-1]=f[idx-2]=0;//删边
cout<<ans+dinic();
}
return 0;
}
有源汇上下界最小流
\(s\) 到 \(t\) 的最大流减去 \(t\) 到 \(s\) 的最大流。
参考代码:
#include<bits/stdc++.h>
#define int long long
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=50010,M=(N+125003)*2,inf=1e18;
int n,m,S,T;
int h[N],e[M],f[M],ne[M],idx;
int q[N],d[N],cur[N],A[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
signed main(){
int s,t;
cin>>n>>m>>s>>t;
S=0;
T=n+1;
mems(h,-1);
while(m--){
int a,b,c,d;
scanf("%lld%lld%lld%lld",&a,&b,&c,&d);
add(a,b,d-c);
A[a]-=c;
A[b]+=c;
}
int tot=0;
for(int i=1;i<=n;i++){
if(A[i]>0){
add(S,i,A[i]);
tot+=A[i];
}
else if(A[i]<0) add(i,T,-A[i]);
}
add(t,s,inf);
if(dinic()<tot) puts("No Solution");
else{
int ans=f[idx-1];
S=t;
T=s;//反向求最大流
f[idx-1]=f[idx-2]=0;
cout<<ans-dinic();
}
return 0;
}
多源汇最大流

见图知思路,对于刚刚学完上下界的我们来说,这就是易如反掌。(
参考代码:
#include<bits/stdc++.h>
#pragma GCC optimize(3,"Ofast","inline")
#define endl '\n'
#define mems(a,b) memset(a,b,sizeof a)
#ifdef PII
#define x first
#define y second
#endif
using namespace std;
template<typename T>
using _heap=priority_queue<T,vector<T>,greater<T>>;
const int N=10010,M=(100000+N)*2,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
int sc,tc;
cin>>n>>m>>sc>>tc;
S=0,T=n+1;
mems(h,-1);
while(sc--){
int x;
scanf("%d",&x);
add(S,x,inf);
}
while(tc--){
int x;
scanf("%d",&x);
add(x,T,inf);
}
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
cout<<dinic();
return 0;
}
关键边
抽象化题意:求有多少条边的容量变大后,整个流网络的最大流也会变大。
-
求最大流 \(f\)。
-
若 \(f(u,v)=c(u,v)\),且在残留网络 \(G_f\) 中,存在从 \(S\) 到 \(u\) 的路径和从 \(v\) 到 \(T\) 的路径,那么答案++(路径:沿着剩余容量不等于 \(0\) 的边)。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=510,M=10010,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
bool viss[N],vist[N];//viss表示在残留网络中,是否存在S到u的路径,vist同理
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=1;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]){
int t=find(ver,min(f[i],limit-flow));
if(!t) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
void dfs(int u/*枚举到的点*/,bool st[],int t/*是否用反向边*/){
st[u]=true;
for(int i=h[u];~i;i=ne[i]){
int j=i^t,ver=e[i];
if(f[j] && !st[ver]) dfs(ver,st,t);//容量不等于0,且没走到过
}
}
int main(){
cin>>n>>m;
S=0;
T=n-1;
mems(h,-1);
for(int i=1;i<=m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
dinic();
dfs(S,viss,0);
dfs(T,vist,1);
int ans=0;
for(int i=0;i<m*2;i+=2)
if(!f[i] && viss[e[i^1]] && vist[e[i]]) ans++;
cout<<ans;
return 0;
}
最大流判定
概括:给你一个 \(N\) 个点,\(P\) 条边的无向图,每条边只能走一次,要从 \(1\) 号点到 \(N\) 号点走 \(T\) 次,问:最大边权的最小值。
我们假设答案是 \(x\),那么如果只走边权属于 \([1,y_0]\ (y_0<x)\) 的边,一定不能走到,因为如果能走到的话,\(x\) 会更小。如果只走边权属于 \([1,y_1]\ (y_1>x)\) 的边,一定可以走到,所以说答案具有二段性,所以我们可以通过二分来求出答案。

如果我们二分出来的值是 \(m\) 的话,我们需要判断:如果只走边权属于 \([1,m]\) 的边,能不能从 \(1\) 号点到 \(N\) 号点走 \(T\) 次,如果可以,那么就到左边二分,否则就到右边二分。
可以先把长度大于 \(m\) 的边删掉,然后问题就变成了:给定一个无向图,判断是否存在 \(T\) 条从 \(1\) 号点到 \(T\) 号点相互之间没有公共边的路径。然后我们再将这个问题转化为网络流问题。
由于每条边只能走一次,所以每条边的容量为 \(1\);将 \(1\) 号点当作源点,将 \(N\) 号点当作汇点;对于无向图,我们可以建两条有向边;然后求最大流,流量就是有几条路径,所以只需要判断一下流量和 \(T\) 的大小关系就可以了。
问题 \(1\):原问题说正反两条边只能流一次,但是我们建的流网络是正反各能流一次,这两个等价吗?我们仔细想想,正着流一次,反着再流一次,就相当于没流,所以如果出现了这种情况,只需要删除两条边的流量就可以了。
问题 \(2\):建立残留网络的时候,是不是要建四条边?当我们建立流网络的时候,如果两条边重合,那么就相当于容量相加的一条边,所以我们只需要建立两条容量为 \(1\) 的边就可以了。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=210,M=80010,inf=0x3f3f3f3f;
int n,m,K,S,T;//原题的T变为K
int h[N],e[M],ne[M],f[M],w[M],idx;//w表示长度
int q[N],d[N],cur[N];
void add(int a,int b,int c){
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
w[idx]=c;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[t]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
bool check(int mid){
for(int i=0;i<idx;i++){
if(w[i]>mid) f[i]=0;//删掉边
else f[i]=1;
}
return (dinic()>=K);
}
int main(){
cin>>n>>m>>K;
S=1;
T=n;
mems(h,-1);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
int l=1,r=1e6;
while(l<r){
int mid=l+r>>1;
if(check(mid)) r=mid;//mid是可行的答案
else l=mid+1;//mid是不可行的答案
}
cout<<r;
return 0;
}
题目概述(为了方便,将 \(-1\) 号点当作 \(n+1\) 号点):有 \(m\) 辆公交车,在 \([0,n+1]\) 这个区间内按特定顺序移动,每移动一次花费 \(1\) 天,第 \(p_i\) 辆公交车最多能拉 \(h_{p_i}\) 人,问 \(k\) 个人从 \(0\) 号点到 \(n+1\) 号点需要多长时间。
判断有没有解:可以用并查集判断(\(0\) 号点与 \(n+1\) 号点是否连通)。
寻找答案:如果从 \(0\) 号点到 \(n+1\) 号点需要 \(d\) 天的话,那么我们就建立 \(d+1\) 层图,第 \(0\) 天到第 \(d\) 天,每一层有 \(n+2\) 个点,代表 \(0\) 号点到 \(n+1\) 号点。源点向 \((0,0)\) 连一条容量为 \(k\) 的边,每一层的第 \(n+1\) 号点向汇点连一条容量为 \(+\infty\) 的边,每辆公交车按路线跨一天连一条容量为 \(r_i\) 的边,每个点向明天的自己连一条容量为 \(+\infty\) 的边。

图有点乱,再总结一下:分为三类边,第一类是源汇连边,从源点向第 \(0\) 天的第 \(0\) 号点连一条容量为 \(k\) 的边,从每天的第 \(n+1\) 号点向汇点连一条容量为 \(+\infty\) 的边;第二类是公交边,按照每辆公交的运行顺序,向明天的下一个点连一条容量为 \(h_{p_i}\) 的边;第三类是等待边,因为每个站台都可以容纳无穷个人,所以每个点向明天的这个点连一条容量为 \(+\infty\) 的边。
关于 \(d\),因为流网络的点数是和 \(d\) 成正比的,所以可以直接增加,再对剩下的增广。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=1101*22+10,M=(N+1101+13*1101)*2+10,inf=0x3f3f3f3f;
int n,m,k,S,T;
int h[N],f[M],e[M],ne[M],idx;
int q[N],d[N],cur[N];
struct ship{
int h,r,id[30];
}ships[30];
int p[30];//并查集
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int get(int i/*空间站*/,int d/*天数*/){//获得编号
return d*(n+2)+i;
}
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[t]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>n>>m>>k;
S=N-2;
T=N-1;
mems(h,-1);
for(int i=0;i<30;i++) p[i]=i;
for(int i=0;i<m;i++){
int a,b;
scanf("%d%d",&a,&b);
ships[i]={a,b};
for(int j=0;j<b;j++){
int id;
scanf("%d",&id);
if(id==-1) id=n+1;
ships[i].id[j]=id;
if(j>0){//合并
int x=ships[i].id[j-1];//上一个点
p[find(x)]=find(id);
}
}
}
if(find(0)!=find(n+1)) puts("0");//无解
else{
//初始的源汇边
add(S,get(0,0),k);
add(get(n+1,0),T,inf);
int day=1,ans=0;
while(1){
add(get(n+1,day),T,inf);//今天的源汇边
for(int i=0;i<=n+1;i++) add(get(i,day-1),get(i,day),inf);//等待边
for(int i=0;i<m;i++){
int r=ships[i].r;
int a=ships[i].id[(day-1)%r],b=ships[i].id[day%r];
add(get(a,day-1),get(b,day),ships[i].h);//公交边
}
ans+=dinic();//增广新加的
if(ans>=k) break;
day++;
}
cout<<day;
}
return 0;
}
拆点
人话:三分图匹配。

我们很容易想到这样做:源点向每个食品连一条容量为 \(1\) 的边,每个饮料向汇点连一条容量为 \(1\) 的边,匹配的再连一条容量为 \(1\) 的边。但是这样做是不对的,比如一头牛同时匹配了两种食品和三种饮料,那么它就会吃掉两份餐。
所以我们可以把每头奶牛拆成两个点,两点之间连一条容量为 \(1\) 的边,这样就可以确保每头奶牛只吃一份餐了。
拆点通常会拆成两个点:入点和出点,所有连向原来点的边连向入点,所有从原来点出去的边从出点出去。所以只需要把每头牛拆成出入两点,左连入点,出点连右。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=410,M=40610,inf=0x3f3f3f3f;
int n,F,D,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>n>>F>>D;
S=0;
T=n*2+F+D+1;
mems(h,-1);
for(int i=1;i<=F;i++) add(S,n*2+i,1);//源点
for(int i=1;i<=D;i++) add(n*2+F+i,T,1);//汇点
for(int i=1;i<=n;i++){
add(i,n+i,1);
int a,b,t;
scanf("%d%d",&a,&b);
while(a--){
scanf("%d",&t);
add(n*2+t,i,1);//食物
}
while(b--){
scanf("%d",&t);
add(i+n,n*2+F+t,1);//饮料
}
}
cout<<dinic();
return 0;
}
第一问:直接 DP。
第二问:枚举 DP 数组的每一个元素,对于每一个元素再遍历一遍数组,如果 \(g(i)=g(j)+1\) 的话,从 \(j\) 向 \(i\) 连一条容量为 \(1\) 的边,因为每个点只能选一次,所以拆点,中间连一条容量为 \(1\) 的边。源点向每个 \(g(i)=1\) 的点连边,所有 \(g(i)=s\) 的点向汇点连边。
第三问:(对于第 \(1\) 个点和第 \(n\) 个点)将入点连向出点的边的容量设为 \(+\infty\),源点和汇点的连边容量也设为 \(+\infty\)。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=1010,M=251010,inf=0x3f3f3f3f;
int n,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
int g[N],w[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>n;
S=0;
T=n*2+1;
mems(h,-1);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
int s=0;
for(int i=1;i<=n;i++){
add(i,i+n,1);
g[i]=1;
for(int j=1;j<i;j++)
if(w[j]<=w[i]) g[i]=max(g[i],g[j]+1);
for(int j=1;j<i;j++)
if(w[j]<=w[i] && g[j]+1==g[i]) add(n+j,i,1);
s=max(s,g[i]);
if(g[i]==1) add(S,i,1);
}
for(int i=1;i<=n;i++)
if(g[i]==s) add(n+i,T,1);
cout<<s<<endl;
if(s==1){//因为如果s==1的话,从S到T的流量在第三问中将不受限制,所以需要特判
cout<<n<<endl<<n;
return 0;
}
int ans=dinic();
cout<<ans<<endl;
for(int i=0;i<idx;i+=2){
int a=e[i^1],b=e[i];
if(a==S && b==1) f[i]=inf;
else if(a==1 && b==n+1) f[i]=inf;
else if(a==n && b==n+n) f[i]=inf;
else if(a==n+n && b==T) f[i]=inf;
}
cout<<ans+dinic();
return 0;
}
我们可以把企鹅看成流,然后源点向每个有企鹅的冰块连边,因为我们要求的是汇点,所以枚举汇点。如果两个冰块之间能相互到达,那么就相互连一条容量为 \(+\infty\) 的边。对于冰块起跳限制,我们可以将每个冰块拆成两个点,两个点之间连一条容量为 \(m_i\) 的边。最后只需要判断最大流的流量是不是企鹅的数量就可以了。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
const int N=210,M=20410,inf=0x3f3f3f3f;
const double eps=1e-8;
int n,S,T;
double D;
int h[N],e[M],ne[M],f[M],idx;
int d[N],q[N],cur[N];
PII p[N];//坐标
bool check(PII a,PII b){
int dx=a.x-b.x,dy=a.y-b.y;
return (sqrt(dx*dx+dy*dy)<D+eps);
}
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
int cases;
cin>>cases;
while(cases--){
mems(h,-1);
idx=0;
scanf("%d%lf",&n,&D);
S=0;
int tot=0;//总人数
for(int i=1;i<=n;i++){
int x,y,a,b;
scanf("%d%d%d%d",&x,&y,&a,&b);
p[i]={x,y};
add(S,i,a);
add(i,i+n,b);
tot+=a;
}
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
if(check(p[i],p[j])){//可以跳
add(i+n,j,inf);
add(j+n,i,inf);
}
int cnt=0;
for(int i=1;i<=n;i++){
T=i;
for(int j=0;j<idx;j+=2){//还原网络
f[j]+=f[j^1];
f[j^1]=0;
}
if(dinic()==tot){
printf("%d ",i-1);
cnt++;
}
}
if(cnt==0) puts("-1");
else puts("");
}
return 0;
}
建图
我们先考虑没有第二条的情况,那么这道题就是一个简单的二分图匹配问题,如果加上第二条,也就是每个猪舍之间的转移,可以从顾客向每个猪舍连一条容量为 \(+\infty\) 的边,相当于通过顾客来转移猪。但是这样做会打乱时序,换句话说,所有顾客在同一时间来买猪,所以我们可以删掉猪舍对应的点,直接从先来的顾客向后来的顾客连边。

对于顾客匹配的猪舍,如果猪舍是第一次被打开的话,就从源点向顾客连一条容量为初始猪数量的边,如果不是第一次被打开,可以从上一个打开的顾客向这个顾客连一条容量为 \(+\infty\) 的边,每个顾客向汇点连一条容量为自己购买数量的边。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=110,M=200020,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
int w[1010],last[1010];//猪舍猪的数量和上一次打开这个猪舍的人
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>m>>n;
S=0;
T=n+1;
mems(h,-1);
for(int i=1;i<=m;i++) scanf("%d",&w[i]);
for(int i=1;i<=n;i++){
int a,b;
scanf("%d",&a);
while(a--){
int id;
scanf("%d",&id);
if(!last[id]) add(S,i,w[id]);
else add(last[id],i,inf);
last[id]=i;//更新上一次打开猪舍的人
}
scanf("%d",&b);
add(i,T,b);
}
cout<<dinic();
return 0;
}
最小割
算法模板
通过最大流最小割定理得知,如果 \(f\) 为最大流,那么一定存在一个割使得 \(|f|=c(S,T)\),又因为最小割一定小于所有割,所以 \(|f|=c(S,T)\ge\) 最小割。
又通过割的性质得知,最大流 \(\le\) 最小割,综合得到最大流等于最小割,那么我们只需要再写一遍最大流的代码就可以了。
直接应用
对于这种形式,我们可以随便找一个数 \(\lambda\),如果 \(\displaystyle \frac{\sum w_e}{|C|}<\lambda\),那么
对于最后的变形,因为 \(\sum w_e\) 中一共有 \(|C|\) 个元素,所以就提取了一下。
如果 \(\displaystyle \frac{\sum w_e}{|C|}>\lambda\) 的话,同理。那么给出 \(\lambda\) 后,只需要判断 \(\sum(w_e-\lambda)\) 的最小值是不是大于 \(0\),如果 \(\sum(w_e-\lambda)>0\),那么就说明 \(\displaystyle \frac{\sum w_e}{|C|}>\lambda\),所以说我们要求的最小值要比 \(\lambda\) 大,反之同理,\(\lambda\) 只需要二分就可以了。可以推出想让 \(\displaystyle \frac{\sum w_e}{|C|}\) 最小就相当于要让 \(\sum w_e\) 最小。
这个式子的值只需要把原图 \(G\) 的每条边的值减去 \(\lambda\) 得到一个新图 \(G'\),那么
-
\(w_e'<0\),则必选。
-
对于剩下的边,我们先按下面的方式划分集合,因为我们要选尽量少的边,所以我们不应该选两个集合中的边,之选集合之间的边,那么只需要求最小割就可以了。

参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=110,M=810,inf=0x3f3f3f3f;
const double eps=1e-8;
int n,m,S,T;
int h[N],e[M],ne[M],w[M],idx;
double f[M];
int q[N],d[N],cur[N];
void add(int a,int b,int c){
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
w[idx]=c;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
double find(int u,double limit){
if(u==T) return limit;
double flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
double t=find(ver,min(f[i],limit-flow));
if(t<eps) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
double dinic(double mid){
double ans=0;
for(int i=0;i<idx;i+=2){
if(w[i]<=mid){//必选
ans+=w[i]-mid;
f[i]=f[i^1]=0;
}
else f[i]=f[i^1]=w[i]-mid;
}
double r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r+ans;
}
int main(){
cin>>n>>m>>S>>T;
mems(h,-1);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
double l=0,r=1e7;
while(r-l>eps){
double mid=(l+r)/2;
if(dinic(mid)<0) r=mid;
else l=mid;
}
printf("%.2lf",r);
return 0;
}
在异或运算中,每一位的运算都是独立的,所以我们希望最后的结果最小,就是让每一位最小。设当前看的是第 \(k\) 位,那么所有点只会被分为两类:第 \(k\) 位为 \(0\) 的和第 \(k\) 位为 \(1\) 的,则如果两个点都是第一类,异或后还是 \(0\),如果都是第二类,异或后仍然是 \(0\),如果两个点不同类,异或后就是 \(1\),如果有 \(m\) 个不同类的,那么第 \(k\) 为贡献的答案就是 \(m\cdot 2^k\),我们希望答案尽可能小,就可以让 \(m\) 尽可能小,就相当于让不同类的点尽可能少,现在就可以转化成最小割问题了。
对于原先有编号,如果属于第一类,就从源点向这个点连一条容量为 \(+\infty\) 的边,如果属于第二类,就向汇点连一条容量为 \(+\infty\) 的边,其余边的容量都设为 \(1\)。
做完最小割后,我们可以在残留网络中从源点出发,沿着容量大于 \(0\) 的边搜索,如果能搜到,就是第一类,搜不到就是第二类。
参考代码:
#include<bits/stdc++.h>
#define LL long long
#define mems(a,b) memset(a,b,sizeof a)
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
const int N=510,M=(3000+N*2)*2,inf=0x3f3f3f3f;
int n,m,k,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
int p[N];
PII edges[3010];//存图
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=d;
ne[idx]=h[b];
h[b]=idx++;
}
void build(int k){//建图
mems(h,-1);
idx=0;
for(int i=1;i<=m;i++){
int a=edges[i].x,b=edges[i].y;
add(a,b,1,1);
}
for(int i=1;i<=n;i++)
if(p[i]>=0){
if(p[i]>>k&1) add(i,T,inf,0);
else add(S,i,inf,0);
}
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
LL dinic(int k){
build(k);
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>n>>m;
S=0;
T=n+1;
for(int i=1;i<=m;i++) scanf("%d%d",&edges[i].x,&edges[i].y);
cin>>k;
mems(p,-1);
while(k--){
int a,b;
scanf("%d%d",&a,&b);
p[a]=b;
}
LL ans=0;
for(int i=0;i<=30;i++) ans+=dinic(i)<<i;//可能越界
cout<<ans;
return 0;
}
最大权闭合图
一个点集,要求其中的点不能指向点集外的点,这个点集加上点集中的边,就是闭合(子)图,最大权闭合图就是指这个图的所有闭合图中权值和最大的那个,其中的权值会在点上。
我们先把这个有向图 \(G=(V,E)\) 转化为一个流网络 \(N=(V_N,E_N)\),如果这个点的点权为正,那么就从源点向这个点连一条容量为这个点点权的边;如果点权为负,那么就从这个点向汇点连一条容量为这个点点权的绝对值的边;如果点权为 \(0\),就不用连边;其余边的容量设为 \(+\infty\)。
所以 \(V_N=V+\{s,t\},E_N=\{(s,u)|w_u>0\}\cup\{(u,t)|w_u<0\}\cup E\)。
针对这个问题,我们定义一种割,称为简单割,就是说割边一定与源点或汇点相连的割。不难发现,上面的流网络的最小割就是一个简单割,因为最大流 \(=\) 最小割,而最大流一定是有限的,所以最小割也一定是有限的,那么割边就不能是容量为 \(+\infty\) 的边,也就是中间不与源点和汇点相连的边。
我们将闭合子图的点集设为 \(V'\),割 \([S,T]\) 可以这样分割:\(S=V'+{s},T=V_N-S\),因为从 \(V'\) 中的任何一个点走,只能走到 \(V'\) 中的点,所以如果割边在中间的话,\(S\) 和 \(T\) 间就没有边相连,那么割边就只能与源点或汇点相连,很显然,这些割都是简单割。
如果 \([S,T]\) 是一个简单割,定义 \(V'=S-\{s\}\),那么就不存在一条原图中的边,使得 \(S\) 中的点能走到 \(T\),也就是说,\(S\) 中的点只能走到 \(S\),所以 \(V'\) 就是闭合图。

根据上面的推到得知,闭合图 \(\hArr\) 简单割。我们现在已知一个简单割 \([S,T]\),设 \(V_1=S-\{s\},V_2=V-V_1\),现在将这个割的容量 \(c[S,T]\) 拆分开,可以得到上面的四对,因为这是一个简单割,所以不存在从 \(V_1\) 到 \(V_2\) 的边,显然这个流网络中是不存在 \(s\) 到 \(t\) 的边,下面我们用 \(V_1^+\) 来表示 \(V_1\) 中点权为正的点,\(V_1^-\) 来表示 \(V_1\) 中点权为负的点,那么
两者相加可得
因为 \(w(V^+)\) 是一个定值,所以说如果我们希望 \(w(V_1)\) 最大,就是要让 \(c[S,T]\) 最小,那么答案就是总正权值 \(-\) 最小割。
回到题目,概括题意:一共有 \(n\) 个地点可以建立通讯站,在每个地点建立通讯站的花费是不同的,在第 \(i\) 个地点建立通讯站需要花费 \(p_i\) 元。有 \(m\) 个用户群,满足每个用户群需要建立他们需要的两个通讯站,如果满足了一个用户群,就会获得 \(c_i\) 的收益,问如何建立通讯站,使得总收益最大。
我们可以将每个通讯站看成一个点,点权为 \(-p_i\),每个用户群也看成一个点,点权为 \(c_i\),每个用户群向需要的两个通讯站连边,那么要选一个用户群就需要选两个相连的通讯站,也就是要选闭合图,现在希望权值最大,所以这就是最大权闭合图问题。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=55010,M=(50000*3+5000)*2+10,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>n>>m;
S=0;
T=n+m+1;
mems(h,-1);
for(int i=1;i<=n;i++){
int p;
scanf("%d",&p);
add(m+i,T,p);
}
int tot=0;
for(int i=1;i<=m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(S,i,c);
add(i,m+a,inf);
add(i,m+b,inf);
tot+=c;
}
cout<<tot-dinic();
return 0;
}
最大密度子图
定义:在原图 \(G=(V,E)\) 中选一个点集 \(V'\) 和一个边集 \(E'\),使得 \(E'\) 内任何一条边相连接的两个点都在 \(V'\) 中,且使 \(\displaystyle \frac{|E'|}{|V'|}\) 最大(让选出的子图最稠密)。
最大化分式的值,可以用前面讲过的 01 分数规划解决,每次二分一个值 \(g\),若 \(max(|E'|-g\cdot |V'|)>0\),就等价于 \(\displaystyle \frac{|E'|}{|V'|}>g\),然后根据结果二分。
很显然,如果点集已经固定,那么将点集内的边全部选上,就是最优解。
我们现在考虑如何求 \(max(|E'|-g|V'|)\),通过简单的变换可得,如果要让 \(|E'|-g|V'|\) 最大,就相当于让 \(g|V'|-|E'|\) 最小,现在设 \(d_v\) 为 \(v\) 这个点的度数,那么(\(\overline{V'}\) 是 \(V'\) 的补集)
我们发现 \(\sum_{v\in V'}(2g-d_v)\) 非常烦人,所以我们考虑合理构建图,以将这一项合并到最小割。将 \(d_v\) 看成点权,那么可以让原图中的所有点向汇点连一条容量为 \(2g-d_v\) 的边,让原图的边的容量设为 \(1\),源点向原图中的所有点连一条容量为 \(U\),考虑到 \(2g-d_v\) 可能为负数,所以将所有连向汇点的边也加上 \(U\),我们只要保证 \(U\) 比所有的度数大就可以了,所以 \(U\) 可以直接取 \(m\)。
现在设 \(c[S,T]\) 为流网络的其中一个割,定义 \(V=新图整个点集-\{s,t\},V'=S-\{s\},\overline{V'}=V-V'\),按之前的拆分方法可以得到下面的四种组合,显然不存在 \(s\) 到 \(t\) 的边,所以我们整理剩下的三种组合:

现在设 \(E'\) 为 \(V'\) 中两两相连的边,整理 \(c[S,T]\) 得
我们希望 \(g\cdot |V'|-|E'|\) 最小,因为 \(U\cdot n\) 为定值,所以就等价于让 \(c[S,T]\) 最小,也就是要求最小割,整理答案得 \(ans=\displaystyle \frac{U\cdot n-c[S,T]}{2}\)。
这题要求我们输出方案,在上面的证明中我们知道,\(V'\) 就是一个合法方案,所以我们可以在残留网络中沿着容量大于 \(0\) 的边走,能走到的点就是 \(S\) 中的点,然后将 \(s\) 点删掉就是 \(V'\)。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=110,M=(1000+N*2)*2,inf=0x3f3f3f3f;
const double eps=1e-8;
int n,m,S,T;
int h[N],e[M],ne[M],idx;
double f[M];
int q[N],d[N],cur[N];
int dg[N];//度数
struct node{//存图
int a,b;
}edge[M];
int ans;
bool st[N];
void add(int a,int b,double c,double d){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=d;
ne[idx]=h[b];
h[b]=idx++;
}
void build(double g){//建图
mems(h,-1);
idx=0;
for(int i=1;i<=m;i++) add(edge[i].a,edge[i].b,1,1);
for(int i=1;i<=n;i++){
add(S,i,m,0);
add(i,T,m+2*g-dg[i],0);
}
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
double find(int u,double limit){
if(u==T) return limit;
double flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
double t=find(ver,min(f[i],limit-flow));
if(t<=0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
double dinic(double g){
build(g);
double r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
void dfs(int u){
st[u]=true;
if(u!=S) ans++;
for(int i=h[u];~i;i=ne[i]){
int ver=e[i];
if(!st[ver] && f[i]>0) dfs(ver);
}
}
int main(){
cin>>n>>m;
S=0;
T=n+1;
for(int i=1;i<=m;i++){
int a,b;
scanf("%d%d",&a,&b);
dg[a]++;
dg[b]++;
edge[i]={a,b};
}
double l=0,r=m;
while(r-l>eps){
double mid=(l+r)/2;
double t=dinic(mid);
if(m*n-t>0) l=mid;
else r=mid;
}
dinic(l);
dfs(S);
if(ans==0){
cout<<1<<endl<<1;
return 0;
}
cout<<ans<<endl;
for(int i=1;i<=n;i++)
if(st[i]) printf("%d\n",i);
return 0;
}
最小权点覆盖集
点覆盖集:在一个无向图中,选一个点集,使得每一条边连接的点中至少有一个点在这个点集内,最小权就是让选出来的点的权值和最小。
在一般的图中,这个问题是一种 NPC 问题(只能暴搜得到答案),所以说我们这里只研究二分图的最小权点覆盖集。
因为二分图最大匹配中的边两两没有公共点,所以说最大匹配数 \(=\) 最小点覆盖数,如果点权不为 \(1\) 的话,就只能用网络流来解决。
我们可以让中间的边的容量设为 \(+\infty\),这样就能保证中间不存在割边,换而言之,割边只能出现在左边或右边;源点向左边的点连一条容量为它的点权的边,右边的点向汇点连一条容量为点权的边。

很显然,这个流网络的最小割与最小权点覆盖集是一一对应的。
回到题目,现在将 \(a^-\) 定义为删除从 \(a\) 出发的边的花费,将 \(b^+\) 定义为删除到达 \(b\) 的边的花费,我们发现如果要删除 \(a\) 到 \(b\) 的边,要么操作 \(a^-\),要么操作 \(b^+\),注意到这和我们之前的问题有点相似,所以我们可以按下面的步骤转化。
因为每个点都有两种操作,所以我们将每个点进行拆点,左边为入点(\(+\) 的点),右边为出点(\(-\) 的点),如果这条边是 \(a\) 到 \(b\) 的边,就从左边的 \(b\) 向右边的 \(a\) 连一条边,这样就可以构造出一个二分图,且能转化成最小权点覆盖集问题。
我们先考虑如何输出方案,首先将最大流对应到最小割的方案:在残留网络中,从 \(s\) 出发,沿着容量大于 \(0\) 的边,能走到的点就是集合 \(S\) 中的点,剩下的就是集合 \(T\) 中的点;现在找出所有的割边,如果割边是左边的边,就说明选的是 \(+\) 的点,否则就是选了 \(-\) 的点。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=210,M=5200*2+10,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
bool st[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
void dfs(int u){
st[u]=true;
for(int i=h[u];~i;i=ne[i])
if(f[i] && !st[e[i]]) dfs(e[i]);
}
int main(){
cin>>n>>m;
S=0;
T=n*2+1;
mems(h,-1);
for(int i=1;i<=n;i++){
int w;
scanf("%d",&w);
add(S,i,w);
}
for(int i=1;i<=n;i++){
int w;
scanf("%d",&w);
add(i+n,T,w);
}
while(m--){
int a,b;
scanf("%d%d",&a,&b);
add(b,n+a,inf);
}
cout<<dinic()<<endl;
dfs(S);
int cnt=0;
for(int i=0;i<idx;i+=2){
int a=e[i^1],b=e[i];
if(st[a] && !st[b]) cnt++;//a属于S且b属于T
}
cout<<cnt<<endl;
for(int i=0;i<idx;i+=2){
int a=e[i^1],b=e[i];
if(st[a] && !st[b])
if(a==S) printf("%d +\n",b);
}
for(int i=0;i<idx;i+=2){
int a=e[i^1],b=e[i];
if(st[a] && !st[b])
if(b==T) printf("%d -\n",a-n);
}
return 0;
}
最大权独立集
定义:选出总权值和最大的点集,使得点集中的点两两之间没有边。
在所有点权都等于 \(1\) 时,求最大独立集 \(\Leftrightarrow\) 去掉最少的点,使得所有边都破坏掉 \(\Leftrightarrow\) 去掉最小点覆盖,所以最大独立集 \(=n-\) 最小点覆盖。
如果带上点权,那么显然最大权独立集 \(=\) 总权值 \(-\) 最小权点覆盖集。
观察题目,我们发现下面几个性质:
-
只能在偶数秒拿走宝石。
-
不可能同时拿走相邻格子上的宝石。
现在我们将格子看成点,相邻两个格子连边,那么我们选出的点集一定是一个最大权独立集;又因为这是一个网格图,所以可以将这个图进行染色,那么所有的边一定连接一个红色的点和一个黄色的点,如果将红色格子放到一边,黄色格子放到一边,不难发现,这就是一个二分图。很显然,原问题现在被转化成最大权独立集问题。

现在问题来了:如何将一个独立集转化成合法方案?我们可以两行两行走,如果下一个格子的下一个格子是目标宝石,就判断下一步是奇数秒还是偶数秒,如果是奇数秒,直接走,如果是偶数秒,就停一下再走。可以参考下面的图片,蓝色代表目标宝石,红色圈代表在这个格子停一秒,黑色的线代表路线。

参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=10010,M=60010,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
int get(int x,int y){
return (x-1)*m+y;
}
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>n>>m;
mems(h,-1);
S=0;
T=n*m+1;
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
int tot=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
int w;
scanf("%d",&w);
if(i+j&1){//i+j为奇数
add(S,get(i,j),w);
for(int k=0;k<4;k++){
int x=i+dx[k],y=j+dy[k];
if(x>=1 && x<=n && y>=1 && y<=m) add(get(i,j),get(x,y),inf);
}
}
else add(get(i,j),T,w);//因为这里设左边为奇数点,而最小割只算正向边,所以这里不需要再向四周连边
tot+=w;
}
cout<<tot-dinic();
return 0;
}
建图
枚举 \(s,t\),原问题转化成删掉一些点,使得源点和汇点不连通,但是我们最小割割的是边,不是点,所以可以拆点,两个点之间相互连一条容量为 \(1\) 的边,原图中边的容量设为 \(+\infty\)。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=110,M=5200,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
while(cin>>n>>m){
mems(h,-1);
idx=0;
for(int i=0;i<n;i++){
add(i,n+i,1);
add(n+i,i,1);
}
while(m--){
int a,b;
scanf(" (%d,%d)",&a,&b);
add(a+n,b,inf);
add(b+n,a,inf);
}
int ans=inf;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++){
S=i+n;
T=j;
for(int k=0;k<idx;k+=2){//还原流网络
f[k]+=f[k^1];//反向边的流量流回正向边
f[k^1]=0;//清空反向边
}
ans=min(ans,dinic());
}
if(ans==inf) ans=n;
printf("%d\n",ans);
}
return 0;
}

要求沿着与它相连的任何一条边走,走到的点都在这个点集里,不难看出,这就是一个最大权闭合图问题的板子。
输出方案:从 \(s\) 出发,能遍历到的点就属于 \(S\),否则属于 \(T\),在 \(S\) 中的点就是我们选出的点。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=110,M=5210,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
bool st[N];
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
q[0]=S;
d[S]=0;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
void dfs(int u){
st[u]=true;
for(int i=h[u];~i;i=ne[i])
if(f[i]>0 && !st[e[i]]) dfs(e[i]);
}
int main(){
cin>>m>>n;
S=0;
T=m+n+1;
mems(h,-1);
getchar();//过滤回车
int tot=0;
for(int i=1;i<=m;i++){
int w,id;
string line;
getline(cin,line);//读入整行
stringstream ssin(line);//字符串转换成数字,并根据空格记录在一个“数组”中
ssin>>w;
add(S,i,w);
while(ssin>>id) add(i,m+id,inf);
tot+=w;
}
for(int i=1;i<=n;i++){
int p;
scanf("%d",&p);
add(m+i,T,p);
}
int ans=dinic();
dfs(S);
for(int i=1;i<=m;i++)
if(st[i]) printf("%d ",i);
puts("");
for(int i=m+1;i<=m+n;i++)
if(st[i]) printf("%d ",i-m);
puts("");
cout<<tot-ans;
return 0;
}
我们将所有能够互相攻击到的点连边,原问题转化成:选出一些点,使得选出的点之间不存在边,AUV,这不就是最大独立集吗?
同时,这题也是一个二分图,原问题的配图已经疯狂暗示了,而且每条边连的点一定是一个\(\color{red}红点\)和一个\(\color{#DADA00}黄点\)。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=40010,M=400010,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],idx;
int q[N],d[N],cur[N];
bool g[210][210];
int get(int x,int y){
return (x-1)*n+y;
}
void add(int a,int b,int c){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
h[b]=idx++;
}
bool bfs(){
int hh=0,tt=0;
mems(d,-1);
d[S]=0;
q[0]=S;
cur[S]=h[S];
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(d[ver]==-1 && f[i]>0){
d[ver]=d[t]+1;
cur[ver]=h[ver];
if(ver==T) return true;
q[++tt]=ver;
}
}
}
return false;
}
int find(int u,int limit){
if(u==T) return limit;
int flow=0;
for(int i=cur[u];~i && flow<limit;i=ne[i]){
cur[u]=i;
int ver=e[i];
if(d[ver]==d[u]+1 && f[i]>0){
int t=find(ver,min(f[i],limit-flow));
if(t==0) d[ver]=-1;
f[i]-=t;
f[i^1]+=t;
flow+=t;
}
}
return flow;
}
int dinic(){
int r=0,flow;
while(bfs())
while(flow=find(S,inf)) r+=flow;
return r;
}
int main(){
cin>>n>>m;
S=0;
T=n*n+1;
mems(h,-1);
while(m--){
int x,y;
scanf("%d%d",&x,&y);
g[x][y]=true;
}
int dx[8]={-2,-1,1,2,2,1,-1,-2};
int dy[8]={1,2,2,1,-1,-2,-2,-1};
int tot=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
if(g[i][j]) continue;
if(i+j&1){
add(S,get(i,j),1);
for(int k=0;k<8;k++){
int x=dx[k]+i,y=dy[k]+j;
if(x>=1 && x<=n && y>=1 && y<=n && !g[x][y]) add(get(i,j),get(x,y),inf);
}
}
else add(get(i,j),T,1);
tot++;
}
cout<<tot-dinic();
return 0;
}
平面图转最短路
一些定义
平面嵌入:在平面上可画出的无交叉边的图(可以有曲线)。
可平面图:能转化成平面嵌入发图。
平面图:可平面图的任意一个平面嵌入。
平面图的面:每一个闭合的区域(整个外界也算一个面)。
平面图的边界:一个面的边界。
对偶图:对于一个平面图 \(G\),按下面的步骤建出来的图就是原图的对偶图:
-
在 \(G\) 中每一个面 \(r_i\) 设一个结点 \(v_i\)。
-
过 \(r_i\) 和 \(r_j\) 的公共边界 \(e_k\) 做一条边 \((v_i,v_j)\) 与 \(e_k\) 相交。
-
当 \(e_k\) 只为 \(r_i\) 的边界时,在 \(v_i\) 上作一个环与 \(e_k\) 相交(即割边需要做一个环)。
算法
当原网络 \(G\) 是平面图,那么把容量转化为边权后,再转化成对偶图 \(G'\),那么 \(G\) 的最小割(最大流)就是 \(G'\) 的最短路。这样就可以降低时间复杂度。
暂时没有找到合适的题目。
费用流
含义
题目会给每条边赋予一个边权 \(w(u,v)\),而费用流指所有最大可行流中,费用的最小(大)值。
一个可行流的费用定义为 \(\sum f(u,v)\cdot w(u,v)\)。
算法模板
直接将 EK 算法中的 bfs 函数换成 SPFA 即可。
智障证明(设 \(c(p)\) 为路径 \(p\) 的长度):
假设可行流 \(f\) 是一个最小费用流,流量为 \(v\),且残留网络 \(G_f\) 中无负环,现在按最短路径 \(p\) 增广,得到新流 \(f'\),流量为 \(v+\delta\),则 \(\operatorname{cost}(f'-f)=c(p)\delta\)。假设存在一个可行流 \(f_n\),使得 \(|f_n|=v+\delta\),且 \(f_n\) 的费用小于 \(f'\) 的费用,考虑流差 \(f_n-f\),因为流的分解定理,\(f_n-f\) 可以分解为若干从源到汇的路径和若干环:
-
路径:至少存在一条路径 \(p'\),使得 \(p'\) 的长度小于 \(p\) 的长度,否则 \(f_n\) 的费用不可能小于 \(f\) 的费用。
-
环:由于 \(G_f\) 中不存在负环,所以环贡献的长度和费用一定非负,为了降低费用,一定不选环。
因为 \(p\) 为最短路径,所以不存在 \(p'\) 的长度小于 \(p\) 的长度,所以不存在 \(p'\),那么 \(f'\) 就是最优解。
现在证明加黑部分:
设 \(f_n-f\) 分解出来的路径分别为 \(p_1,p_2,...,p_k\),每条路径 \(p_i\) 携带 \(\delta_i\) 的流量,使得 \(\sum \delta_i=\delta\),则
因为 \(c(p_i)\ge c(p)\),所以路径部分的总费用满足:
所以
这就与 \(f_n\) 的费用比 \(f'\) 的费用小而矛盾,所以这样做是正确的。
在残留网络中,如果正向边的边权为 \(w(u,v)\),那么反向边的边权 \(w(v,u)\) 定义为 \(-w(u,v)\),可以看成退流。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=5010,M=100010,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],w[M],idx;
int q[N],d[N],pre[N],incf[N];//d表示从源点到每个点的长度,incf表示走到每个点时的最大流量
bool st[N];
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=c;
w[idx]=d;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
w[idx]=-d;
ne[idx]=h[b];
h[b]=idx++;
}
bool spfa(){
int hh=0,tt=1;
mems(d,0x3f);
mems(incf,0);
q[0]=S;
d[S]=0;
incf[S]=inf;
while(hh!=tt){
int t=q[hh++];
if(hh==N) hh=0;//循环队列
st[t]=false;//一个点可以多次入队
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(f[i]>0 && d[ver]>d[t]+w[i]){
d[ver]=d[t]+w[i];
pre[ver]=i;
incf[ver]=min(f[i],incf[t]);
if(!st[ver]){
q[tt++]=ver;
if(tt==N) tt=0;
st[ver]=true;
}
}
}
}
return (incf[T]>0);
}
void EK(int &flow,int &cost){
flow=cost=0;
while(spfa()){
int t=incf[T];//流量
flow+=t;
cost+=t*d[T];
for(int i=T;i!=S;i=e[pre[i]^1]){
f[pre[i]]-=t;
f[pre[i]^1]+=t;
}
}
}
int main(){
cin>>n>>m>>S>>T;
mems(h,-1);
while(m--){
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
add(a,b,c,d);
}
int flow,cost;
EK(flow,cost);
cout<<flow<<" "<<cost;
return 0;
}
直接应用
建立虚拟源点和汇点,源点向每个仓库连一条容量为 \(a_i\),费用为 \(0\) 的边;每个商店向汇点连一条容量为 \(b_j\),费用为 \(0\) 的边。对于运输的路径,连一条容量为 \(+\infty\),费用为 \(c_{i,j}\) 的边。

对于最优方案,直接求最小费用最大流,对于最差方案,求最大费用最大流。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=160,M=5150*2+10,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],w[M],idx;
int q[N],d[N],pre[N],incf[N];
bool st[N];
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=c;
w[idx]=d;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
w[idx]=-d;
ne[idx]=h[b];
h[b]=idx++;
}
bool spfa(){
int hh=0,tt=1;
mems(d,0x3f);
mems(incf,0);
q[0]=S;
d[S]=0;
incf[S]=inf;
while(hh!=tt){
int t=q[hh++];
if(hh==N) hh=0;
st[t]=false;
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(f[i]>0 && d[ver]>d[t]+w[i]){
d[ver]=d[t]+w[i];
pre[ver]=i;
incf[ver]=min(incf[t],f[i]);
if(!st[ver]){
st[ver]=true;
q[tt++]=ver;
if(tt==N) tt=0;
}
}
}
}
return (incf[T]>0);
}
int EK(){
int cost=0;
while(spfa()){
int t=incf[T];
cost+=t*d[T];
for(int i=T;i!=S;i=e[pre[i]^1]){
f[pre[i]]-=t;
f[pre[i]^1]+=t;
}
}
return cost;
}
int main(){
cin>>m>>n;
S=0;
T=n+m+1;
mems(h,-1);
for(int i=1;i<=m;i++){
int a;
scanf("%d",&a);
add(S,i,a,0);
}
for(int i=1;i<=n;i++){
int b;
scanf("%d",&b);
add(i+m,T,b,0);
}
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++){
int c;
scanf("%d",&c);
add(i,j+m,inf,c);
}
cout<<EK()<<endl;
for(int i=0;i<idx;i+=2){
f[i]+=f[i^1];
f[i^1]=0;
w[i]=-w[i];//将每条边的费用取反,那么求出来的最小费用最大流就是原先最大的
w[i^1]=-w[i^1];
}
cout<<-EK();//注意这里也要取反
return 0;
}
设分配后每个仓库有 \(\overline{x}\) 个货物,则可以将所有点分为两类:
那么我们可以类比上一题,将第一类点放到左边,将第二类点放到右边,源点向左边的点连一条容量为 \(a_i-\overline{x}\),费用为 \(0\) 的边;右边的点向汇点连一条容量为 \(\overline{x}-a_i\),费用为 \(0\) 的边。每个点向原环中相邻的两个点连一条容量为 \(+\infty\),费用为 \(1\) 的边(这里不保证两个点一个在左边,一个在右边)。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=110,M=610,inf=0x3f3f3f3f;
int n,S,T;
int s[N];
int h[N],e[M],ne[M],f[M],w[M],idx;
int q[N],d[N],pre[N],incf[N];
bool st[N];
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=c;
w[idx]=d;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
w[idx]=-d;
ne[idx]=h[b];
h[b]=idx++;
}
bool spfa(){
int hh=0,tt=1;
mems(d,0x3f);
mems(incf,0);
q[0]=S;
d[S]=0;
incf[S]=inf;
while(hh!=tt){
int t=q[hh++];
st[t]=false;
if(hh==N) hh=0;
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(f[i]>0 && d[ver]>d[t]+w[i]){
d[ver]=d[t]+w[i];
pre[ver]=i;
incf[ver]=min(incf[t],f[i]);
if(!st[ver]){
st[ver]=true;
q[tt++]=ver;
if(tt==N) tt=0;
}
}
}
}
return (incf[T]>0);
}
int EK(){
int cost=0;
while(spfa()){
int t=incf[T];
cost+=d[T]*t;
for(int i=T;i!=S;i=e[pre[i]^1]){
f[pre[i]]-=t;
f[pre[i]^1]+=t;
}
}
return cost;
}
int main(){
cin>>n;
S=0;
T=n+1;
mems(h,-1);
int tot=0;
for(int i=1;i<=n;i++){
scanf("%d",&s[i]);
tot+=s[i];
add(i,(i<n ? i+1 : 1),inf,1);
add(i,(i>1 ? i-1 : n),inf,1);
}
tot/=n;
for(int i=1;i<=n;i++){
if(tot<s[i]) add(S,i,s[i]-tot,0);
else add(i,T,tot-s[i],0);
}
cout<<EK();
return 0;
}
二分图最优匹配
在这类问题中,每条边都会有一个边权,最优匹配指在最大匹配的基础上,边权和最大(最小)的匹配方案。
类比二分图最大匹配的建图方式,只不过中间的边添加费用,费用为 \(c_{i,j}\),其他的边的费用为 \(0\)。
这题建图后,直接求最大费用最大流和最小费用最大流即可。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=110,M=114514,inf=0x3f3f3f3f;
int n,S,T;
int s[N];
int h[N],e[M],ne[M],f[M],w[M],idx;
int q[N],d[N],pre[N],incf[N];
bool st[N];
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=c;
w[idx]=d;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
w[idx]=-d;
ne[idx]=h[b];
h[b]=idx++;
}
bool spfa(){
int hh=0,tt=1;
mems(d,0x3f);
mems(incf,0);
q[0]=S;
d[S]=0;
incf[S]=inf;
while(hh!=tt){
int t=q[hh++];
st[t]=false;
if(hh==N) hh=0;
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(f[i]>0 && d[ver]>d[t]+w[i]){
d[ver]=d[t]+w[i];
pre[ver]=i;
incf[ver]=min(incf[t],f[i]);
if(!st[ver]){
st[ver]=true;
q[tt++]=ver;
if(tt==N) tt=0;
}
}
}
}
return (incf[T]>0);
}
int EK(){
int cost=0;
while(spfa()){
int t=incf[T];
cost+=d[T]*t;
for(int i=T;i!=S;i=e[pre[i]^1]){
f[pre[i]]-=t;
f[pre[i]^1]+=t;
}
}
return cost;
}
int main(){
cin>>n;
S=0;
T=n*2+1;
mems(h,-1);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
int a;
scanf("%d",&a);
add(i,j+n,1,a);
}
for(int i=1;i<=n;i++){
add(S,i,1,0);
add(i+n,T,1,0);
}
cout<<EK()<<endl;
for(int i=0;i<idx;i+=2){
f[i]+=f[i^1];
f[i^1]=0;
w[i]=-w[i];
w[i^1]=-w[i^1];
}
cout<<-EK();
return 0;
}
最大权不相交路径
第一问:每个点向左下、右下的点连一条容量为 \(1\),费用为 \(0\) 的边,因为每个点只能走一次,所以将每个点拆成入点和出点,之间连一条容量为 \(1\),费用为这个数字的边。源点向最上面的点连一条容量为 \(1\),费用为 \(0\) 的边,最下面的点向汇点连一条容量为 \(+\infty\),费用为 \(0\) 的边。
第二问:将拆点边的容量改成 \(+\infty\)。
第三问:将左下、右下边的容量也改成 \(+\infty\)。
直接求最大费用最大流即可。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=1210,M=4010,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],w[M],idx;
int q[N],d[N],pre[N],incf[N];
bool st[N];
int id[40][40],cost[40][40];
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=c;
w[idx]=d;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
w[idx]=-d;
ne[idx]=h[b];
h[b]=idx++;
}
bool spfa(){
int hh=0,tt=1;
mems(incf,0);
mems(d,-0x3f);
q[0]=S;
d[S]=0;
incf[S]=inf;
while(hh!=tt){
int t=q[hh++];
if(hh==N) hh=0;
st[t]=false;
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(f[i]>0 && d[ver]<d[t]+w[i]){
d[ver]=d[t]+w[i];
pre[ver]=i;
incf[ver]=min(incf[t],f[i]);
if(!st[ver]){
st[ver]=true;
q[tt++]=ver;
if(tt==N) tt=0;
}
}
}
}
return (incf[T]>0);
}
int EK(){
int cost=0;
while(spfa()){
int t=incf[T];
cost+=t*d[T];
for(int i=T;i!=S;i=e[pre[i]^1]){
f[pre[i]]-=t;
f[pre[i]^1]+=t;
}
}
return cost;
}
int main(){
int cnt=0;
cin>>m>>n;
S=++cnt;
T=++cnt;
for(int i=1;i<=n;i++)
for(int j=1;j<=m+i-1;j++){
scanf("%d",&cost[i][j]);
id[i][j]=++cnt;//编号
}
//第一问
mems(h,-1);
idx=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m+i-1;j++){
add(id[i][j]*2,id[i][j]*2+1,1,cost[i][j]);
if(i==1) add(S,id[i][j]*2,1,0);
if(i==n) add(id[i][j]*2+1,T,inf,0);// *2表示入点,*2+1表示出点
if(i<n){
add(id[i][j]*2+1,id[i+1][j]*2,1,0);
add(id[i][j]*2+1,id[i+1][j+1]*2,1,0);
}
}
cout<<EK()<<endl;
//第二问
mems(h,-1);
idx=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m+i-1;j++){
add(id[i][j]*2,id[i][j]*2+1,inf,cost[i][j]);
if(i==1) add(S,id[i][j]*2,1,0);
if(i==n) add(id[i][j]*2+1,T,inf,0);
if(i<n){
add(id[i][j]*2+1,id[i+1][j]*2,1,0);
add(id[i][j]*2+1,id[i+1][j+1]*2,1,0);
}
}
cout<<EK()<<endl;
//第三问
mems(h,-1);
idx=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m+i-1;j++){
add(id[i][j]*2,id[i][j]*2+1,inf,cost[i][j]);
if(i==1) add(S,id[i][j]*2,1,0);
if(i==n) add(id[i][j]*2+1,T,inf,0);
if(i<n){
add(id[i][j]*2+1,id[i+1][j]*2,inf,0);
add(id[i][j]*2+1,id[i+1][j+1]*2,inf,0);
}
}
cout<<EK()<<endl;
return 0;
}
网格图模型
我们可以这样建图:

其中源点连向左上角的边和右下角连向汇点的边的容量为 \(k\),每个点向下面的点和右面的点连边,这样就可以把原问题的路径转化为可行流。
考虑到原问题的费用在点上,所以我们进行拆点,入点向出点连一条费用为 \(w_{i,j}\),容量为 \(1\) 的边,但是如果单纯是这样的话,这个点只能走一次,而原问题是:可以走很多次,但是只有一次能累加费用,所以我们再从入点向出点连一条容量为 \(+\infty\),费用为 \(0\) 的边。然后直接求最大费用最大流。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=50010,M=160010,inf=0x3f3f3f3f;
int n,k,S,T;
int h[N],e[M],ne[M],f[M],w[M],idx;
int q[N],d[N],pre[N],incf[N];
bool st[N];
int get(int x,int y,int t){//t表示是否为入点
return (x*n+y)*2+t;
}
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=c;
ne[idx]=h[a];
w[idx]=d;
h[a]=idx++;
e[idx]=a;
f[idx]=0;
ne[idx]=h[b];
w[idx]=-d;
h[b]=idx++;
}
bool spfa(){
int hh=0,tt=1;
mems(d,-0x3f);
mems(incf,0);
q[0]=S;
d[S]=0;
incf[S]=inf;
while(hh!=tt){
int t=q[hh++];
st[t]=false;
if(hh==N) hh=0;
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(f[i]>0 && d[ver]<d[t]+w[i]){
d[ver]=d[t]+w[i];
pre[ver]=i;
incf[ver]=min(f[i],incf[t]);
if(!st[ver]){
st[ver]=true;
q[tt++]=ver;
if(tt==N) tt=0;
}
}
}
}
return (incf[T]>0);
}
int EK(){
int cost=0;
while(spfa()){
int t=incf[T];
cost+=t*d[T];
for(int i=T;i!=S;i=e[pre[i]^1]){
f[pre[i]]-=t;
f[pre[i]^1]+=t;
}
}
return cost;
}
int main(){
cin>>n>>k;
S=2*n*n;
T=S+1;
mems(h,-1);
add(S,get(0,0,0),k,0);
add(get(n-1,n-1,1),T,k,0);
for(int i=0;i<n;i++)
for(int j=0;j<n;j++){
int c;
scanf("%d",&c);
add(get(i,j,0),get(i,j,1),1,c);
add(get(i,j,0),get(i,j,1),inf,0);
if(i+1<n) add(get(i,j,1),get(i+1,j,0),inf,0);
if(j+1<n) add(get(i,j,1),get(i,j+1,0),inf,0);
}
cout<<EK();
return 0;
}
浪费了我五分钟读题时间。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=1145,M=114514,inf=0x3f3f3f3f;
int a,b;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],w[M],idx;
int q[N],d[N],pre[N],incf[N];
bool st[N];
int get(int x,int y){
return x*(m+1)+y;
}
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=c;
w[idx]=d;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
w[idx]=-d;
ne[idx]=h[b];
h[b]=idx++;
}
bool spfa(){
int hh=0,tt=1;
mems(d,-0x3f);
mems(incf,0);
q[0]=S;
d[S]=0;
incf[S]=inf;
while(hh!=tt){
int t=q[hh++];
if(hh==N) hh=0;
st[t]=false;
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(f[i]>0 && d[ver]<d[t]+w[i]){
d[ver]=d[t]+w[i];
pre[ver]=i;
incf[ver]=min(incf[t],f[i]);
if(!st[ver]){
q[tt++]=ver;
st[ver]=true;
if(tt==N) tt=0;
}
}
}
}
return (incf[T]>0);
}
int EK(){
int cost=0;
while(spfa()){
int t=incf[T];
cost+=t*d[T];
for(int i=T;i!=S;i=e[pre[i]^1]){
f[pre[i]]-=t;
f[pre[i]^1]+=t;
}
}
return cost;
}
int main(){
cin>>a>>b>>n>>m;
S=(n+1)*(m+1);
T=S+1;
mems(h,-1);
for(int i=0;i<n+1;i++)
for(int j=0;j<m;j++){
int c;
scanf("%d",&c);
add(get(i,j),get(i,j+1),1,c);
add(get(i,j),get(i,j+1),inf,0);
}
for(int i=0;i<m+1;i++)
for(int j=0;j<n;j++){
int c;
scanf("%d",&c);
add(get(j,i),get(j+1,i),1,c);
add(get(j,i),get(j+1,i),inf,0);
}
while(a--){
int k,x,y;
scanf("%d%d%d",&k,&x,&y);
add(S,get(x,y),k,0);
}
while(b--){
int r,x,y;
scanf("%d%d%d",&r,&x,&y);
add(get(x,y),T,r,0);
}
cout<<EK();
return 0;
}
拆点

原问题的方案可以与上图的最大流相对应,直接求最小费用最大流即可。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=1610,M=10010,inf=0x3f3f3f3f;
int n,p,x,xp,y,yp,S,T;
int h[N],e[M],ne[M],f[M],w[M],idx;
int q[N],d[N],pre[N],incf[N];
bool st[N];
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=c;
w[idx]=d;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
w[idx]=-d;
ne[idx]=h[b];
h[b]=idx++;
}
bool spfa(){
int hh=0,tt=1;
mems(incf,0);
mems(d,0x3f);
q[0]=S;
d[S]=0;
incf[S]=inf;
while(hh!=tt){
int t=q[hh++];
if(hh==N) hh=0;
st[t]=false;
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(f[i]>0 && d[ver]>d[t]+w[i]){
d[ver]=d[t]+w[i];
pre[ver]=i;
incf[ver]=min(incf[t],f[i]);
if(!st[ver]){
st[ver]=true;
q[tt++]=ver;
if(tt==N) tt=0;
}
}
}
}
return (incf[T]>0);
}
int EK(){
int cost=0;
while(spfa()){
int t=incf[T];
cost+=t*d[T];
for(int i=T;i!=S;i=e[pre[i]^1]){
f[pre[i]]-=t;
f[pre[i]^1]+=t;
}
}
return cost;
}
int main(){
cin>>n>>p>>x>>xp>>y>>yp;
S=0;
T=2*n+1;
mems(h,-1);
for(int i=1;i<=n;i++){
int r;
scanf("%d",&r);
add(S,i,r,0);
add(n+i,T,r,0);
add(S,n+i,inf,p);//购买
if(i<n) add(i,i+1,inf,0);//拖延
if(i+x<=n) add(i,i+x+n,inf,xp);//快洗
if(i+y<=n) add(i,n+i+y,inf,yp);//慢洗
}
cout<<EK();
return 0;
}
上下界可行流
我们从点 \(i\) 向点 \(i+1\) 连边,表示第 \(i\) 天志愿者的人数,而且每一条都至少要有 \(A_i\) 个人,所以这就是一个无源汇上下界最小费用可行流。
如果一个志愿者能从第 \(S_i\) 天工作到第 \(T_i\) 天,就让第 \(S_i\) 条边到第 \(T_i\) 条边流 \(1\) 的流量。但是我们要保证流量守恒,所以从第 \(T_i+1\) 个点向第 \(S_i\) 个点连一条容量为 \(+\infty\),费用为 \(C_i\),下界为 \(0\) 的边,让这个流量流回去,那么这条边的流量就等于这类志愿者使用了多少人。其他边的容量为 \(+\infty\),费用为 \(0\),下界为 \(A_i\)。

参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=10010,M=220010,inf=0x3f3f3f3f;
int n,m,S,T;
int h[N],e[M],ne[M],f[M],w[M],idx;
int q[N],d[N],pre[N],incf[N];
bool st[N];
void add(int a,int b,int c,int d){
e[idx]=b;
f[idx]=c;
w[idx]=d;
ne[idx]=h[a];
h[a]=idx++;
e[idx]=a;
f[idx]=0;
w[idx]=-d;
ne[idx]=h[b];
h[b]=idx++;
}
bool spfa(){
int hh=0,tt=1;
mems(incf,0);
mems(d,0x3f);
incf[S]=inf;
d[S]=0;
q[0]=S;
while(hh!=tt){
int t=q[hh++];
if(hh==N) hh=0;
st[t]=false;
for(int i=h[t];~i;i=ne[i]){
int ver=e[i];
if(f[i]>0 && d[ver]>d[t]+w[i]){
d[ver]=d[t]+w[i];
pre[ver]=i;
incf[ver]=min(incf[t],f[i]);
if(!st[ver]){
st[ver]=true;
q[tt++]=ver;
if(tt==N) tt=0;
}
}
}
}
return (incf[T]>0);
}
int EK(){
int cost=0;
while(spfa()){
int t=incf[T];
cost+=t*d[T];
for(int i=T;i!=S;i=e[pre[i]^1]){
f[pre[i]]-=t;
f[pre[i]^1]+=t;
}
}
return cost;
}
int main(){
cin>>n>>m;
S=0;
T=n+2;
mems(h,-1);
int last=0;//前一天的人数,相当于C入
for(int i=1;i<=n;i++){
int cur;
scanf("%d",&cur);
if(last>cur) add(S,i,last-cur,0);
else add(i,T,cur-last,0);
add(i,i+1,inf,0);
last=cur;
}
add(S,n+1,last,0);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(b+1,a,inf,c);
}
cout<<EK();
return 0;
}
后记
非常感谢你能看到这里!
整理自:AcWing算法进阶课。

浙公网安备 33010602011771号