迷宫守卫 题解

给个题目链接:迷宫守卫

下面直接开始讲了。

发现一个事情,省选的题已经不怎么考板子难度很高的题了,现在考的都是思维难度非常高的题。

首先,我们考虑字典序的性质,如果第一位劣,那么后面无论多优都没用,所以我们要优先满足靠前的位置。

于是我们考虑使用二分来找出第一个数,后面以此类推。每次对于每个比当前二分答案更小的叶子要至少选择一个点让他拐到其他路上,我们可以使用树形 \(dp\) 来求出每一次代价。具体地,考虑左右子树,左子树无法改变优先级,而如果右子树的花费还不如直接在这个点强制左拐,那就直接左拐,否则在右子树内操作,感觉不是很好理解,所以放一下代码:

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]);
	//右边花费很大,那就在这个点直接强制向左
	//然后两个花费加一块 
}

然后说一下 \(dfs\) 函数,这个非常复杂,所以一部分一部分讲。

先考虑如果当前点是叶子,如果发现这个点的值小于我们要求的最小值,则我们需要操作一下,然后想一下为什么操作是有用的,如果是左子节点不满足要求那不就炸了吗?

再想一下我们的要求,我们能走到这个点的话,如果这个点不能满足要求还是个左儿子,那我们要么返回了不合法,要么就已经在上面往其他路拐弯了,所以这个叶子要么最后一步能拐弯,要么不能拐弯但满足要求。然后看一下代码:

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);//按照每个点的值排序 

二分有点难理解。我们考虑在排序好的数组中二分,这里我们的 \(dfs\) 也记录了一个当前要求的最小数,所以我们二分时判断的数不能小于记录的数,取个 \(\operatorname{max}\) 就好了。

接下来考虑如果二分的值比 \(dfs\) 记录的值更大即更严格,我们就不用管,因为满足了二分要求。但是,如果记录值更大,则我们可以考虑只满足二分值,但是强制要求先向左子树走,最后的花费就是这俩玩意的最小值。

然后就是正常二分,判断花费超了以没调整边界,并记录最小花费和下一个点的位置。代码:

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;
}

然后我们因为是通过 \(dfs\) 走这棵树,故在他走完子树之后下一站肯定是他兄弟。但是我们又发现,为了满足当前子树的要求,兄弟子树未必取到最优,但是当前子树一定最优。所以我们考虑先减去当前子树的花费,因为这个花费必然要付。

然后把当前方案下兄弟子树的花费加回来,这里加的东西事实上就是对两个东西取个最小值:

  • 如果当前子树是左儿子,证明可能父亲节点采取强制措施,花费显然,否则是极大值,因为不可能强制向右。

  • 另外一个就是兄弟子树的第一个数优于当前子树的最小花费。

最后我们就一直这样走到根即可,代码:

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;
}

好了,\(2024\) 省选的迷宫守卫就讲完了,感觉难度挺高的,而且代码细节挺多的,所以建议先理解一遍,然后自己尝试补全一下细节。

posted @ 2024-07-15 21:40  zxh923  阅读(3)  评论(0编辑  收藏  举报