网络流学习笔记(一)——基本概念&最大流
N总 早早地切掉了网络流 24 题,于是让我快学习一下网络流。
流网络
一个流网络 是一张有向图。图中存在两个特殊的点,源点 和汇点 ,每条边 都有一个给定的权值,称为边的容量。
流函数
设 是定义在节点二元组 上的实数函数,且满足:
1.
2.
3.,。
被称为整个网络的流量( 表示源点)。
以上三个性质分别称为 的容量限制、斜对称和流量守恒。这三个性质告诉我们网络中,除源点、汇点以外,任何节点不储存流,其流入总量等于流出总量。网络流模型可以形象地描述为:在不超过容量限制的前提下,“流”从
源点源源不断地产生,流经整个网络,最终全部归于汇点。
最大流
对于一个给定的网络,合法的流函数 有很多。其中使得整个网络流量 最大的流函数被称为网络的最大流,此时的流量被称为网络的最大流量。
最大流能解决许多实际问题。以二分图为例,对于一张 个点 条边的二分图,我们可以新增一个源点 和一个汇点 ,从 到每个左部点连有向边,从每个右部点到 连有向边,把原二分图的每条边看成是从左部到右部的一条有向边,形成一张 个点 条边的网络,网络中每条边的容量都设为 。可以发现,二分图的最大匹配数就等于网络的最大流量,求出最大流后,所有有“流”经过的点、边就是匹配点、匹配边。
残留网络
残留网络与流函数一一对应,对于不同的流函数,残留网络也就不同。因此,把残留网络写作 。
, 的所有反向边。
对于残留网络中的每一条边 ,都有一个残留网络的容量 ,定义为:
1. 是正向边,。
2. 是反向边,。
残留网络也存在流函数。
定理
如果把原网络的流函数写作 ,残留网络的流函数写作 ,那么 也是原网络的一个流函数。
证明
先解释流函数相加的方法,对于同向边,直接相加即可;而对于反向边,则需要减去反向边的流函数。
要证明新的函数也是原网络的一个流函数,就是要证明新的函数满足流量限制、流量守恒。
两条边的方向相同。因为 ,那么也就得到了 。满足流量限制。
两条边的方向不相同。有 。那么就有 。满足流量限制。
而对于流量守恒,在 和 中都满足流量守恒,那么相加之后显然也满足。
而新的流函数的流量也就是原流函数的流量加上残留网络的流量。
从上面一句话可以发现一个推论:如果一个流函数的残留网络的流函数的最大流大于零,那么原流函数一定不是最大流。反之,如果流函数的残留网络的流函数的最大流为零,那么原流函数就是最大流。
增广路径
在一个流函数的残留网络中,如果存在一条从源点出发,只经过流量大于零的边可以到达汇点,那么这条路径就被称为一条增广路径。二分图匈牙利算法中的增广路径就是这里的一条特殊增高路径。
定理
如果一个流函数的残留网络中不存在增广路,那么该流函数就是原网络的最大流。
割
定义:对于一个网络 ,把 分成两个子集 ,满足源点 ,汇点 ,,。那么就称该分割方法为原网络的一个割。
割的容量:。
割的流量:。
注意,如果边 不存在,那么 。
使割的容量最小的割被称为最小割。
如果一个网络的割确定了,那么割的容量就确定了,但是割的流量会随着 的变化而变化。
性质
。
( 表示流函数 的流量)。直观上理解, 就是从 流向 的流量之和,而 流向 必然要经过连接 的边,那么流量就是从 流向 的减去从 流向 的。
从上面两个性质可以推出:
。
最大流最小割定理
对于一个网络 ,以下三个条件是等价的:
(1) 是最大流。
(2) 残留网络 中不存在增广路径。
(3) ,。
【模板】网络最大流
给出一个网络图,以及其源点和汇点,求出其网络最大流。
数据范围
,。
EK 算法
根据上面提到的,EK 算法实际上就是在残留网络中不断找增广路径,直到不存在增广路径为止,就得到了原网络的最大流。
有一些实现细节见代码。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1010;
const int M=2e4+10;
const int INF=0x3f3f3f3f;
#define LL long long
int n,m,s,t,h[N],idx=1;//利用成对变换的技巧,因为找到增广路径后要更新残留网络
int pre[N],q[N],d[N];//d[i] 表示从源点到 i 经过的边权的最小值
bool vis[N];
struct edge{
int v,w,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
int max(int a,int b){return a>b?a:b;}
void add(int u,int v,int w){e[++idx].v=v;e[idx].w=w;e[idx].nex=h[u];h[u]=idx;}
bool bfs()//bfs求增广路径
{
int hh=0,tt=-1;
memset(vis,0,sizeof(vis));
memset(d,0,sizeof(d));
d[s]=INF;vis[s]=true;q[++tt]=s;
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(vis[v]||e[i].w==0) continue;//增广路径中不包含容量为0的边
vis[v]=true;
pre[v]=i;
d[v]=min(d[u],e[i].w);
if(v==t) return true;
q[++tt]=v;
}
}
return false;
}
LL EK()
{
LL res=0;
while(bfs())
{
res+=d[t];
for(int i=t;i!=s;i=e[pre[i]^1].v)
{
e[pre[i]].w-=d[t],e[pre[i]^1].w+=d[t];
}
}
return res;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int u,v,w,i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,0);//初始时反向边的流量为0
}
printf("%lld\n",EK());
return 0;
}
Dinic 算法
注意到 EK 算法中每次 BFS 一遍只会找到一条增广路径,这样的效率非常低下。考虑对找增广路径的过程进行优化。
定义 表示 到 最少需要经过的边数。在残留网络中,满足 的边构成的图被称为分层图。而这样的一张图显然是一张有向无环图。
Dinic 算法的流程如下:
1.在残留网络上构造出分层图。
2.在分层图上 DFS 寻找增广路,在回溯的同时更新边的流量。另外,在代码中也加入了一些优化。具体实现见代码。
Dinic 算法的时间复杂度是 ,实际运用中远远达不到这个上界。特别的,Dinic 算法求二分图最大匹配的时间复杂度是 ,实际效率更高。
code:
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int N=1e4+10;
const int M=2e5+10;
#define ll long long
const int INF=0x3f3f3f3f;
struct edge{
int v,w,nex;
}e[M];
int q[N],h[N],idx=1,n,m,s,t,d[N],cur[N];
void add(int u,int v,int w){e[++idx].v=v;e[idx].nex=h[u];e[idx].w=w;h[u]=idx;}
int min(int a,int b){return a<b?a:b;}
int max(int a,int b){return a>b?a:b;}
bool bfs() //构造分层图
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));
d[s]=0;q[++tt]=s;cur[s]=h[s];
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;
cur[v]=h[v];//当前弧优化
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
ll dfs(int u,int limit)//limit 是最大的容量
{
if(u==t) return limit;
ll flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)//如果当前节点向子节点的流量已经超过了父节点最多传给它的流量,那么就没必要继续搜下去了
{
cur[u]=i;//如果遍历到了当前边,就说明前面的边都已经到了流量上限
int v=e[i].v;
if(d[v]==d[u]+1&&e[i].w)
{
ll t=dfs(v,min(e[i].w,limit-flow));
if(!t) d[v]=-1;//如果不能向子节点传输流量了,那么当前节点也就没有用了
e[i].w-=t,e[i^1].w+=t;flow+=t;
}
}
return flow;
}
ll Dinic()
{
ll res=0,flow;
while(bfs())
{
while(flow=dfs(s,INF)) res+=flow;//如果存在增广路径
}
return res;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int u,v,w,i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,0);
}
printf("%lld\n",Dinic());
return 0;
}
【应用】飞行员配对方案问题
给出一张左部有 个点,右部有 个点的二分图,求一组二分图最大匹配,并输出方案。
数据范围
。
思路
能用最大流解决问题的条件:
原问题是在一些可行解的集合中求一个最优解,通过网络流的建图,可以转化为网络上的流函数,且流函数和可行解一一对应,那么原问题就能用最大流解决。
最大流求解二分图最大匹配
匈牙利算法的时间复杂度是 ,如果能通过一种构造方法将二分图最大匹配转化为最大流问题,用 Dinic 算法求解,就可以将时间复杂度降低到 。
考虑如何建图。
事实上,可以将原二分图中的边看成是网络中一条从左部节点流向右部节点,且容量为 的边。再从源点向所有左部节点连一条容量为 的边,从所有右部节点向汇点连一条容量为 的边。
如果边的流函数值只能是整数。那么此时二分图匹配的边数,就是网络的流量。于是只需求网络的最大流即可得出答案。
注意到题目还要求输出方案,那么就可以枚举所有的正向边,如果连接的是左右两边的节点且在残留网络上的流量为 ,那么在最大匹配中就要选择这条边。输出节点的编号即可。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=110;
const int M=5210;
const int INF=0x3f3f3f3f;
int n,m,q[N],cur[N],d[N],h[N],idx=1,s,t;
struct edge{
int v,w,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w){e[++idx].v=v;e[idx].w=w;e[idx].nex=h[u];h[u]=idx;}
bool bfs()
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));d[s]=0,q[++tt]=s;cur[s]=h[s];
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;
cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int dfs(int u,int limit)
{
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
cur[u]=i;
int v=e[i].v;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(limit-flow,e[i].w));
if(!t) d[v]=-1;
flow+=t;e[i].w-=t,e[i^1].w+=t;
}
}
return flow;
}
int dinic()
{
int res=0,flow=0;
while(bfs())
{
while(flow=dfs(s,INF)) res+=flow;
}
return res;
}
int main()
{
scanf("%d%d",&m,&n);
s=0,t=n+1;
int u,v;while(scanf("%d%d",&u,&v),u!=-1&&v!=-1) add(u,v,1),add(v,u,0);
for(int i=1;i<=m;i++) add(s,i,1),add(i,s,0);
for(int i=m+1;i<=n;i++) add(i,t,1),add(t,i,0);
printf("%d\n",dinic());
for(int i=2;i<=idx;i+=2)
if(e[i].v>m&&e[i].v<=n&&!e[i].w) printf("%d %d\n",e[i^1].v,e[i].v);
return 0;
}
【应用】圆桌问题
假设有来自 个不同单位的代表参加一次国际会议。
每个单位的代表数分别为 。
会议餐厅共有 张餐桌,每张餐桌可容纳 个代表就餐。
为了使代表们充分交流,希望从同一个单位来的代表不在同一个餐桌就餐。
试设计一个算法,给出满足要求的代表就餐方案。
数据范围
,,
思路
可以发现,如果把每一个单位都看成是一个左部节点,把每一张餐桌都看成是一个右部节点。此题可以转化为一个二分图多重匹配的问题。
而用网络流求解二分图多重匹配和二分图最大匹配的建图方式区别不是很大,只需要把源点向每个左部节点连的边的容量从 改成 ,把每个右部节点向汇点连的边的容量从 改成 即可。
本题要求一个满足所有代表都能就餐的方案,实际上也就是求一组匹配数为 的最大匹配,在图中就是判断是否存在流量为 的可行流。
输出方案也就是枚举一下所有的正向边判断一下。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=610;
const int M=1e5+10;
const int INF=0x3f3f3f3f;
int h[N],idx=1,s,t,n,m,tot;
int q[N],cur[N],d[N];
struct edge{
int v,w,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w){e[++idx].v=v;e[idx].w=w;e[idx].nex=h[u];h[u]=idx;}
bool bfs()
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));d[s]=0;cur[s]=h[s];q[++tt]=s;
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;
cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int dfs(int u,int limit)
{
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
int v=e[i].v;cur[u]=i;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(e[i].w,limit-flow));
if(!t) d[v]=-1;
flow+=t;e[i].w-=t;e[i^1].w+=t;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs()) while(flow=dfs(s,INF)) res+=flow;
return res;
}
int main()
{
scanf("%d%d",&m,&n);
s=0,t=n+m+1;
for(int w,i=1;i<=m;i++) scanf("%d",&w),add(s,i,w),add(i,s,0),tot+=w;
for(int w,i=1;i<=n;i++) scanf("%d",&w),add(i+m,t,w),add(t,i+m,0);//注意要把所有右部节点的编号加上m,防止和左部节点的编号重合
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
add(i,j+m,1),add(j+m,i,0);
if(dinic()<tot) puts("0");
else
{
puts("1");
for(int u=1;u<=m;u++)
{
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(v>m&&v<=n+m&&e[i].w==0) printf("%d ",v-m);//别忘了减去原来加上的编号
}
puts("");
}
}
}
【模板】无源汇上下界可行流
给定一个包含 个点 条边的有向图,每条边都有一个流量下界和流量上界。
求一种可行方案使得在所有点满足流量平衡条件的前提下,所有边满足流量限制。
数据范围
。
思路
注意到本题中的边的流量有上界和下界,而在一般的网络流中都是没有下界的,或者是默认下界为 。不难想到可以将每条边的容量范围从 变成 。
但是这样会发现一个问题,如果一个点入边减去的总容量和出边减去的总容量不相等,那么就不满足流量守恒。此时直接用 Dinic 算法就是错误的了。
注意到本题并没有明确的源点和汇点,于是可以想到建出源点和汇点,在边的容量改变后的新图中,如果一个点入边减去的容量比出边更多,实际上就是在这个节点储存了流量,那么就需要从源点向这个点连一条容量为 的边,也就是使得多出来的这部分流量可以流出;反之,就是当前点的流量不够了,那么就从当前点向汇点连一条流量为 的边,也就是让入边能够把这部分流量给补上。
最终判断是否有解,也就是判断是否所有差的流量能被补上,也就是判断是否存在流量为 的流函数,最后输出的时候补上原来减去的流量即可。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=210;
const int M=22222;
const int INF=0x3f3f3f3f;
int tot,h[N],idx=1,delta[N],n,m,s,t,q[N],cur[N],d[N];
struct edge{
int v,w,low,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int a,int b)
{
e[++idx].v=v;e[idx].nex=h[u];e[idx].low=a,e[idx].w=b-a;h[u]=idx;
}
bool bfs()
{
int hh=0,tt=-1;
q[++tt]=s;
memset(d,-1,sizeof(d)),d[s]=0,cur[s]=h[s];
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;
cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int dfs(int u,int limit)
{
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(limit-flow,e[i].w));
if(!t) d[v]=-1;
flow+=t;e[i].w-=t,e[i^1].w+=t;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs()) while(flow=dfs(s,INF)) res+=flow;
return res;
}
int main()
{
scanf("%d%d",&n,&m);
s=0,t=n+1;
for(int u,v,a,b,i=1;i<=m;i++)
{
scanf("%d%d%d%d",&u,&v,&a,&b);
add(u,v,a,b);
add(v,u,b,b);//反向边的最低流量其实没有用处
delta[u]-=a;delta[v]+=a;
}
for(int i=1;i<=n;i++)
if(delta[i]>0) add(s,i,0,delta[i]),add(i,s,0,0),tot+=delta[i];
else if(delta[i]<0) add(i,t,0,-delta[i]),add(t,i,0,0);
if(dinic()<tot) puts("NO");
else
{
puts("YES");
for(int i=2;i<=2*m+1;i+=2) printf("%d\n",e[i^1].w+e[i].low);//注意是反向边的流量,因为保存的是残留网络,所以反向边的流量才是真实的流量
}
return 0;
}
【模板】多源汇最大流
给定一个包含 个点 条边的有向图,并给定每条边的容量,边的容量非负。
其中有 个源点, 个汇点。
求整个网络的最大流。
数据范围
,。
思路
显然只需要新建一个虚拟源点 ,向所有源点连一条容量为正无穷的边;新建一个虚拟汇点 ,从所有汇点向虚拟汇点连一条容量为正无穷的边即可。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e4+10;
const int M=3e5+10;
const int INF=0x3f3f3f3f;
int h[N],cur[N],idx=1,n,m,s,t,q[N],d[N],S,T;
struct edge{
int v,w,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w){e[++idx].v=v;e[idx].w=w;e[idx].nex=h[u];h[u]=idx;}
bool bfs()
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));cur[s]=h[s];q[++tt]=s;d[s]=0;
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;
cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int dfs(int u,int limit)
{
// printf("%d %d\n",u,limit);
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
cur[u]=i;
int v=e[i].v;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(limit-flow,e[i].w));
if(!t) d[v]=-1;
flow+=t;e[i].w-=t,e[i^1].w+=t;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=dfs(s,INF)) res+=flow;
}
return res;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&S,&T);
s=0,t=n+1;
for(int x,i=1;i<=S;i++) scanf("%d",&x),add(s,x,INF),add(x,s,0);
for(int x,i=1;i<=T;i++) scanf("%d",&x),add(x,t,INF),add(t,x,0);
for(int u,v,w,i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,0);
}
printf("%d\n",dinic());
return 0;
}
【关键边】2236. 伊基的故事 I - 道路重建
给出一个网络,求该网络中存在多少条边,使得仅增大这条边的容量,可以使最大流量增大。
数据范围
,。
思路
首先求出原图中的最大流。
发现增大当前边 的流量可以增大最大流量,当且仅当 且增大当前边的流量后,残留网络中存在一条经过 的增广路径。
那么就可以深搜预处理出源点能到的点以及汇点能到的点,再枚举原网络中的所有边判断一下即可。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=510;
const int M=1e4+10;
const int INF=0x3f3f3f3f;
int h[N],res,idx=1,n,m,s,t;
int q[N],cur[N],d[N];
struct edge{
int v,w,nex;
}e[M];
bool vs[N],vt[N];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w){e[++idx].v=v;e[idx].nex=h[u];e[idx].w=w;h[u]=idx;}
bool bfs()
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));cur[s]=h[s];d[s]=0;q[++tt]=s;
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;
cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int dfs(int u,int limit)
{
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(limit-flow,e[i].w));
if(!t) d[v]=-1;
flow+=t;e[i].w-=t;e[i^1].w+=t;
}
}
return flow;
}
void dinic()
{
while(bfs()) while(dfs(s,INF));
}
void dfs_nlc(int u,bool vis[],int dir)
{
vis[u]=true;
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(!vis[v]&&e[i^dir].w)
{
dfs_nlc(v,vis,dir);
}
}
}
int main()
{
scanf("%d%d",&n,&m);
s=0,t=n-1;
for(int u,v,w,i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,0);
}
dinic();
dfs_nlc(s,vs,0);
dfs_nlc(t,vt,1);//注意从汇点往前走的时候要判断反向边的容量
for(int i=2;i<=idx;i+=2) res+=(!e[i].w&&vs[e[i^1].v]&vt[e[i].v]);
printf("%d\n",res);
return 0;
}
【应用】Secret Milking Machine
给出一张 个点 条边的带权无向图,找出 条不相交的从 走到 的路径,最小化这些路径中权值最大的边。
数据范围
,,。
思路
看到题意是求路径边权最大值的最小值,会想到二分答案。考虑如何判定当前答案是否合法。
可以将所有边权小于等于 的边的容量看成 ,其余的边容量看出 。再求一遍原图的最大流,此时的流量就是最多找出不相交的边数。于是,只需比较一下 与 的大小关系即可。
但是本题中的边均为无向边。那么就不能用有向图的建图方式。直观来想,可以将原图中的一条无向边看成两条反向边,注意到此时再残留网络中,一条边的反向边的流量就是另一条边的正向边的流量。于是可以考虑合并这两条有向边。
code:
#include<cstdio>
#include<cstring>
const int N=210;
const int M=1e5+10;
const int INF=0x3f3f3f3f;
int h[N],idx=1,k,s,t,n,m;
int q[N],cur[N],d[N];
struct edge{
int v,w,f,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int f){e[++idx].v=v;e[idx].f=f;e[idx].nex=h[u];h[u]=idx;}
bool bfs()
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));cur[s]=h[s];q[++tt]=s;d[s]=0;
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;
cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int dfs(int u,int limit)
{
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
cur[u]=i;
int v=e[i].v;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(limit-flow,e[i].w));
if(!t) d[v]=-1;
e[i].w-=t,e[i^1].w+=t;flow+=t;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=dfs(s,INF)) res+=flow;
}
return res;
}
bool check(int lim)
{
for(int i=2;i<=idx;i++) e[i].w=e[i].f<=lim;
return dinic()>=k;
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
s=1,t=n;
for(int u,v,w,i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,w);
}
int l=1,r=1e6;
while(l<r)
{
int mid=l+r>>1;
if(check(mid)) r=mid;
else l=mid+1;
}
printf("%d\n",r);
}
【应用】星际转移问题
由于人类对自然资源的消耗,人们意识到大约在 年之后,地球就不能再居住了。
于是在月球上建立了新的绿地,以便在需要时移民。
令人意想不到的是, 年冬由于未知的原因,地球环境发生了连锁崩溃,人类必须在最短的时间内迁往月球。
现有 个太空站(编号 )位于地球与月球之间,且有 艘公共交通太空船在其间来回穿梭。
每个太空站可容纳无限多的人,而每艘太空船 只可容纳 个人。
每艘太空船将周期性地停靠一系列的太空站,例如:(,,) 表示该太空船将周期性地停靠太空站 …。
每一艘太空船从一个太空站驶往任一太空站耗时均为 。
人们只能在太空船停靠太空站(或月球、地球)时上、下船。
初始时所有人全在地球上,太空船全在初始站,即行驶周期中的第一个站。
试设计一个算法,找出让所有人尽快地全部转移到月球上的运输方案。
(由于本题题意较为复杂且复杂度很玄学,所以就不用管数据范围了)
思路
如果把地球、月球和每个空间站分别当成一个源点,那么此时网络流的建图就十分困难(可能无法实现)。于是可以考虑将空间站的点拆分成第 天的第 个空间站。
那么根据题意,第 天就只能在地球上。从太空船 第 天停靠的点向第 天停靠的点连一条容量是 的边;同时人也可以停留在空间站里,那么就让第 个空间站的第 天向第 条边连一条容量是正无穷的边。那么此时就只需要判断一下网络的最大流是否 即可。
虽然本题可以用二分答案来做,但是会发现从第 天向第 天连边后,在原来的残留网络上仍然可以继续找增广路,于是可以从小到大枚举天数,这样连边就比二分答案方便很多。
关于无解,可以用并查集的思想(此时不用拆点),把地球看成 ,把月球看成 ,在输入太空船停靠周期的时候合并并查集,再判断一下 和 是否在同一个并查集里面即可。
code:
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int N=10010;
const int M=1e5+10;
const int INF=0x3f3f3f3f;
int h[N],idx=1,n,m,k,s,t;
int stop[110][110],cnt[N],cap[N],cur[N],q[N],d[N],fa[N],ans=1,res=0;
struct edge{
int v,w,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w){e[++idx].v=v;e[idx].nex=h[u];e[idx].w=w;h[u]=idx;}
int getfa(int x){return fa[x]==x?x:fa[x]=getfa(fa[x]);}
bool bfs()
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));d[s]=0,cur[s]=h[s];q[++tt]=s;
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;
cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int dfs(int u,int limit)
{
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
cur[u]=i;int v=e[i].v;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(limit-flow,e[i].w));
if(!t) d[v]=-1;
flow+=t;e[i].w-=t;e[i^1].w+=t;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=dfs(s,INF)) res+=flow;
}
return res;
}
int nlc(int day,int i){return day*(n+2)+i;}
bool check(int now)
{
for(int i=0;i<=n+1;i++)
{
int u=nlc(now-1,i),v=nlc(now,i);
add(u,v,INF),add(v,u,0);
}
add(nlc(now,n+1),t,INF),add(t,nlc(now,n+1),0);
for(int i=0;i<m;i++)
{
int u=nlc(now-1,stop[i][(now-1)%cnt[i]]),v=nlc(now,stop[i][now%cnt[i]]);
add(u,v,cap[i]),add(v,u,0);
}
int T=dinic();
res+=T;
return res>=k;
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
s=0,t=n+1;
for(int i=1;i<=n+1;i++) fa[i]=i;
for(int i=0;i<m;i++)
{
scanf("%d%d",&cap[i],&cnt[i]);
for(int j=0;j<cnt[i];j++)
{
scanf("%d",&stop[i][j]);
if(stop[i][j]==-1) stop[i][j]=t;
if(j)
{
int fx=getfa(stop[i][j-1]),fy=getfa(stop[i][j]);
if(fx!=fy) fa[fx]=fy;
}
}
}
if(getfa(s)!=getfa(t))
{
puts("0");
return 0;
}
s=10001,t=10000;
add(s,0,INF),add(0,s,0);
while(!check(ans)) ans++;
printf("%d\n",ans);
return 0;
}。
【拆点】Dining
农夫约翰一共烹制了 种食物,并提供了 种饮料。
约翰共有 头奶牛,其中第 头奶牛有 种喜欢的食物以及 种喜欢的饮料。
约翰需要给每头奶牛分配一种食物和一种饮料,并使得有吃有喝的奶牛数量尽可能大。
每种食物或饮料都只有一份,所以只能分配给一头奶牛食用(即,一旦将第 种食物分配给了一头奶牛,就不能再分配给其他奶牛了)。
数据范围
。
思路
一种很 naive 的想法,从奶牛向食物连边,再从食物向对应的奶牛连边跑最大流。但是这样想是错的,因为无法保证每个食物只被用一次(也就是中间一列的点,直接和源点或汇点相连的边都可以保证)。
考虑拆点。也就是把每个食物拆成入点和出点,再从入点向出点连一条容量为 的边。这样就可以保证每一个食物只被使用一次。在具体实现的时候,可以把奶牛拆点,因为这样才能满足限制关系。
接下来就只需要跑一遍 Dinic 求最大流即可。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=410;
const int M=40010;
const int INF=0x3f3f3f3f;
int h[N],s,t,n,f,D,idx=1,cur[N],q[N],d[N],a[N],b[N];
struct edge{
int v,w,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w){e[++idx].v=v;e[idx].w=w;e[idx].nex=h[u];h[u]=idx;}
bool bfs()
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));d[s]=0,q[++tt]=s;cur[s]=h[s];
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int dfs(int u,int limit)
{
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(limit-flow,e[i].w));
if(!t) d[v]=-1;
flow+=t;e[i].w-=t;e[i^1].w+=t;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs()) while(flow=dfs(s,INF)) res+=flow;
return res;
}
int main()
{
scanf("%d%d%d",&n,&f,&D);
s=0,t=n+f*2+D+1;
for(int cnt1,cnt2,i=1;i<=n;i++)
{
int u=f+i*2-1,v=f+i*2;
add(u,v,1),add(v,u,0);
scanf("%d%d",&cnt1,&cnt2);
for(int j=1;j<=cnt1;j++) scanf("%d",&a[j]),add(a[j],u,1),add(u,a[j],0);
for(int j=1;j<=cnt2;j++) scanf("%d",&b[j]),add(v,b[j]+f+2*n,1),add(b[j]+n*2+f,v,0);
}
for(int i=1;i<=f;i++) add(s,i,1),add(i,s,0);
for(int i=1;i<=D;i++) add(i+f+n*2,t,1),add(t,i+f+n*2,0);
printf("%d\n",dinic());
return 0;
}
【拆点】最长不下降子序列问题
给定正整数序列 。
1.计算其最长递增子序列的长度 。
2.计算从给定的序列中最多可取出多少个长度为 的递增子序列。(给定序列中的每个元素最多只能被取出使用一次)
3.如果允许在取出的序列中多次使用 和 ,则从给定序列中最多可取出多少个长度为 的递增子序列。
递增指非严格递增。
数据范围
。
思路
对于第一问,直接 DP 转移就行了。
对于第二问,考虑网络流建图,由于每个元素只能被选一次,那么就可以拆点。对于 ,如果满足 。那么就连一条边。根据上面的连边条件,一旦到了 的元素,那么之前一定已经选了 个元素。故跑一遍最大流即可。
对于第三问,就是在第二问的基础上加上 和 能重复使用,只需要在第二问的残留网络上将源点到 以及 到汇点的边的容量改成 即可。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1010;
const int M=N*N;
const int INF=0x3f3f3f3f;
int h[N],idx=1,n,S,s,t,f[N],a[N];
int q[N],cur[N],d[N];
struct edge{
int v,w,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
int max(int a,int b){return a>b?a:b;}
void add(int u,int v,int w){e[++idx].v=v;e[idx].w=w;e[idx].nex=h[u];h[u]=idx;}
bool bfs()
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));d[s]=0,cur[s]=h[s];q[++tt]=s;
while(hh<=tt)
{
int u=q[hh++];
// printf("%d ",u);
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
// puts("");
}
return false;
}
int dfs(int u,int limit)
{
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
int v=e[i].v;cur[u]=i;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(limit-flow,e[i].w));
if(!t) d[v]=-1;
flow+=t;e[i].w-=t;e[i^1].w+=t;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=dfs(s,INF)) res+=flow;
}
return res;
}
int main()
{
scanf("%d",&n);s=0,t=2*n+1;
for(int i=1;i<=n;i++)
{
add(i,i+n,1);add(i+n,i,0);
scanf("%d",&a[i]);f[i]=1;
for(int j=1;j<i;j++)
if(a[j]<=a[i]) f[i]=max(f[i],f[j]+1);
for(int j=1;j<i;j++)
if(a[j]<=a[i]&&f[j]+1==f[i]) add(j+n,i,1),add(i,j+n,0);
if(f[i]==1) add(s,i,1),add(i,s,0);
S=max(S,f[i]);
}
for(int i=1;i<=n;i++)
if(f[i]==S) add(i+n,t,1),add(t,i+n,0);
// printf("%d\n",idx);
if(S==1) printf("%d\n%d\n%d\n",S,n,n);
else
{
printf("%d\n",S);
int res=dinic();printf("%d\n",res);
for(int i=2;i<idx;i++)
{
int u=e[i^1].v,v=e[i].v;
if(u==s&&v==1) e[i].w=INF;
if(u==1&&v==n+1) e[i].w=INF;
if(u==n&&v==n+n) e[i].w=INF;
if(u==n+n&&v==t) e[i].w=INF;
}
printf("%d\n",res+dinic());
}
return 0;
}
March of the Penguins
给定 块冰的坐标和企鹅能跳的距离 ,每块冰有 个属性,分别为 坐标, 坐标,上面原有的企鹅的数量和最多能跳出多少次,求哪些冰块可以让所有企鹅都跳到上面。
数据范围
,,
思路
用网络流解决此题的时候,注意到每个点有最多能起跳的次数,即对于点的次数限制,可以用拆点的方法来满足。将所有满足要求的点之间连一条边(注意是两条有向边,即无向边,因为两个点相互可以跳到)。新建一个源点,向所有点连一条容量为该点原企鹅数量的边。
由于本题没有固定的汇点,于是需要枚举汇点。这样做的复杂度看上去直接爆炸,但由于 Dinic 算法求最大流的时间复杂度很低,故可以通过本题。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=210;
const int M=1e5+10;
const int INF=0x3f3f3f3f;
const double eps=1e-8;
int h[N],idx=1,n,s,t,q[N],d[N],cur[N],cnt,tot;
double D;
struct node{
int x,y;
}p[N];
struct edge{
int v,w,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w){e[++idx].v=v;e[idx].w=w;e[idx].nex=h[u];h[u]=idx;}
bool bfs()
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));d[s]=0,cur[s]=h[s];q[++tt]=s;
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;
cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int dfs(int u,int limit)
{
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
cur[u]=i;int v=e[i].v;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(limit-flow,e[i].w));
if(!t) d[v]=-1;
flow+=t;e[i].w-=t;e[i^1].w+=t;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=dfs(s,INF)) res+=flow;
}
return res;
}
bool check(int i,int j)
{
double dx=p[i].x-p[j].x,dy=p[i].y-p[j].y;
return dx*dx+dy*dy<D*D+eps;
}
int main()
{
int T;scanf("%d",&T);
while(T--)
{
memset(h,0,sizeof(h));idx=1;s=tot=cnt=0;
scanf("%d%lf",&n,&D);
for(int a,b,i=1;i<=n;i++)
{
scanf("%d%d%d%d",&p[i].x,&p[i].y,&a,&b);
add(s,i,a),add(i,s,0),add(i,i+n,b),add(i+n,i,0),tot+=a;
}
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
if(check(i,j))
{
add(i+n,j,INF),add(j,i+n,0);
add(j+n,i,INF),add(i,j+n,0);
}
for(t=1;t<=n;t++)
{
for(int i=2;i<=idx;i+=2) //还原网络
{
e[i].w+=e[i^1].w;
e[i^1].w=0;
}
if(dinic()>=tot)
{
printf("%d ",t-1);//注意题目中的编号是0~n-1
cnt++;
}
}
if(!cnt) puts("-1");
else puts("");
}
return 0;
}
【建图】PIGS
米尔克在一家养猪场工作,该养猪场共有 个上了锁的猪舍。
由于米尔克没有钥匙,所以他无法打开任何猪舍。
个顾客们顺次来到了养猪场,不会有两个顾客同一时间过来。他们中的每个人都有一些猪舍的钥匙,并且想买一定数量的猪。
米尔克每天一大早就可以得到计划在当天来到农场的客户的所有数据,以便他制定销售计划,从而最大限度地提高出售生猪的数量。
更准确的说,过程如下:
1.顾客到达养猪场,将所有他有钥匙的猪舍的大门全部打开,米尔克从所有未上锁的猪舍中挑选一定数量的猪卖给该顾客。
2.如果米尔克愿意,他还可以给未上锁的猪舍里剩下的猪重新分配位置。
3.在每个顾客到达之前,会将上一个顾客打开的猪舍全部关闭。
每个猪舍中都可以放置无限数量的猪。
数据范围
。
思路
考虑没有限制 的情况,在这种情况下,题意就是求一个二分图最大带权多重匹配,直接无脑 Dinic 即可。
回到本题,如果把养猪场当成点来建图,那么无论如何也无法满足顾客的先后顺序。于是考虑把顾客当成点来建图。
具体的,如果第 个人和第 个人()的钥匙有共同部分(同是还要满足 之间没有其他的顾客有这个猪圈的钥匙,即 是最近一个拥有该猪圈钥匙的顾客),那么就可以从 向 连一条容量为正无穷(因为可以把所有的猪赶到一个猪舍里)的边。而如果这个猪舍是第一次有人用,那么就直接从源点向这个点连一条容量为猪舍内原始猪的数量的边。这样建图就可以巧妙地满足上述三个条件。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=110;
const int M=N*N;
const int INF=0x3f3f3f3f;
int h[N],idx=1,n,m,q[N],s,t,cur[N],d[N],a[M],last[M];
struct edge{
int v,w,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w){e[++idx].v=v;e[idx].w=w;e[idx].nex=h[u];h[u]=idx;}
bool bfs()
{
int hh=0,tt=-1;
memset(d,-1,sizeof(d));q[++tt]=s;d[s]=0,cur[s]=h[s];
while(hh<=tt)
{
int u=q[hh++];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)
{
d[v]=d[u]+1;cur[v]=h[v];
if(v==t) return true;
q[++tt]=v;
}
}
}
return false;
}
int dfs(int u,int limit)
{
if(u==t) return limit;
int flow=0;
for(int i=cur[u];i&&flow<limit;i=e[i].nex)
{
int v=e[i].v;cur[u]=i;
if(d[v]==d[u]+1&&e[i].w)
{
int t=dfs(v,min(limit-flow,e[i].w));
if(!t) d[v]=-1;
flow+=t;e[i].w-=t;e[i^1].w+=t;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=dfs(s,INF)) res+=flow;
}
return res;
}
int main()
{
scanf("%d%d",&m,&n);s=0,t=n+1;
for(int i=1;i<=m;i++) scanf("%d",&a[i]);
for(int p,b,cnt,i=1;i<=n;i++)
{
scanf("%d",&cnt);
while(cnt--)
{
scanf("%d",&p);
if(!last[p]) add(s,i,a[p]),add(i,s,0);
else add(last[p],i,INF),add(i,last[p],0);
last[p]=i;
}
scanf("%d",&b);add(i,t,b),add(t,i,0);
}
printf("%d\n",dinic());
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律