网络瘤24题解+总结
updata:删除了板子题部分,因为除了使得本文冗余之外没有任何作用。
顺序主观决定,
至于为什么不是24个,嘿嘿,机器人那个被我吃了
至于为什么不是23个,嘿嘿,软件的补丁是好东西,可不是毒瘤
众所周知,在网络流24里,有两个假题。。。。一个是IDA*的搜索,一个是状态压缩的最短路。。。
其实,硬要说,是三个假题,因为某题严格来说是最短路。。。
如果不行的话,其实,有五个题正解和网络流没有关系。。
网络流 题-思维提高篇
(注:水题在下面的基础技巧篇)
太空飞行计划
教训:(开始想费用流,搞半天出不来)
网络流解决最大/小费用问题,要么最小割最大流,要么最小费用流
最小费用流的前提是最大流,所以在有一些点可以不取换最优代价的时候,是不可行的。
当每个点都只能取一次的时候,可以考虑费用化为容量,求最小割/最大流。
回到这个题,假如我们把一个实验所需器材用有向边连起来,问题就化为了:
在一个二分图中,左部点向右部点有若干连边,若选择了某一个左部点,则所连右部点必选。求最后最大的点权和
将其转化为有向图,问题化为:
在一张有向图中,选出一个子图,满足选择了一个点,它的后继所有点都必选,求最大和
最大权闭合子图模型
这个模型是用来解决上述问题的。
定理:若将源点与所有正权点连边,容量为点权,汇点与所有负权点连边,容量为点权的相反数,同时保留原有向图所有边,容量正无穷,则正权点的点权和减去最小割即为所求。
被割掉的边的意义:
- 边
被割,表示不选 - 边
被割,表示选
这样就可以求出最大权闭合子图的点集,进而求出图
引理:Dinic 算法最后一次分层(failed)的
由此,我们可以由源点向每个实验连边,容量为收益,由汇点向每个器材连边,容量为代价。由每个实验向所需器材连边,容量正无穷。
若某实验没被割,则加入答案。若某器材被割,则加入答案。
read(m);read(n);int ans=0;s=n+m+1,t=n+m+2;num=t;
for(int i=1;i<=m;i++){
int y;
int x=read(y);
add(s,i,y);ans+=y;
while(!x){
int y;x=read(y);
add(i,m+y,inf);
}
}//鬼畜输入
for(int i=1;i<=n;i++){
int x;read(x);add(m+i,t,x);
}
ans-=dinic();
for(int i=1;i<=m;i++)if(dep[i])cout<<i<<" ";
cout<<"\n";
for(int i=m+1;i<=n+m;i++)if(dep[i])cout<<i-m<<" ";cout<<"\n";
cout<<ans<<"\n";
最小路径覆盖问题
给定一个DAG,求其最小路径覆盖。
这里体现的重要思想有两个:
- 将一个点的不同状态拆分,化为左右部,分别求解
- 部分计算,合并答案
- 正难则反,拆分——>合并
首先,我们将在DAG里拆路径的过程倒过来,变为合并路径。最初将
然后我们考虑合并路径的操作。要路径条数最少,则合并次数最多。
注意到有向图中,每个点有两个状态:作为该边的入点,作为该边的出点。而在路径合并中,显然要区分开这两种状态,只能入状态向出状态合并。
为什么?我们需要的是合并次数,而非正常建图的流量。求最大流,也即最大化流量,而现在我们要最大化合并次数,所以合并次数就是我们的流量,而合并次数为了判重(一个点最多给1次),必须一对对拆。
显然原图一条边最多提供1次合并,那么对于原图的每一条边,由入状态向出状态的另一端点连边,容量1,表示提供1的合并次数。
那么对于入状态的点,可以找一个点合并,则由源点向其连边,容量1
对于出状态的点,可以被一个点合并,则由其向汇点连边,容量1
综上,我们将一个点拆为
用总点数减去最大合并次数即可得到最小路径数。
关于方案输出?网络流的方案输出一般体现在满流边和残量网络上,当然也可以像动态规划一样记录上一个点。
这里常用的方法是由汇点开始找残量网络,找到的路径就对应了合并关系。
但更简单的方法?注意到原图里边的容量全是1,那么就必有
请注意,这里还没完,我们只得到了若干对合并关系,但不能就此输出整个路径。我们可以通过合并关系确定每个点在合并后路径上的
vector<int>f[N];int dcc;
int vis[N],suf[N];
void init(){
cin>>n>>m;s=n+n+1,t=n+n+2;num=t;
for(int i=1;i<=m;i++){
int u,v;cin>>u>>v;add(u,v+n,1);
}
for(int i=1;i<=n;i++)add(s,i,1);
for(int i=n+1;i<=n<<1;i++)add(i,t,1);
int ans=dinic();
for(int i=head[t];i;i=nxt[i]){
if(cost[i]==1){
++dcc;int x=ver[i];
while(x!=s){
f[dcc].push_back(x),x=lst[x];
}
if(f[dcc][1]>n)f[dcc][1]-=n;
if(f[dcc][0]>n)f[dcc][0]-=n;
suf[f[dcc][1]]=f[dcc][0];vis[f[dcc][0]]=1;
}
}
for(int i=1;i<=n;i++){
if(!vis[i]){
int x=i;
while(x){
cout<<x<<" ";x=suf[x];
}cout<<"\n";
}
}
cout<<n-ans<<"\n";
}
下面来看一个同为网络流24题的变形题。
魔术球问题
一个显然的结论是:柱子越多,可以投入的数越大,我们可以考虑逐步增大答案。
注意到我们如果将两个和为完全平方数的数连边,限制大连小,那么一个石柱其实就对应了这样一张DAG的简单路径。问题化为求该DAG的最小路径覆盖。当这个路径覆盖数超过
vector<int>f[N];int dcc,ans;
void dinic(){
while(bfs()){
for(int i=1;i<=num;i++)cur[i]=head[i];
ans+=dfs(s,0x3f3f3f3f);
}
// cout<<ans<<"\n";
}
int vis[N],suf[N];
void init(){
cin>>n;
s=1,t=2;int d=0;
for(int i=1;;i++){
int x=i*2+1,y=(i+1)*2;
add(s,x,1);add(y,t,1);
for(int j=1;j<i;j++){
int q=sqrt(i+j);if(i+j==q*q){
add(x,(j+1)*2,1);
}
}
num=y;
dinic();
if(i-ans==n+1){
cout<<i-1<<"\n";d=i-1;break;
}
}
for(int i=1;i<=num;i++)head[i]=0;tot=1;
for(int i=1;i<=d;i++){
int x=i*2+1,y=(i+1)*2;
add(s,x,1);add(y,t,1);
for(int j=1;j<i;j++){
int q=sqrt(i+j);if(i+j==q*q){
add(x,(j+1)*2,1);
}
}
}
num=d*2+2;
dinic();
for(int i=head[t];i;i=nxt[i]){
if(cost[i]==1){
++dcc;int x=ver[i];
while(x!=s){
f[dcc].push_back(x),x=lst[x];
}
f[dcc][0]=f[dcc][0]/2-1;
f[dcc][1]/=2;
suf[f[dcc][0]]=f[dcc][1];vis[f[dcc][1]]=1;
}
}
for(int i=1;i<=d;i++){
if(!vis[i]){
int x=i;
while(x){
cout<<x<<" ";vis[x]=1;x=suf[x];
}cout<<"\n";
}
}
}
最长 k 可重区间集问题
题意:给定若干 开区间,求选出一个区间集
真的是非常巧妙的问题!
首先,虽然这是个区间操作问题,我们仍然可以将其抽象为(亦或者说微观化?)序列元素操作问题。我们将区间端点离散化后,这题就变成了:选出若干区间,使得每个点覆盖次数小于等于
我们需要解决的问题有两个:
- 如何表示区间覆盖
- 如何表示一个点的覆盖次数
注意到我们要求的是最大长度为区间长度和,而非覆盖范围。每一个点必定在最后都是合法的,而我们解决问题的基本元素就是点,则最终网络满流,所以这个长度和必定是一个费用,本题为最大费用最大流问题。
那么用费用表示区间长度,则只能用流量表示覆盖次数了。如果我们设经过一个点的流量表示这个点的覆盖次数,可以吗?发现难以维护区间被覆盖的问题,本质原因是在流网络中,加边的后果就是导致分流。分流有两种情况,一是从源点引流,增加该边以后的流量,而是从其他点分流,使得经过其他点的流量减少,而我们影响的对象是该区间内的点,只能选择分流该区间的流量。此时就是本题做法的最精妙之处了:设经过一个点的流量表示其还能被覆盖几次。正难则反的思想是处处可见呢!
有了这个状态定义,整个问题就变得容易了起来。状态的定义是解决问题的大前提,类似于战略
我们为了限制一个点的被覆盖次数最多
那么我们考虑使得通过一个点的流量减少1,则有一条容量为1的,费用更优的边在旁边分流。故建立
有最大流的限制,使得求该网络的最大费用即为所求。
回顾整个题,不难发现闪烁思想光芒的点主要有三个:
- 问题转化——将区间选择更换角度为点的选择
- 状态定义——运用推理的方法,找到最优的状态
- 最大流性质—利用所有点必选的最大流限制,来限制区间选择
在这里提一句,在费用流问题中,流量是第一关键字,费用只是第二关键字。一般我们用费用流求最优化问题时,都有所有状态必选的性质。
void init(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>l[i]>>r[i];if(l[i]>r[i])swap(l[i],r[i]);a[++m]=l[i],a[++m]=r[i];
}
a[++m]=0;
sort(a+1,a+m+1);
m=unique(a+1,a+m+1)-a-1;
for(int i=1;i<=n;i++){
int L=lower_bound(a+1,a+m+1,l[i])-a;
int R=lower_bound(a+1,a+m+1,r[i])-a;
add(L,R,1,l[i]-r[i]);
}
for(int i=1;i<=m;i++)add(i,i+1,k,0);s=m+2,t=m+3;num=t;
add(s,1,k,0);add(m+1,t,k,0);
zkw(-1);
}
最长 k 可重线段集问题
类比上题,我们现在来解决线段集问题。显然,在这个题里面,
真的吗?不是的。考虑存在两条线段:
处理这种问题,在数轴上,我们选择扩域,将每个点扩两个域,中间部分用来表示该点。所以连接
好的交上去91pts,为什么?
注意到线段
综上,对于
扩域时要格外注意相等的情况怎么处理
void init(){
cin>>n>>k;
for(int i=1;i<=n;i++){
int x,y;
cin>>l[i]>>x>>r[i]>>y;if(l[i]>r[i])swap(l[i],r[i]);
len[i]=sqrt((l[i]-r[i])*(l[i]-r[i])+(x-y)*(x-y));
l[i]<<=1;r[i]<<=1;
if(l[i]==r[i])r[i]++;
else l[i]++;
a[++m]=l[i],a[++m]=r[i];
}
a[++m]=0;
sort(a+1,a+m+1);
m=unique(a+1,a+m+1)-a-1;
for(int i=1;i<=n;i++){
int L=lower_bound(a+1,a+m+1,l[i])-a;
int R=lower_bound(a+1,a+m+1,r[i])-a;
add(L,R,1,-len[i]);
}
for(int i=1;i<=m;i++)add(i,i+1,k,0);s=m+2,t=m+3;num=t;
add(s,1,k,0);add(m+1,t,k,0);
zkw(-1);
}
星际转移问题
结合时间轴的网络流问题。
先判无解再说。如何?将一个飞船经过的所有空间站全部合并在一起,表示可以互达。这一步用并查集。如果最后地球月球不在同一连通块,就没了。
然后我们考虑求解这个问题。注意问题与时间有关,而数据很小,刻画空间站状态需要用到时间,所以我们考虑对每一时间的空间站都开一个状态,但需要当前时刻向下一时刻的空间站连边,容量无穷,表示时间的转移,人留在了这个太空站。接着,源汇点分别向该时刻的地球月球连边,容量无穷。而每一个飞船可以计算得到上一时刻的某个空间站可以转移到当前时刻的空间站,故连边,容量为该飞船容量。
下面我们再来看一个与时间有关的网络流题。
餐巾计划问题
本题的核心思想是:将同一事物的不同状态拆点
首先注意到餐巾分为新旧两类,且第
那么我们用
再者,对于题中的几个操作,显然可以连边
同时,当天没用的旧餐巾可以转递到下一天,有
本题有三分结合贪心的算法,可以做到
void init(){
cin>>n>>g>>d1>>c1>>d2>>c2;
s=n+n+1,t=n+n+2,num=t;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)add(s,i+n,a[i],0),add(s,i,inf,g),add(i,t,a[i],0);
for(int i=1;i<n;i++)add(i+n,i+n+1,inf,0);
for(int i=1;i<=n;i++){
if(i+d1<=n)add(i+n,i+d1,inf,c1);
if(i+d2<=n)add(i+n,i+d2,inf,c2);
}
}
最长不下降子序列问题
本题核心:动态规划结合网络流,话说怎么有点类似某教辅品牌所谓“三阶分层练”
首先,最长不下降子序列可以使用动态规划求出,解决第一问。
我们现在来思考动态规划给我们的
引理:原序列中,任意一个最长不下降子序列的第
证明:如果这样的
那这就简单了,说明此为一个 分层图网络流。
第三问?将源点与
signed main(){
ios::sync_with_stdio(false);
cin>>n;s=n+n+1,t=n+n+2;num=t;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++){
f[i]=1;
for(int j=1;j<i;j++){
if(a[j]<=a[i])f[i]=max(f[i],f[j]+1);
}
mx=max(mx,f[i]);
}
cout<<mx<<"\n";int id1=-1,id2=-1;
for(int i=1;i<=n;i++){
if(f[i]==1)add(s,i,1);
if(f[i]==mx)add(i+n,t,1);
if(i==n&&f[i]==mx)id2=tot;
add(i,i+n,1);
if(i==n)id1=tot;
}
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
if(a[j]<=a[i]&&f[i]==f[j]+1)add(j+n,i,1);
}
}
cnt=dinic();cout<<cnt<<"\n";
if(n==1){
cout<<"1\n";return 0;
}
cost[2]=inf,cost[3]=0;cost[4]=inf,cost[5]=0;
if(id1!=-1)cost[id1]=0,cost[id1^1]=inf;
if(id2!=-1)cost[id2]=0,cost[id2^1]=inf;
cout<<cnt+dinic();
}
航空路线问题
注意到双向道路,从西到东等价于从东到西,所以原题化为求出两条起点终点分别为
How?注意到网络流当流量恒为1时其等价于找一条
但怎么限制途径城市最多呢?用费用来限制。
拆点,将每一个城市(除起终点)拆为
然后跑网络流。方案的话,顺着残量网络跑dfs,一次只找一条路径,然后把流量减掉,找两条合并输出即可。
map<string,int>h;
string a[N];
void init(){
cin>>n>>m;s=n+n+1,t=s+1,num=t;
for(int i=1;i<=n;i++){
cin>>a[i];h[a[i]]=i;add(i,i+n,1+(i==1||i==n),-1);
}
for(int i=1;i<=m;i++){
string x,y;cin>>x>>y;
int u=h[x],v=h[y];
if(u>v)swap(u,v);
add(u+n,v,inf,0);
}
add(s,1,2,0);add(n+n,t,2,0);
zkw(-1);
}
int ans[N],dcc;
bool dfs(int u){
if(u==s)return true;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(i&1){
if(val[i]>0&&dfs(v)){
if(u<=n)ans[++dcc]=u;
--val[i];
return true;
}
}
}
return false;
}
signed main(){
ios::sync_with_stdio(false);
init();
dfs(t);
for(int i=1;i<=dcc;i++)cout<<a[ans[i]]<<"\n";
dcc=0;
dfs(t);
for(int i=dcc-1;i;--i)cout<<a[ans[i]]<<"\n";
}
数字梯形问题
承接上题,这是一道综合三类路径问题。
首先,我们知道:点不可重复严格强于边不可重复,同样的,我们进行拆点,连接
以下简称原图中有的边实边,
然后我们考虑点不可重复经过,这意味着虚边
考虑边不可重复经过,则实边容量为1,虚边容量无限大
考虑点,边均可重复经过,将虚实边的容量全部设置为无限大即可。
火星探险问题
首先明确本题的考察内容。对于基础地图的处理是简单的,拆出入点即可,我们主要就解决岩石标本的问题。
注意到岩石标本只能被采集一次,但换个角度,对于当前格点而言,有无岩石是两个不同的状态。
我们可以对于一个点,将其拆为出入点,用连接入点和出点的不同边表示走过当前节点的不同状态/决策。
也即建立费用、流量不同的重边。
对于走地图问题,流量一般表示为走过的次数(容量为次数上限),而费用一般表示为收益/代价。
所以,我们可以在有岩石的格点,专门建立一条边表示采集岩石这个决策,收益为1,次数上限为1。
也即容量为1,费用为1.
最后跑最大费用流即可。
可恶的是输出方案,可以考虑跑残量网络每一次暴力找路径
bool dfs(int u){
if(u==s)return true;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];if(i%2==0)continue;
if(val[i]>0){
if(dfs(v)){
if(abs(u-v)!=n*m&&u!=t)ans[++dcc]=u;
--val[i];return true;
}
}
}
return false;
}
深海机器人问题
和上题一样,但本题更加明显。对于一个点向东/北走,本质上对应了两种决策,而路径上的标本即为收益,后续路径即为决策后效性。
这题的决策体现在边上,就无需拆点了。
那么,由于标本只能被采集一次,我们仍然可以对两个相邻的点连两条边,一条边表示采集标本,容量1,费用为标本价值。而另一条边表示路过这个决策,容量inf,费用0.
最后跑最大费用流。
注意在这两个走地图问题中,有一个不易发现的套路。
只用费用表收益/决策,保证本地图满流。否则费用流就会受到最大流限制。
void init(){
int c1,c2;cin>>c1>>c2;
cin>>n>>m;n++,m++;
for(int i=1;i<=n;i++)for(int j=1;j<m;j++)cin>>a[i][j];
for(int i=1;i<=m;i++)for(int j=1;j<n;j++)cin>>b[j][i];
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)id[i][j]=++num;
//技巧:建重边,费用不同
s=num+1,t=num+2;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
int x=i,y=j;
if(x!=n){
++x;
add(id[i][j],id[x][y],1,-b[i][j]);
add(id[i][j],id[x][y],inf,0);
}
x=i,y=j;
if(y!=m){
++y;
add(id[i][j],id[x][y],1,-a[i][j]);
add(id[i][j],id[x][y],inf,0);
}
}
}
for(int i=1;i<=c1;i++){
int cnt,x,y;cin>>cnt>>x>>y;x++,y++;
add(s,id[x][y],cnt,0);
}
for(int i=1;i<=c2;i++){
int cnt,x,y;cin>>cnt>>x>>y;x++,y++;
add(id[x][y],t,cnt,0);
}
num=t;
zkw(-1);
}
汽车加油行驶问题
这是分层图的网络流问题,主要可以学到的是抽象状态的能力。
注意到在本题中,多了一个油量限制,这是独立于容量,费用之外的限制。对于这种东西,我们考虑用动态规划的思想去刻画状态。
我们将流量也视作一个状态,结合最短路,建立流量分层图即可。
int dx[4]={1,-1,0,0},dy[4]={0,0,1,-1},id[105][105][15],a,b,c,k,S[105][105];
signed main(){
ios::sync_with_stdio(false);
cin>>n>>k>>a>>b>>c;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)cin>>S[i][j];
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)for(int p=0;p<=k;p++)id[i][j][p]=++num;
s=++num,t=++num;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(S[i][j]){
for(int p=0;p<=k;p++){
add(id[i][j][p],id[i][j][k],1,a);
}
for(int p=0;p<4;p++){
int x=i+dx[p],y=j+dy[p];
if(x<1||y<1||x>n||y>n)continue;
add(id[i][j][k],id[x][y][k-1],1,b*(p&1));//分层
}
}
else {
add(id[i][j][0],id[i][j][k],1,a+c);
for(int p=k;p;--p){
for(int q=0;q<4;q++){
int x=i+dx[q],y=j+dy[q];
if(x<1||y<1||x>n||y>n)continue;
add(id[i][j][p],id[x][y][p-1],1,b*(q&1));
}
}
}
}
}
add(s,id[1][1][k],1,0);
for(int i=0;i<=k;i++)add(id[n][n][i],t,1,0);
zkw(1);
return 0;
}
网络流 题—— 总括篇
- 网络流解决最大/小费用问题,要么最小割最大流,要么最小费用流
最小费用流的前提是最大流,所以在有一些点可以不取换最优代价的时候,是不可行的。
当每个点都只能取一次的时候,可以考虑费用化为容量,求最小割/最大流。
- 最大权闭合子图模型
- 将一个点的不同状态拆分,化为左右部,分别求解
- 部分计算,合并答案
- 正难则反,拆分——>合并
- 拆路径的过程倒过来,变为合并路径
- 网络流的方案输出一般体现在满流边和残量网络上,当然也可以像动态规划一样记录上一个点
- 区间操作问题,我们仍然可以将其抽象为(亦或者说微观化?)序列元素操作问题
- 在流网络中,加边的后果就是导致分流
- 化整为零,化零为整
- 在数轴上,我们选择扩域,将每个点扩两个域,中间部分用来表示该点,类似于拆点连边
- 转化新旧两个状态,等价于给出旧的,收回新的,借助其他点转化,而非直接转化。
- 在网络流题中,对于同一物品的不同状态,不同决策,一般可以分为两种处理办法
- 拆点,将不同状态拆开,并进行不同限制 ,用点作为决策主体
- 建立“重边”,用边来体现不同决策
- 流量恒为1的网络流问题,等价于最短路问题
- 一个流的意义是一条路径,一条路径可以被赋予多重意义。
- 网络流的转移,有些类似于动态规划。对于动态规划的不同状态点连边,也可以化为网络流问题。(多个终态。若只有一个终态则可以简化为最短路问题)
- 点不可重复严格强于边不可重复
- 对于走地图问题,流量一般表示为走过的次数(容量为次数上限),而费用一般表示为收益/代价
- 用动态规划的思想去刻画状态
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!