网络最大流专题
无源汇上下界可行流
给定一个包含 \(n\) 个点 \(m\) 条边的有向图 \(G<V,E,C>\),每条边都有一个流量下界和流量上界。
求一种可行方案使得在所有点满足流量平衡条件的前提下,所有边满足流量限制。
设流量下界为 \(c_l\),上界为\(c_r\),则 \(c_l(u,v)\le f(u,v)\le c_r(u,v)\) ,可以得到 \(0\le f(u,v)-c_l(u,v)\le c_r(u,v)-c_l(u,v)\)
我们可以建一个新图 \(G^{\prime}<V^{\prime},E^{\prime},C^{\prime}>\) 使得新图中的每一条边的流量减去一个这个边的容量限制,容量也要减去一个容量下界,
即:\(f^{\prime}(u,v)=f(u,v)-c_l(u,v),c^{\prime}=c_r(u,v)-c_l(u,v)\)
于是,我们会发现它满足不了流向守恒原则!
比如对于 \(x\) 点 ,它少出去了 \(C_{出}=\sum\limits_{(x,v)\in E}c_l(x,v)\) 这么多流量,少输入了 \(C_\text{入}=\sum\limits_{(v,x)\in E}c_l(v,x)\) 这么多流量。由于原图是满足流量守恒的,但 \(C_\text{入}>C_\text{出}\),新图就不守恒了。
这就类似于 Dijkstra不能求负权图最短路,权值加上一个数变成正值还是不可做的。
但有这么个 Johnson全源最短路 \(link\) 解决了问题。
我们亦可以借鉴其思路,从建立的超级源点向每个点连一条 \(c^{\prime}(u,v)=C_\text{入}-C_\text{出}\) 的边,流量就能够守恒了。
注意,原图是没有源汇点,源汇点是我们在新图中自建的。
但是那也意味着我们新建的这条边必须是满流,换句话说,原图的可行流是新图的最大流,且源点的出边都必须满流。
这样下来,通过新建超级源点和新边,最开始的那一步直接减掉 \(c_l(u,v)\) 的操作就是可行的,是能够使得新图流量守恒的了。
一一对应性就不证明了。
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10,M=2e6+10,INF=1e8;
int n,m,s,t;
int head[N],ver[M],nxt[M],cc[M],L[M],tot=0;
void add(int x,int y,int l,int r)
{
ver[tot]=y; cc[tot]=r-l; L[tot]=l; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],cur[N];
int A[N],sum=0;
bool bfs()
{
int hh=0,tt=0;
memset(d,-1,sizeof d);
q[0]=s,d[s]=0,cur[s]=head[s];
while(hh<=tt)
{
int x=q[hh++];
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(d[y]==-1 && cc[i])
{
d[y]=d[x]+1;
cur[y]=head[y];
if(y==t) return 1;
q[++tt]=y;
}
}
}
return 0;
}
int find(int u,int lim)
{
if(u==t) return lim;
int flow=0;
for(int i=head[u];~i && flow<lim;i=nxt[i])
{
cur[u]=i;
int y=ver[i];
if(d[y]==d[u]+1 && cc[i])
{
int tmp=find(y,min(cc[i],lim-flow));
if(!tmp) d[y]=-1;
cc[i]-=tmp; cc[i^1]+=tmp; flow+=tmp;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=find(s,INF)) res+=flow;
}
return res;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
s=n+m+1,t=n+m+3;
memset(head,-1,sizeof head);
for(int i=1;i<=m;i++)
{
int x,y,l,r;
cin>>x>>y>>l>>r;
add(x,y,l,r);
A[x]-=l,A[y]+=l;
}
sum=0;
for(int i=1;i<=n;i++)
if(A[i]>0) add(s,i,0,A[i]),sum+=A[i];
else if(A[i]<0) add(i,t,0,-A[i]);
if(dinic()!=sum) cout<<"NO\n";
else
{
cout<<"YES\n";
for(int i=0;i<m*2;i+=2)
cout<<cc[i^1]+L[i]<<"\n";//原图边的流量等于残留网络中反向边的容量
}
return 0;
}
有源汇上下界最大(小)流
给定一个包含 \(n\) 个点 \(m\) 条边的有向图 \(G<V,E>\) ,每条边都有一个流量下界和流量上界。
给定源点 \(s\) 和汇点 \(t\),求源点到汇点的最大流。
其实这个和上面的无源汇有相似的处理方法,我们可以从汇点 \(t\) 向源点 \(s\) 连一条容量为 \(+\infty\) 的虚边 ,整个图就流量守恒了。
于是问题就变成了如何求出无源汇上下界的最大流。
按照我们之前的构造方式,可以构造出新图 \(G^{\prime}\) 使得原图中一个特定的可行流 \(f_0\) 可以变为新图中的一个满流 \(f_0^{\prime}\) 。新图的源汇点分别为 \(S,T\)
下面我们考虑新图 \(G^{\prime}\) 对于 \(f_0^{\prime}\) 的残留网络 \(G^{\prime}_{f_0^{\prime}}\)。
此时令 \(f^{\prime}\) 是一个残留网络中从 \(S\rightarrow T\) 的一个满流,并且其中有一部分是从 \(s\rightarrow t\) 流过去的,设其为 \(f_{s\rightarrow t}^{\prime}\)。\(f_{s\rightarrow t}^{\prime}\) 是可求的,就是我们一开始加的虚边里面的流量。
现在我们考虑残留网络中所有经过 \(s\rightarrow t\) 的可行流 \(f\)
任取一个 \(f_{s\rightarrow t}^{\prime}\),让它加上 \(f_0^{\prime}\)
则 \(|f_{s\rightarrow t}^{\prime}+f_0^{\prime}|\) 中,由于与 \(S,T\) 相邻的边要么满流要么进去就出不来,故这个 \(f_{s\rightarrow t}^{\prime}\) 应该是和 \(S,T\) 及其相邻的边没什么关系的,该满流还是满流,所以我们不必考虑这些东西。
对于中间这部分的的点,不考虑虚边的话除了 \(s,t\) 以外都是守恒的,考虑的话都守恒。
总之,\(f_{s\rightarrow t}^{\prime}+f_0^{\prime}\) ,得到的还是新图的一个满流,还是原图的一个可行流。
所以,任意的 \(f^{\prime}_{s\rightarrow t}\) 在原图中都能找到一个唯一的 \(f\) 与之对应。
反向证明:在原图中任取一个可行流 \(f\) , 对应到新图中是 \(f^{\prime}\)
参考上面的证明过程,我们可以试试 \(f^{\prime}-f_0^{\prime}\) 会发生什么。
减完之后,\(S,T\) 邻边的流量全部抵消了,所有的流量都在中间这部分,流量守恒可以得知,此时减得的流量也可以是 \(s\rightarrow t\) 的可行流。也就是能对应一个 \(f^{\prime}_{s\rightarrow t}\) 。
综上,我们在新图中任取一个满流 \(f_0^{\prime}\) 使得这个图中任意一个满流 \(f^{\prime}\) 都可以由 \(f_0^{\prime}\) 和 \(f^{\prime}_{s\rightarrow t}\) 叠加得到,也就与原图可行流一一对应。
也就是说,令 \(f^{\prime}_{0,s\rightarrow t}\) 表示流 \(f_0^{\prime}\) 流经 \(s\rightarrow t\) 的部分,我们只要最大化 \(|f^{\prime}_{0,s\rightarrow t}+f^{\prime}_{s\rightarrow t}|\) 即可
由于 \(|f^{\prime}_{0,s\rightarrow t}|\) 已经确定了, 让 \(|f^{\prime}_{s\rightarrow t}|\) 最大只需要在新图上求一次 \(s\rightarrow t\) 的最大流就可以了。
同时我们得到一个结论:对于一个网络 \(G\) 的任何一个流 \(f\) 的流量都可以通过 \(|f_0+f^{\prime}|\) 的方式求出。
其中,\(f_0\) 是在以上面两题的建图方式建出的新图 \(G^{\prime}\) 中的一个满流,\(f^{\prime}\) 是残留网络 \(G^{\prime}_{f_0}\) 的某个可行流。
code(有源汇上下界最大流):
#include <bits/stdc++.h>
using namespace std;
const int N=210,M=(N+10005)*2,INF=2e8;
int n,m,S,T;
int head[N],ver[M],cc[M],nxt[M],tot=0;
int q[N],d[N],cur[N],A[N];
void add(int x,int y,int c)
{
ver[tot]=y; cc[tot]=c; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; nxt[tot]=head[y]; head[y]=tot++;
}
bool bfs()
{
int hh=0,tt=0;
memset(d,-1,sizeof d);
q[0]=S,d[S]=0,cur[S]=head[S];
while(hh<=tt)
{
int x=q[hh++];
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(d[y]==-1&&cc[i])
{
d[y]=d[x]+1;
cur[y]=head[y];
if(y==T) return 1;
q[++tt]=y;
}
}
}
return 0;
}
int find(int u,int lim)
{
if(u==T) return lim;
int flow=0;
for(int i=head[u];~i&&flow<lim;i=nxt[i])
{
int y=ver[i];
if(d[y]==d[u]+1&& cc[i])
{
int tmp=find(y,min(cc[i],lim-flow));
if(!tmp) d[y]=-1;
cc[i]-=tmp,cc[i^1] +=tmp,flow+=tmp;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=find(S,INF)) res+=flow;
}
return res;
}
int main()
{
int s,t;
scanf("%d%d%d%d",&n,&m,&s,&t);
S=0,T=n+1;
memset(head,-1,sizeof head);
for(int i=1;i<=m;i++)
{
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 cnt=0;
for(int i=1;i<=n;i++)
{
if(A[i]>0) add(S,i,A[i]),cnt+=A[i];
else if(A[i]<0) add(i,T,-A[i]);//Johnson Trick
}
add(t,s,INF);//添加虚边
if(dinic()<cnt) printf("No Solution\n");
else
{
int ff=cc[tot-1];
S=s,T=t;//重新制定源汇点
cc[tot-1]=cc[tot-2]=0;//删边
printf("%d\n",ff+dinic());
}
}
而最小流只需最大化 \(|f^{\prime}_{t\rightarrow s}|\) 就可以使得 \(|f^{\prime}_{s\rightarrow t}|\) 最小了。
#include <bits/stdc++.h>
using namespace std;
const int N=50010,M=(N+125003)*2,INF=2147483647;
int n,m,S,T;
int head[N],ver[M],nxt[M],cc[M],tot=0;
void add(int x,int y,int c)
{
ver[tot]=y; cc[tot]=c; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],cur[N],A[N];
bool bfs()
{
int hh=0,tt=0;
memset(d,-1,sizeof d);
q[0]=S,d[S]=0,cur[S]=head[S];
while(hh<=tt)
{
int x=q[hh++];
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(d[y]==-1 && cc[i])
{
d[y]=d[x]+1;
cur[y]=head[y];
if(y==T) return true;
q[++tt]=y;
}
}
}
return false;
}
int find(int x,int lim)
{
if(x==T) return lim;
int flow=0;
for(int i=cur[x]; ~i && flow<lim;i=nxt[i])
{
cur[x]=i;
int y=ver[i];
if(d[y]==d[x]+1 && cc[i])
{
int tmp=find(y,min(cc[i],lim-flow));
if(!tmp) d[y]=-1;
cc[i]-=tmp; cc[i^1]+=tmp;flow+=tmp;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=find(S,INF)) res+=flow;
}
return res;
}
int main()
{
int s,t;
scanf("%d%d%d%d",&n,&m,&s,&t);
S=0,T=n+1;
memset(head,-1,sizeof head);
for(int i=1;i<=m;i++)
{
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 sum=0;
for(int i=1;i<=n;i++)
{
if(A[i]>0) add(S,i,A[i]),sum+=A[i];
else if(A[i]<0) add(i,T,-A[i]);
}
add(t,s,INF);
if(dinic()<sum) printf("No Solution\n");
else
{
int ff=cc[tot-1];
S=t,T=s;
cc[tot-1]=cc[tot-2]=0;
int ans=dinic();
printf("%d\n",ff-ans);
}
return 0;
}
多源汇最大流
给定一个包含 \(n\) 个点 \(m\) 条边的有向图,并给定每条边的容量,边的容量非负。
其中有 \(S_c\) 个源点,\(T_c\) 个汇点。
图中可能存在重边和自环。
求整个网络的最大流。
这个再简单不过了啊,只需要建立一个超级源点向所有源点连无穷边,所有的汇点再向一个超级汇点连无穷边,跑dinic就是了。
真这么简单????
对,就这么简单。重边和自环在网络流里都可能是有意义的,不能轻易删去或松弛。
这个证明过于简单,就不给了
原图和新图的公共边部分容量没有任何改变,新图多出来的边容量都是 \(+\infty\) 故新图和原图可行流一一对应且流量值能够相等。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=1e6+10,INF=1e8;
int n,m,sn,tn;
int head[N],ver[M],nxt[M],cc[M],tot=0;
void add(int x,int y,int c)
{
ver[tot]=y; cc[tot]=c; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],cur[N];
int S,T;
bool bfs()
{
int hh=0,tt=0;
memset(d,-1,sizeof d);
q[0]=S,d[S]=0,cur[S]=head[S];
while(hh<=tt)
{
int x=q[hh++];
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(d[y]==-1 && cc[i])
{
d[y]=d[x]+1;
cur[y]=head[y];
if(y==T) return 1;
q[++tt]=y;
}
}
}
return 0;
}
int find(int u,int lim)
{
if(u==T) return lim;
int flow=0;
for(int i=cur[u];~i&&flow<lim;i=nxt[i])
{
cur[u]=i;
int y=ver[i];
if(d[y]==d[u]+1 && cc[i])
{
int tmp=find(y,min(cc[i],lim-flow));
if(!tmp) d[y]=-1;
cc[i]-=tmp; cc[i^1]+=tmp;flow+=tmp;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=find(S,INF)) res+=flow;
}
return res;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&sn,&tn);
S=0,T=n+1;
memset(head,-1,sizeof head);
for(int i=1;i<=sn;i++)
{
int x;
scanf("%d",&x);
add(S,x,INF);
}
for(int i=1;i<=tn;i++)
{
int x;
scanf("%d",&x);
add(x,T,INF);
}
for(int i=1;i<=m;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
printf("%d",dinic());
return 0;
}
关键边问题
道路重建
伊基是一个小国 – 凤凰国的国王。
凤凰国是如此之小,以至于只有一个城市负责日常商品的生产,并使用公路网将商品运送到首都。
伊基发现本国最大的问题在于运输速度太慢了。
因为伊基以前是 NOI 的参赛者,他意识到这其实是一个最大流问题。
他编写了一个最大流程序,并计算出了当前运输网络的最大运输能力。
他对运输速度的现状十分不满,并希望能够提高国家的运输能力。
提高运输能力的方法很简单,伊基将在运输网络中重建一些道路,以使这些道路具有更高的运输能力。
但是不幸的是,凤凰国的财力有限,道路建设经费只够重建一条道路。
伊基想要知道共有多少条道路可以纳入重建道路候选名单。
这些道路需要满足,将其重建后,国家的总运输能力能够增加。
输入格式
第一行包含 \(N\) 和 \(M\),分别表示城市和道路的数量。
接下来 \(M\) 行,每行包含三个整数 \(a,b,c\),表示存在一条道路从城市 \(a\) 通往城市 \(b\),且运输能力为 \(c\)。
所有道路都是有方向的。
城市编号从 \(0\) 到 \(N−1\)。
生产日常商品的城市为 \(0\) 号城市,首都为 \(N−1\) 号城市。
输出格式
输出一个整数 \(K\),表示存在 \(K\) 条道路,对其中每条道路进行重建都会增加运输网络的运输能力。
数据范围
\(1≤N≤500, 1≤M≤5000, 0≤a,b<N, 0≤c≤100\)
输入样例:
2 1
0 1 1
输出样例:
1
题目让我们找一条边,使得增大这条边的权值能够增大最大流的值。
对于某个最大流 \(f\) 首先我们可以很快想到,这条边一定是满流的,也就是瓶颈边,另外,若这条边为 \((u,v)\) ,当在残留网络中存在一条非 \(0\) 路径 \(s\rightarrow u\) 和 \(v\rightarrow t\) 时,这条边一定被选中。反之亦然。
所以,我们只用随便求一个最大流,然后在残留网络里面搜一下源点能到的点,再从汇点搜一下能反向到汇点的点,最后再枚举所有边,判断是否满流且端点能否满足条件。
#include <bits/stdc++.h>
using namespace std;
const int N=1e4+10,M=2e5+10,INF=1e8;
int n,m,S,T;
int head[N],ver[M],nxt[M],cc[M],tot=0;
void add(int x,int y,int c)
{
ver[tot]=y; cc[tot]=c; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],cur[N];
bool viss[N],vist[N];
bool bfs()
{
int hh=0,tt=0;
memset(d,-1,sizeof d);
q[0]=S,d[S]=0,cur[S]=head[S];
while(hh<=tt)
{
int x=q[hh++];
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(d[y]==-1&&cc[i])
{
d[y]=d[x]+1;
cur[y]=head[y];
if(y==T) return 1;
q[++tt]=y;
}
}
}
return 0;
}
int find(int u,int lim)
{
if(u==T) return lim;
int flow=0;
for(int i=cur[u];~i&&flow<lim;i=nxt[i])
{
cur[u]=i;
int y=ver[i];
if(d[y]==d[u]+1 && cc[i])
{
int tmp=find(y,min(cc[i],lim-flow));
if(!tmp) d[y]=-1;
cc[i]-=tmp;cc[i^1] +=tmp; flow+=tmp;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=find(S,INF)) res+=flow;
}
return res;
}
void dfs(int x,bool vis[],int t)
{
vis[x]=1;
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i],j=i^t;
if(cc[j] && !vis[y])
dfs(y,vis,t);
}
}
int main()
{
scanf("%d%d",&n,&m);
S=0,T=n-1;
memset(head,-1,sizeof head);
for(int i=1;i<=m;i++)
{
int x,y,c;
scanf("%d%d%d",&x,&y,&c);
add(x,y,c);
}
int ans=dinic();
dfs(T,vist,1);
dfs(S,viss,0);
int res=0;
for(int i=0;i<m*2;i+=2)
if(!cc[i] && viss[ver[i^1]] && vist[ver[i]]) res++;
printf("%d",res);
}
最大流判定
秘密挤奶机
农夫约翰正在制造一台新的挤奶机,并希望对这件事进行严格保密。
他将挤奶机藏在了农场的深处,使他能够在不被发现的情况下,进行这项任务。
在机器制造的过程中,他要在牛棚和挤奶机之间一共进行 \(T\) 次往返。
他有一个秘密通道只能在返程时使用。
农场由 \(N\) 个地标(编号 \(1∼N\))组成,这些地标由 \(P\) 条双向道路(编号 \(1∼P\))连接。
每条道路的长度为正,且不超过 \(10^6\)。
多条道路可能连接同一对地标。
为了尽可能的减少自己被发现的可能性,农场中的任何一条道路都最多只能使用一次,并且他应该尝试使用尽可能短的道路。
帮助约翰从牛棚(地标 \(1\))到达秘密挤奶机(地标 \(N\))总共 \(T\) 次。
找到他必须使用的最长的单个道路的最小可能长度。
请注意,目标是最小化所使用的最长道路的长度,而不是最小化所有被使用道路的长度之和。
保证约翰可以在不走重复道路的情况下完成 \(T\) 次行程。
输入格式
第一行包含三个整数 \(N,P,T\)。
接下来 \(P\) 行,每行包含三个整数 \(A_i,B_i,L_i\),表示地标 \(A_i\) 和 \(B_i\) 之间存在一条长度为 \(L_i\) 的道路。
输出格式
输出一个整数,表示约翰必须使用的最长的单个道路的最小可能长度。
数据范围
\(1≤T≤200,\\ 2≤N≤200,\\ 1≤P≤40000,\\ 1≤Ai,Bi≤N,\\ 1≤Li≤106\)
输入样例:
7 9 2
1 2 2
2 3 5
3 7 5
1 4 1
4 3 1
4 5 7
5 7 1
1 6 3
6 7 3
输出样例:
5
样例解释
约翰可以选择以下两条路径:\(1−2−3−7\) 和 \(1−6−7\)。
两条路径中包含的所有道路长度均不超过 \(5\)。
约翰无法在仅使用长度小于 \(5\) 的道路的情况下,两次从 \(1\) 到 \(7\)。
解析
二分+网络流
首先我们能想到的是二分,因为题中有 “最小化最大” 这类字眼。
再看二分正确性。
二分需要目标函数满足单调性或二段性,才能得到正确答案。
在本题中,我们可以想到,当最小化的最长长度是 \(x_0\) 时,大于 \(x_0\) 的值一定能够满足题中条件,小于 \(x_0\) 的值一定无解。所以满足二段性,二分可以使用。
接下来的任务是如何判定当长度最大值为某个值 \(x_i\) 时是否有解的问题。
我们可以先在原图 \(G\) 把长度大于 \(x_i\) 的边都删掉得到新图 \(G_0\),问题就变成了在 \(G_0\) 中是否存在 \(T\) 条互相没有公共边的路径。
这个问题是可以使用网络流解决的。 由于原图是无向图,所以我们在网络建边时要建两条相反的容量为 \(1\) 的有向边。最大流的值就是路径数。
虽然我们在讨论网络流定义的时候是没有考虑反向边的,但是我们讨论出来的定义与反向边兼容。
然而还有问题,我们这样建边只能保证两条边分别走一次啊?
其实不用担心,如果真的过去一次回来一次,那么这两点之间流量就能够抵消了,对答案没有影响。很多时候考虑网络流时并不需要考虑流是怎么走的,那样太麻烦了。
还有个问题,此时残留网络两个点之间整整有 \(4\) 边,其实大可不必。 网络流中我们是可以合并重边的,合并出来的边容量等于两边容量相加。也就是说,两点之间建两条边就够了。
当然,你要建 \(4\) 条边也没人拦你就是了。
code
#include <bits/stdc++.h>
using namespace std;
const int N=510,M=1e5+10,INF=1e8+10;
int head[N],ver[M],nxt[M],cc[M],w[M],tot=0;//w代表长度
void add(int x,int y,int c)
{
ver[tot]=y; w[tot]=c; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; w[tot]=c; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],cur[N];
int n,m,K,S,T;//原题的T=K
bool bfs()
{
int hh=0,tt=0;
memset(d,-1,sizeof d);
q[0]=S,d[S]=0,cur[S]=head[S];
while(hh<=tt)
{
int x=q[hh++];
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(d[y]==-1&&cc[i])
{
d[y]=d[x]+1;
cur[y]=head[y];
if(y==T) return 1;
q[++tt]=y;
}
}
}
return 0;
}
int find(int u,int lim)
{
if(u==T) return lim;
int flow=0;
for(int i=cur[u];~i&&flow<lim;i=nxt[i])
{
cur[u]=i;
int y=ver[i];
if(d[y]==d[u]+1 && cc[i])
{
int tmp=find(y,min(cc[i],lim-flow));
if(!tmp) d[y]=-1;
cc[i]-=tmp;cc[i^1]+=tmp;flow+=tmp;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
if(flow=find(S,INF)) res+=flow;
}
return res;
}
bool check(int mid)
{
for(int i=0;i<tot;i++)
if(w[i]>mid) cc[i]=0;//删去长度>mid的边->将其容量设为0
else cc[i]=1;
return (dinic()>=K);
}
int main()
{
scanf("%d%d%d",&n,&m,&K);
S=1,T=n;
memset(head,-1,sizeof head);
for(int i=1;i<=m;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);//这里我们先确定长度,容量我们二分时再确定
}
int L=1,R=(int)1e6;
while(L<R)
{
int mid=L+R>>1;
if(check(mid)) R=mid;
else L=mid+1;
}
printf("%d",R);
return 0;
}
拆点
餐饮
奶牛们在吃饭方面十分挑剔。
每头奶牛都有自己喜欢的食物和饮料,并且不会食用其他不喜欢的食物和饮料。
农夫约翰为他的奶牛们做了美味的饭菜,但他忘了对照他们的喜好来检查菜单。
虽然他可能无法令所有奶牛满意,但他想给尽可能多的奶牛提供一顿完整的用餐----既有食物可吃,也有饮料可喝。
农夫约翰一共烹制了 \(F\) 种食物,并提供了 \(D\) 种饮料。
约翰共有 N 头奶牛,其中第 \(i\) 头奶牛有 \(F_i\) 种喜欢的食物以及 \(D_i\) 种喜欢的饮料。
约翰需要给每头奶牛分配一种食物和一种饮料,并使得有吃有喝的奶牛数量尽可能大。
每种食物或饮料都只有一份,所以只能分配给一头奶牛食用(即,一旦将第 \(2\) 种食物分配给了一头奶牛,就不能再分配给其他奶牛了)。
输入格式
第一行包含三个整数 \(N,F,D\)。
接下来 \(N\) 行,其中第 \(i\) 行描述第 \(i\) 头奶牛的饮食喜好,首先包含两个整数 \(F_i\) 和 \(D_i\),表示其喜欢的食物和饮料数量,然后包含 \(F_i\) 个整数表示其喜欢的食物的种类编号,最后包含 \(D_i\) 个整数表示其喜欢的饮料的种类编号。
食物编号从 \(1\) 到 \(F\),饮料编号从 \(1\) 到 \(D\)。
输出格式
输出一个整数,表示能够有吃有喝的奶牛的最大数量。
数据范围
\(1≤N,F,D≤100,\\ 1≤Fi≤F,\\ 1≤Di≤D\)
输入样例:
4 3 3
2 2 1 2 3 1
2 2 2 3 1 2
2 2 1 3 1 2
2 1 1 3 3
输出样例:
3
样例解释
一种使得三头奶牛满意的可行方法是:
奶牛 \(1\):没饭。
奶牛 \(2\):食物 \(2\),饮料 \(2\)。
奶牛 \(3\):食物 \(1\),饮料 \(1\)。
奶牛 \(4\):食物 \(3\),饮料 \(3\)。
解析
这是一个三分图匹配(如果有这个说法的话)
参照二分图匹配的建图方式,我们可以由源点向每个食物连一条容量为 \(1\) 的边,每个饮料向汇点连一条容量为 \(1\) 的边,然后对应关系连边。
于是我们又会发现一个问题,一个牛可能被供给多个食物和饮料。换个角度说,一个牛会去对应多个饮料和食物,牛的匹配数量无法控制。
这个时候有一个常用技巧:拆点
一般拆点特指将一个点拆成入点和出点。
形象的说,我们让牛裂开,变成两个点,中间连一条容量为 \(1\) 的点。
至此之后,对应性证明跟二分图匹配就是差不多的,就不给了(逃。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=2e6+10,INF=1e8;
int n,ff,dd,S,T;
int head[N],ver[M],nxt[M],cc[M],tot=0;
void add(int x,int y,int c)
{
ver[tot]=y; cc[tot]=c; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],cur[N];
bool bfs()
{
int hh=0,tt=0;
memset(d,-1,sizeof d);
q[0]=S,d[S]=0,cur[S]=head[S];
while(hh<=tt)
{
int x=q[hh++];
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(d[y]==-1 && cc[i])
{
d[y]=d[x]+1;
cur[y]=head[y];
if(y==T) return 1;
q[++tt]=y;
}
}
}
return 0;
}
int find(int u,int lim)
{
if(u==T) return lim;
int flow=0;
for(int i=cur[u];~i&&flow<lim;i=nxt[i])
{
int y=ver[i];
cur[u]=i;
if(d[y]==d[u]+1 && cc[i])
{
int tmp=find(y,min(cc[i],lim-flow));
if(!tmp) d[y]=-1;
cc[i]-=tmp; cc[i^1] +=tmp; flow+=tmp;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
while(flow=find(S,INF)) res+=flow;
}
return res;
}
int main()
{
scanf("%d%d%d",&n,&ff,&dd);
S=N-2,T=N-3;
memset(head,-1,sizeof head);
for(int i=1;i<=n;i++)//拆点 编号1-n*2,单数(2*牛编号-1)为入点
add((i<<1)-1,(i<<1),1);
for(int i=1;i<=ff;i++)//食物编号(2n+1-2n+ff)
add(S,i+n*2,1);
for(int i=1;i<=dd;i++)
add(n*2+ff+i,T,1);//饮料编号(2n+ff-2n+ff+dd)
for(int i=1;i<=n;i++)
{
int idi=2*i-1,ido=idi+1;
int F,D;
scanf("%d%d",&F,&D);
for(int i=1;i<=F;i++)
{
int x;
scanf("%d",&x);
add(2*n+x,idi,1);
}
for(int i=1;i<=D;i++)
{
int x;
scanf("%d",&x);
add(ido,2*n+ff+x,1);
}
}
printf("%d",dinic());
return 0;
}
南极企鹅
在南极附近的某个地方,一些企鹅正站在一些浮冰上。
作为群居动物,企鹅们喜欢聚在一起,因此,它们想在同一块浮冰上会合。
企鹅们不想淋湿自己,所以它们只能利用自己有限的跳跃能力,在一块块浮冰之间跳跃移动,从而聚在一起。
但是,最近的温度很高,浮冰上也有了裂纹。
每当企鹅在一块浮冰上发力跳到另一块浮冰上时,起跳的浮冰都会遭到破坏,落点的浮冰并不会因此受到影响。
当浮冰被破坏到一定程度时,浮冰就会消失。
现在已知每块浮冰可以承受的具体起跳次数。
请帮助企鹅找出它们可以会合的所有浮冰。
上图是一个浮冰上站着 \(3\) 个企鹅的示例图。
输入格式
第一行一个整数 \(T\),表示测试数据数量。
对于每组测试数据:
第一行包含一个整数 \(N\) 和一个浮点数 \(D\),表示冰块的数量以及企鹅可以跳跃的最大距离。
接下来 \(N\) 行,第 \(i\) 行包含四个整数 \(x_i,y_i,n_i,m_i,\)用来描述一块浮冰的 \(x\) 坐标、\(y\) 坐标、该浮冰上站着的企鹅数量以及该浮冰可以承受的起跳次数。
\(N\) 块浮冰按照输入的顺序,依次编号为 \(0∼N−1\)。
输出格式
对于每组测试数据:
输出占一行,按从小到大的顺序输出所有可以用来会合的浮冰的编号。
如果无法会合,则输出 \(−1\)。
数据范围
\(1≤T≤100,\\ 1≤N≤100,\\ 0≤D≤105,\\ −10000≤xi,yi≤10000,\\ 0≤ni≤10,\\ 1≤mi≤200\)
输入样例:
2
5 3.5
1 1 1 1
2 3 0 1
3 5 1 1
5 1 1 1
5 4 0 1
3 1.1
-1 0 5 10
0 0 3 9
2 0 1 1
输出样例:
1 2 4
-1
解析
在网络流中,我们有时可以把题目中的有些东西抽象为流。
比如本题,我们可以把企鹅抽象为企鹅流,它们最终都会流向同一浮冰。
能够跳跃到的浮冰互相连边。
企鹅多源点比较好解决,建超级源点即可。
点数不多,我们每次都可以按编号从小到大枚举汇点,判断最大流是否等于企鹅总数,若等于则输出编号。
多测记得清空数组,以及每次做完最大流要还原网络。
还原网络可以在原边的剩余容量上加上流量,反向边容量清零。
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N=1e5+10,M=2e6+10,INF=1e8;
int n,S,T;
double D;
int head[N],ver[M],nxt[M],cc[M],tot=0;
void add(int x,int y,int c)
{
ver[tot]=y; cc[tot]=c; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],cur[N];
PII pos[N];
#define fi first
#define se second
inline bool check(PII a,PII b)
{
return (double)(a.fi-b.fi)*(a.fi-b.fi)+(a.se-b.se)*(a.se-b.se)<(double)D*D+0.000001;
}
bool bfs()
{
int hh=0,tt=0;
memset(d,-1,sizeof d);
q[0]=S,d[S]=0,cur[S]=head[S];
while(hh<=tt)
{
int x=q[hh++];
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(d[y]==-1 && cc[i])
{
d[y]=d[x]+1;
cur[y]=head[y];
if(y==T) return 1;
q[++tt]=y;
}
}
}
return 0;
}
int find(int u,int lim)
{
if(u==T) return lim;
int flow=0;
for(int i=cur[u]; ~i&&flow<lim;i=nxt[i])
{
int y=ver[i];
cur[u]=i;
if(d[y]==d[u]+1 && cc[i])
{
int tmp=find(y,min(cc[i],lim-flow));
if(!tmp) d[y]=-1;
cc[i]-=tmp; cc[i^1]+=tmp; flow+=tmp;
}
}
return flow;
}
int dinic()
{
int res=0,flow;
while(bfs())
{
if(flow=find(S,INF)) res+=flow;
}
return res;
}
int main()
{
int ca;
scanf("%d",&ca);
while(ca--)
{
memset(head,-1,sizeof head);
tot=0;
scanf("%d%lf",&n,&D);
S=0;
int sum=0;
for(int i=1;i<=n;i++)
{
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
pos[i]={a,b};
add(S,i,c);
add(i,n+i,d);//拆点
sum+=c;
}
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
if(check(pos[i],pos[j]))
{
add(n+i,j,INF);
add(n+j,i,INF);//冰块之间连边
}
}
}
int cnt=0;
for(int i=1;i<=n;i++)
{
T=i;//枚举汇点
for(int j=0;j<tot;j+=2)
cc[j]+=cc[j^1],cc[j^1]=0;//还原网络
if(dinic()==sum)
{
printf("%d ",i-1);
cnt++;
}
}
if(!cnt) printf("-1\n");
else printf("\n");
}
}
一个有意思的建图
米尔克的猪
米尔克在一家养猪场工作,该养猪场共有 M 个上了锁的猪舍。
由于米尔克没有钥匙,所以他无法打开任何猪舍。
顾客们顺次来到了养猪场,不会有两个顾客同一时间过来。他们中的每个人都有一些猪舍的钥匙,并且想买一定数量的猪。
米尔克每天一大早就可以得到计划在当天来到农场的客户的所有数据,以便他制定销售计划,从而最大限度地提高出售生猪的数量。
更准确的说,过程如下:
顾客到达养猪场,将所有他有钥匙的猪舍的大门全部打开,
米尔克从所有未上锁的猪舍中挑选一定数量的猪卖给该顾客。
如果米尔克愿意,他还可以给未上锁的猪舍里剩下的猪重新分配位置。
在每个顾客到达之前,会将上一个顾客打开的猪舍全部关闭。
每个猪舍中都可以放置无限数量的猪。
请你编写一个程序,计算他当天可以出售的生猪的最大数量。
输入格式
第一行包含两个整数 \(M\) 和 \(N\),表示猪舍数量以及顾客数量。
猪舍编号从 \(1\) 到 \(M\),顾客编号从 \(1\) 到 \(N\)。
第二行包含 \(M\) 个整数,表示每个猪舍初始猪的数量(不少于 \(0\),不超过 \(1000\))。
接下来 \(N\) 行,描述所有顾客的信息(第 \(i+2\) 行描述第 \(i\) 个到来的顾客的信息):每行首先包含一个整数 \(A\),表示该顾客拥有的钥匙数量,然后包含 \(A\) 个整数 \(K_1,K_2,…,K_A\),表示钥匙对应的猪舍编号(按升序给出),最后包含一个整数 \(B\),表示他想购买的猪的数量。
输出格式
输出可以出售的生猪的最大数量。
数据范围
\(1≤M≤1000,\\ 1≤N≤100,\\ 0≤A≤M,\\ 1≤Ki≤M,\\ 0≤B≤10000\)
输入样例:
3 3
3 1 10
2 1 2 2
2 1 3 3
1 2 6
输出样例:
7
解析
如果猪舍里的猪不能被调整,我们很容易想到这是一个类似于二分图多重匹配的问题。
那多了个猪能调整,我们就在猪舍之间连边嘛。
这样建图有一个问题,当我们在跑最大流的时候,就相当于让所有的顾客同时到达,不符合题意,没有体现时序性。
那怎样体现时序性?分层?
我们尝试一下,发现分层也很复杂,不可做。
我们想到,到达顾客手里的猪有两种情况。一是这个猪舍没被打开过,猪舍里面全是新猪。二是这个猪舍已经被打开过了,里面的猪是别的顾客选剩的。
我们并不关心每个猪舍里面猪到底怎么样,我们只关心顾客总共能拿到多少猪。
所以我们可以抽象猪为猪流。
顾客手中的猪只有两个来源,一是从新猪舍里面来的,二是从旧猪舍里面来的,也就是从其他顾客手中来的。
所以我们可以如下建图。
超级源点超级汇点先建上。将顾客抽象为点。
每个顾客
我们从源点向第一次打开某个猪舍的顾客连一条容量为这个猪舍猪的初始数量的边。
第一次打开某个猪舍的顾客向第二次打开这个猪舍的顾客连一条边,容量为 \(+\infty\)
第二次打开这个猪舍的顾客向第三次打开这个猪舍的顾客连一条边,容量为 \(+\infty\)
\(......\)
这样就能体现时序性了。放心大胆跑 \(Dinic\) 即可。
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10, M=2e6+10, INF=2e8;
int head[N],ver[M],nxt[M],cc[M],tot=0;
void add(int x,int y,int c)
{
ver[tot]=y; cc[tot]=c; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],cur[N];
int poi[N],lst[N];//上一次打开这个猪舍的顾客编号
int n,k;
int S,T;
bool bfs()
{
int hh=0,tt=0;
memset(d,-1,sizeof d);
q[0]=S,d[S]=0,cur[S]=head[S];
while(hh<=tt)
{
int x=q[hh++];
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(d[y]==-1 && cc[i])
{
cur[y]=head[y];
d[y]=d[x]+1;
if(y==T) return 1;
q[++tt]=y;
}
}
}
return 0;
}
int find(int u,int lim)
{
if(u==T) return lim;
int flow=0;
for(int i=cur[u];~i&&flow<lim;i=nxt[i])
{
int y=ver[i];
cur[u]=i;
if(d[y]==d[u]+1 && cc[i])
{
int tmp=find(y,min(cc[i],lim-flow));
if(!tmp) d[y]=-1;
cc[i]-=tmp; cc[i^1]+=tmp; flow+=tmp;
}
}
return flow;
}
int dinic()
{
int res=0, flow=0;
while(bfs())
{
while(flow=find(S,INF)) res+=flow;
}
return res;
}
int main()
{
scanf("%d%d",&k,&n);
memset(head,-1,sizeof head);
S=n+3,T=n+1;
for(int i=1;i<=k;i++)
scanf("%d",&poi[i]);
for(int i=1;i<=n;i++)
{
int a,b;
scanf("%d",&a);
for(int j=1;j<=a;j++)
{
int x;
scanf("%d",&x);
if(!lst[x]) add(S,i,poi[x]);
else if(lst[x]>0) add(lst[x],i,INF);
lst[x]=i;
}
scanf("%d",&b);
add(i,T,b);
}
printf("%d\n",dinic());
return 0;
}