联合省选D2T1 迷宫守卫 题解
没有参加省选 qwq。
简要题意:给定一棵满二叉树,共有 个叶子节点,每个叶子节点有一个值 ,保证所有叶子节点的值构成一个 的排列。每个非叶子节点有一个花费 ,可以花费 的价值操作当前节点,使得遍历到当前点时只可以先访问左子树,再访问右子树。否则可以任意选择一棵子树优先遍历,再遍历另一棵子树。
按照叶子节点遍历的先后可以得到 的一个排列。
现在有两人在树上博弈,甲希望字典序越大,并有 元可以随机操作一些节点,乙知道甲的操作,并希望字典序越小。
求最终的排列。
Solution
看到字典序最小,有一个很通用的套路,就是保证第一位尽量小,因为无论如何后面多小都没有用。
我们先考虑怎么做到排列第一位最小。
站在先用最暴力的方法,考虑枚举法。我们假设已经得到第 位大小是 。考虑为什么 能成为第一位,明显地,因为甲把权值小于等于 的节点全部堵死了,去不了。这时候,是最优节点。
换个思路考虑,我们把所有叶子结点权值 小于等于 的都打上一个标记,表示这个点不能到达。然后计算出堵住这些节点的花费最小值 是否大于钱数 。如果大于,说明堵不住,这时候 是可行的。
我们考虑从小到大枚举这些节点,枚举到第一个可行的就是答案,容易证明从小到大枚举花费一定是递增的,因为要保证堵住原先节点的同时堵住新节点。
现在考虑如何求得堵住这些点的最小花费,不难发现,对于任意点 ,有两种方法:
- 同时堵住 的左子树和右子树
- 堵住 的左子树的同时给 打上标记
这个问题可以使用 dp 。定义 表示堵住 子树的最小花费,如果一个叶子节点 是要求被堵住的,我们设 即可 那么容易求得
同时,我们每次对叶子节点 打上标记 ,只有 到根的路径改变了状态,我们只需改变这些状态即可。
根据上面这些,我们在 的时间内确定了第一个数。
那么怎么求第二个数呢?
其实,根据上面的推理,我们已经确定了第一个数在根节点的哪棵子树内,这时,我们惊奇的发现,这个子树内的推理和根节点的推理是完全一致的!直接递归处理即可!
不对,好像忽略了一个点,还没有确定给这棵子树多少费用。
假设第一个数在左子树,那么前半部分的排列一定在左子树内,我们只需要把保证合法的最少费用抠出来给右子树,其它都给左子树即可。
怎么保证合法呢?不就是右子树堵住 的前提下的最小花费嘛。这个我们在求 时就已经预处理好了,但是是更新 点的上一次。同时还有一种情况就是取了当前节点,直接先不往右子树走了。我们先把这些最小花费从 中扣出来,遍历右子树时再加回去不就保证不会透支了嘛!qwq
我们把左子树遍历完后,加上之前抠出来的预算,就得到了右节点的钱 。这个时候再次考虑要不要取当前节点,如果右节点堵住 的钱比 小,那么取当前节点一定不优。因为保证条件的情况下取了当前节点就是浪费钱,可以理解为在够钱时取不取当前节点遇到的是相同的子问题。
但是如果堵住的钱比 大,那么这个点就必须取了,因为不取就不满足堵住 的条件了。
同时第一个数在右子树同理,讨论左子树即可。但是就没有取当前节点的情况了。走右子树当前节点一定是不取的。
对于子树,我们通过同样方式讨论即可。最后到达叶子节点输出即可。
看起来这个做法太暴力了,但仔细想一想却不是这样。
暴力取出每个点是 的,考虑每个点取到的条件是枚举到了当前节点的祖先,而满二叉树祖先个数和树高数相同的。
暴力求 dp 是 的。考虑每个点都会跳到祖先一次,所以每个点跳跃次数就是祖先深度和。
在从小到大枚举目前最优值时,直接把所有数扔出来 sort 一下就行了,比较还有一个时间复杂度比它大的 dp。
总体时间复杂度是 的。一般不特意卡跑不满。
code
#include<bits/stdc++.h>
#define N 152005
#define ll long long
#define inf 1e18
#define ls(x) x<<1
#define rs(x) x<<1|1
#define fa(x) x/2
using namespace std;
int g;
int n,m,p[N],pos[N];
ll k,w[N],f[N];
int L[N],R[N];
int stk[N],top;
void getf(int now,int topf)
{
f[now]=inf;
now=fa(now);
while(now^topf)
{
f[now]=f[ls(now)]+min(f[rs(now)],w[now]);
now=fa(now);
}
}
void clr(int now,int topf)
{
while(now) f[now]=0,now=fa(now);
}
void calc(int now)
{
if(now>=m)
{
printf("%d ",p[now-m+1]);
return;
}
top=0;
for(int i=L[now];i<=R[now];i++) stk[++top]=p[i-m+1];//暴力求得树内每个数
sort(stk+1,stk+1+top);
int ps=0,wson=0;
ll lv=0,rv=0;
for(int i=1;i<=top;i++)
{
getf(m+pos[stk[i]]-1,fa(now));//求得 dp 数组
if(f[now]>k)
{
ps=i;
break;
}
lv=f[ls(now)],rv=f[rs(now)];//在合法时的左右子树费用
}
for(int i=1;i<=ps;i++)
clr(m+pos[stk[i]]-1,fa(now));//清空 dp 数组
if(pos[stk[ps]]+m-1<=R[ls(now)]) wson=0; //判断去左/右子树
else wson=1;
if(!wson)//要跳左子树
{
k-=min(w[now],rv);
calc(ls(now));
k+=min(w[now],rv);
if(k<rv) k-=w[now];
calc(rs(now));
}
else//要跳右子树
{
k-=lv;
calc(rs(now));
k+=lv;
calc(ls(now));
}
}
int main()
{
scanf("%d",&g);
while(g--)
{
scanf("%d%lld",&n,&k);
m=(1ll<<n);
for(int i=1;i<=m-1;i++)
scanf("%lld",&w[i]),f[i]=0;
for(int i=1;i<=m;i++)
scanf("%d",&p[i]),pos[p[i]]=i,f[i+m-1]=0,L[i+m-1]=R[i+m-1]=i+m-1;
for(int i=m-1;i>=1;i--) L[i]=L[ls(i)],R[i]=R[rs(i)];
calc(1);
printf("\n");
}
return 0;
}
一开始在跳节点时思考错了,认为在当前节点和堵住右子树之间选择小的减去,想了三四个小时都不明白哪错了。
一开始没想到 dp 数组在线求,写的是主席树爆空间,喜提 RE ,时间复杂度还是同阶的……不在满二叉树上跳祖先而主席树上跳,我可真是太聪明了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)