迷宫守卫 题解
给个题目链接:迷宫守卫。
下面直接开始讲了。
发现一个事情,省选的题已经不怎么考板子难度很高的题了,现在考的都是思维难度非常高的题。
首先,我们考虑字典序的性质,如果第一位劣,那么后面无论多优都没用,所以我们要优先满足靠前的位置。
于是我们考虑使用二分来找出第一个数,后面以此类推。每次对于每个比当前二分答案更小的叶子要至少选择一个点让他拐到其他路上,我们可以使用树形
int get_cost(int u,int lim){
if(u>=leaf)return a[u]<lim?inf:0;//如果比要求最小值还小,不合法
int cl=get_cost(u<<1,lim);
int cr=get_cost(u<<1|1,lim);
return cl+min(cr,a[u]);
//右边花费很大,那就在这个点直接强制向左
//然后两个花费加一块
}
然后说一下
先考虑如果当前点是叶子,如果发现这个点的值小于我们要求的最小值,则我们需要操作一下,然后想一下为什么操作是有用的,如果是左子节点不满足要求那不就炸了吗?
再想一下我们的要求,我们能走到这个点的话,如果这个点不能满足要求还是个左儿子,那我们要么返回了不合法,要么就已经在上面往其他路拐弯了,所以这个叶子要么最后一步能拐弯,要么不能拐弯但满足要求。然后看一下代码:
if(u>=leaf){
if(a[u]<lim)m-=a[u>>1];//需要提前拐弯
stk[++top]=a[u];
return;
}
接着说下面的部分。我们考虑维护当前节点为根的子树的编号最小的叶子(注意不是权值,编号是指的从左向右数的次序)和编号最大的叶子,可以发现,叶子是连续的(废话),然后我们记录这些叶子的全职和编号。
接下来我们按照权值给他们排个序,开始二分。代码:
int l=u,r=u,len=0;
while(l<leaf)l=l<<1;
while(r<leaf)r=r<<1|1;
//找出这个子树的最左叶子和最右叶子,形成一段区间
for(int i=l;i<=r;i++){
p[++len]={a[i],i};
}
sort(p+1,p+len+1);//按照每个点的值排序
二分有点难理解。我们考虑在排序好的数组中二分,这里我们的
接下来考虑如果二分的值比
然后就是正常二分,判断花费超了以没调整边界,并记录最小花费和下一个点的位置。代码:
l=1,r=len;
int res=0,pos;
while(l<=r){//看最大能放在第一个数的是哪个,运用二分找出
int mid=l+r>>1;
int cost=get_cost(u,max(p[mid].x,lim));
if(u&1&&u>1&&p[mid].x<lim)cost=min(cost,get_cost(u,p[mid].x)+a[u>>1]);
//是右儿子并且当前点不符合要求,所以可以强制左拐,然后再加上这个点需求的花费
if(cost<=m){
l=mid+1;
pos=p[mid].y;
res=cost;//花费合法记录花费和位置
}
else r=mid-1;
}
然后我们因为是通过
然后把当前方案下兄弟子树的花费加回来,这里加的东西事实上就是对两个东西取个最小值:
-
如果当前子树是左儿子,证明可能父亲节点采取强制措施,花费显然,否则是极大值,因为不可能强制向右。
-
另外一个就是兄弟子树的第一个数优于当前子树的最小花费。
最后我们就一直这样走到根即可,代码:
m-=res;//扣掉花费
stk[++top]=lim=a[pos];//走到这个点
do{
int now=(pos&1?inf:a[pos>>1]);//如果是右节点就不可能强制去,记一下已经用的花费
m+=min(now,get_cost(pos^1,lim));//回溯时把这些花费加回去
dfs(pos^1,lim);
pos>>=1;//走向父亲
}while(pos!=u);
剩下的部分没啥好说的,看一下代码应该就理解了:
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define x first
#define y second
#define leaf (1<<n)
#define N 200005
#define inf 2e18
using namespace std;
int T,n,m,a[N],stk[N],top;
pii p[N];
int get_cost(int u,int lim){
if(u>=leaf)return a[u]<lim?inf:0;//如果比要求最小值还小,不合法
int cl=get_cost(u<<1,lim);
int cr=get_cost(u<<1|1,lim);
return cl+min(cr,a[u]);
//右边花费很大,那就在这个点直接强制向左
//然后两个花费加一块
}
void dfs(int u,int lim){//lim是要求的最小值
if(u>=leaf){
if(a[u]<lim)m-=a[u>>1];//需要提前拐弯
stk[++top]=a[u];
return;
}
int l=u,r=u,len=0;
while(l<leaf)l=l<<1;
while(r<leaf)r=r<<1|1;
//找出这个子树的最左叶子和最右叶子,形成一段区间
for(int i=l;i<=r;i++){
p[++len]={a[i],i};
}
sort(p+1,p+len+1);//按照每个点的值排序
l=1,r=len;
int res=0,pos;
while(l<=r){//看最大能放在第一个数的是哪个,运用二分找出
int mid=l+r>>1;
int cost=get_cost(u,max(p[mid].x,lim));
if(u&1&&u>1&&p[mid].x<lim)cost=min(cost,get_cost(u,p[mid].x)+a[u>>1]);
//是右儿子并且当前点不符合要求,所以可以强制左拐,然后再加上这个点需求的花费
if(cost<=m){
l=mid+1;
pos=p[mid].y;
res=cost;//花费合法记录花费和位置
}
else r=mid-1;
}
m-=res;//扣掉花费
stk[++top]=lim=a[pos];//走到这个点
do{
int now=(pos&1?inf:a[pos>>1]);//如果是右节点就不可能强制去,记一下已经用的花费
m+=min(now,get_cost(pos^1,lim));//回溯时把这些花费加回去
dfs(pos^1,lim);
pos>>=1;//走向父亲
}while(pos!=u);
}
void solve(){
top=0;
cin>>n>>m;
for(int i=1;i<leaf*2;i++){
cin>>a[i];
}
dfs(1,0);
for(int i=1;i<=top;i++){
cout<<stk[i]<<' ';
}
cout<<'\n';
}
signed main(){
cin>>T;
while(T--){
solve();
}
return 0;
}
好了,
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】