联合省选D2T1 迷宫守卫 题解

没有参加省选 qwq。

简要题意:给定一棵满二叉树,共有 \(2^n\) 个叶子节点,每个叶子节点有一个值 \(q_x\) ,保证所有叶子节点的值构成一个 \(1\sim 2^n\) 的排列。每个非叶子节点有一个花费 \(w_x\),可以花费 \(w_x\) 的价值操作当前节点,使得遍历到当前点时只可以先访问左子树,再访问右子树。否则可以任意选择一棵子树优先遍历,再遍历另一棵子树。

按照叶子节点遍历的先后可以得到 \(q\) 的一个排列。

现在有两人在树上博弈,甲希望字典序越大,并有 \(k\) 元可以随机操作一些节点,乙知道甲的操作,并希望字典序越小。

求最终的排列。

Solution

看到字典序最小,有一个很通用的套路,就是保证第一位尽量小,因为无论如何后面多小都没有用。

我们先考虑怎么做到排列第一位最小。

站在先用最暴力的方法,考虑枚举法。我们假设已经得到第 \(1\) 位大小是 \(x\) 。考虑为什么 \(x\) 能成为第一位,明显地,因为甲把权值小于等于 \(x\) 的节点全部堵死了,去不了。这时候,\(x\)是最优节点。

换个思路考虑,我们把所有叶子结点权值 \(q_x\) 小于等于 \(x\) 的都打上一个标记,表示这个点不能到达。然后计算出堵住这些节点的花费最小值 \(v\) 是否大于钱数 \(k\)。如果大于,说明堵不住,这时候 \(x\) 是可行的。

我们考虑从小到大枚举这些节点,枚举到第一个可行的就是答案,容易证明从小到大枚举花费一定是递增的,因为要保证堵住原先节点的同时堵住新节点。

现在考虑如何求得堵住这些点的最小花费,不难发现,对于任意点 \(u\) ,有两种方法:

  • 同时堵住 \(u\) 的左子树和右子树
  • 堵住 \(u\) 的左子树的同时给 \(x\) 打上标记

这个问题可以使用 dp 。定义 \(f_u\) 表示堵住 \(u\) 子树的最小花费,如果一个叶子节点 \(x\) 是要求被堵住的,我们设 \(f_x=inf\) 即可 那么容易求得 \(f_u=\min(f_{lson}+f_{rson},f_{lson}+w_u)\)

同时,我们每次对叶子节点 \(x\) 打上标记 ,只有 \(x\) 到根的路径改变了状态,我们只需改变这些状态即可。

根据上面这些,我们在 \(O(2^n)\) 的时间内确定了第一个数。

那么怎么求第二个数呢?

其实,根据上面的推理,我们已经确定了第一个数在根节点的哪棵子树内,这时,我们惊奇的发现,这个子树内的推理和根节点的推理是完全一致的!直接递归处理即可!

不对,好像忽略了一个点,还没有确定给这棵子树多少费用。

假设第一个数在左子树,那么前半部分的排列一定在左子树内,我们只需要把保证合法的最少费用抠出来给右子树,其它都给左子树即可。

怎么保证合法呢?不就是右子树堵住 \(1\sim x-1\) 的前提下的最小花费嘛。这个我们在求 \(f_u\) 时就已经预处理好了,但是是更新 \(x\) 点的上一次。同时还有一种情况就是取了当前节点,直接先不往右子树走了。我们先把这些最小花费从 \(k\) 中扣出来,遍历右子树时再加回去不就保证不会透支了嘛!qwq

我们把左子树遍历完后,加上之前抠出来的预算,就得到了右节点的钱 \(k_r\)。这个时候再次考虑要不要取当前节点,如果右节点堵住 \(1\sim x-1\) 的钱比 \(k_r\) 小,那么取当前节点一定不优。因为保证条件的情况下取了当前节点就是浪费钱,可以理解为在够钱时取不取当前节点遇到的是相同的子问题。

但是如果堵住的钱比 \(k_r\) 大,那么这个点就必须取了,因为不取就不满足堵住 \(1\to x-1\) 的条件了。

同时第一个数在右子树同理,讨论左子树即可。但是就没有取当前节点的情况了。走右子树当前节点一定是不取的。

对于子树,我们通过同样方式讨论即可。最后到达叶子节点输出即可。

看起来这个做法太暴力了,但仔细想一想却不是这样。

暴力取出每个点是 \(O(n2^n)\) 的,考虑每个点取到的条件是枚举到了当前节点的祖先,而满二叉树祖先个数和树高数相同的。

暴力求 dp 是 \(O(n^22^n)\) 的。考虑每个点都会跳到祖先一次,所以每个点跳跃次数就是祖先深度和。

在从小到大枚举目前最优值时,直接把所有数扔出来 sort 一下就行了,比较还有一个时间复杂度比它大的 dp。

总体时间复杂度是 \(O(n^22^n)\) 的。一般不特意卡跑不满。

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 ,时间复杂度还是同阶的……不在满二叉树上跳祖先而主席树上跳,我可真是太聪明了。

posted @ 2024-03-05 22:10  g1ove  阅读(44)  评论(1编辑  收藏  举报