【2019 NWERC B】Balanced Cut 题解

题目大意

  给定一棵 n n n 个点的 AVL 树(点权恰好为 1 1 1 n n n),你需要选择其中的 k k k 个点,满足:

  1. 如果要选一个点,那么它的祖先也必须选。也就是选出来的 k k k 个点会组成一棵新的树。
  2. 这棵新的树也必须是 AVL 树。

  (放个传送门里面有图片可以看看例子

  每种选法可以表示为一个长度为 n n n 的 01 串(表示每个点选或不选),你需要求出字典序最大的方案。

   1 ≤ k ≤ n ≤ 5 × 1 0 5 1 \leq k \leq n \leq 5 \times 10^5 1kn5×105
  4s,256MB

\\
\\
\\

题解

  做这种全服个位数通过的题其实挺有快感的

  首先大的框架肯定是贪心,从小到大 check 每个点能不能选。

  check 的依据就是两点:会不会跟已选的点冲突,会不会必需结点数目超过 k k k 的限制。AVL 树的一个性质是,树高是 O ( log ⁡ n ) O(\log n) O(logn) 的。基于这个我们可以设计出一个粗略的 check 的方法:
  ( l s o n , r s o n lson,rson lson,rson 表示一个结点的左右儿子)
  设要 check s s s 这个点,从 s s s 开始不断往父亲跳,维护当前子树最少要选的点数 n o w n u m nownum nownum,以及所选的层数 n o w d e e p nowdeep nowdeep。假设当前点是 x x x,父亲为 y y y
  若 x x x y y y 的右儿子,则 y y y 的左儿子肯定已经全部考虑过了,定死了不能变了,所以只需判断这俩兄弟的层数差是否在 1 1 1 以内即可,然后更新 n o w d e e p nowdeep nowdeep n o w n u m nownum nownum
  若 x x x y y y 的左儿子,则 y y y 的右儿子肯定全都没考虑,我们预处理一个 dp 数组 f i , j f_{i,j} fi,j 表示以 i i i 为根的子树选 j j j 层的最少花费,那么这时只需要使 n o w n u m nownum nownum 加上 f r s o n y , n o w d e e p − 1 f_{rson_y,nowdeep-1} frsony,nowdeep1 即可。
  最后判断 n o w n u m ≤ k nownum \leq k nownumk 即是合法。

  会有一些问题:当 x x x y y y 左儿子时, y y y 的右儿子可能曾经被 x x x 子树里的其他结点提出过更大的层数要求,记为 t a g r s o n y tag_{rson_y} tagrsony,是的这其实就是一个不断打 tag 的过程,所以实际上右儿子要选的层数应该是 f r s o n y , max ⁡ { n o w d e e p − 1 , t a g r s o n y } f_{rson_y,\max\{nowdeep-1,tag_{rson_y}\}} frsony,max{nowdeep1,tagrsony}
  然后,当 check 一个点成功时,它必须沿着祖先链上去给所有右兄弟更新 tag。

  这时又会有新的问题:当前结点 x x x 本身可能会有一个 t a g x tag_x tagx 的任务,然而我们为了使点数最少我们一直都只选必需的点,这导致 n o w d e e p nowdeep nowdeep 可能完不成 t a g x tag_x tagx 这个任务,我们就需要计算现在 n o w d e e p nowdeep nowdeep 层补到 t a g x tag_x tagx 层需要额外付出多少代价。
  记 h i ( i ≥ n o w d e e p ) h_i (i \ge nowdeep) hi(inowdeep) 表示当前子树从选 n o w d e e p nowdeep nowdeep 层追加到选 i i i 层需要额外补多少代价。这个东西也很好维护,当 n o w d e e p nowdeep nowdeep 变大成 n o w d e e p ′ nowdeep' nowdeep 时,所有的 h i − = h n o w d e e p ′ h_i-=h_{nowdeep'} hi=hnowdeep;当往上跳遇到右兄弟时,再维护 h i = min ⁡ { h i − 1 + f r s o n , i − f r s o n , t ,   h i + f r s o n , i − 1 − f r s o n , t } h_i=\min\{h_{i-1}+f_{rson,i}-f_{rson,t},~h_i+f_{rson,i-1}-f_{rson,t}\} hi=min{hi1+frson,ifrson,t, hi+frson,i1frson,t}。(其中 t = max ⁡ { n o w d e e p − 1 , t a g r s o n } t=\max\{nowdeep-1,tag_{rson}\} t=max{nowdeep1,tagrson},因为我们维护的是额外补的差价,而 f r s o n , t f_{rson,t} frson,t 是已经付出的)

  时间复杂度:每个点往上跳是 O ( log ⁡ n ) O(\log n) O(logn) 步的,每跳一次维护一下 h h h 数组又是 O ( log ⁡ n ) O(\log n) O(logn) 的,因此总的是 O ( n log ⁡ 2 n ) O(n \log^2 n) O(nlog2n)。但它跑得飞快,甚至这题其他人写的像是 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的(我都没看懂)用时是我的若干倍。。

  (另外有些小技巧,比如如果一个点它的(左)子树中有人确定要选了,那么它作为祖先就一定要选,就不用 check 了,这样的话任何要 check 的点都必然是左子树不选的,于是各变量的初值也方便多了。。

代码

#include<bits/stdc++.h>
#define fo(i,a,b) for(int i=a;i<=b;i++)
#define fd(i,a,b) for(int i=a;i>=b;i--)
using namespace std;

typedef long long LL;

const int maxn=5e5+5, MX=30;
const int inf=522133279;

int n,k,root,son[maxn][2],p[maxn];

int f[maxn][MX+2],nmax[maxn],deep[maxn];
void dfs_dp(int k)
{
	nmax[k]=k;
	int l=son[k][0], r=son[k][1];
	if (l)
	{
		deep[l]=deep[k]+1;
		dfs_dp(l);
	}
	if (r)
	{
		deep[r]=deep[k]+1;
		dfs_dp(r);
		nmax[k]=max(nmax[k],nmax[r]);
	}
	f[k][1]=1;
	fo(j,2,MX) f[k][j]=min(f[k][j],min(f[l][j-1]+f[r][j-2],f[l][j-2]+f[r][j-1])+1);
}

int tag[maxn],maxdeep[maxn],num[maxn],h[MX+2];
void Make_tag(int st)
{
	for(int x=st; x!=-1; x=p[x])
	{
		maxdeep[x]=max(maxdeep[x],deep[st]);
		num[x]++;
		int y=p[x];
		if (y!=-1 && son[y][0]==x) tag[son[y][1]]=max(tag[son[y][1]],maxdeep[x]-1-deep[y]);
	}
}
bool check(int x)
{
	if (num[x]) return 1;
	int nowdeep=0, nownum=0;
	memset(h,31,sizeof(h));
	h[0]=f[son[x][1]][0], h[1]=f[son[x][1]][1];
	for(; x!=-1; x=p[x])
	{
		int y=p[x];
		nownum++;
		if (nowdeep+1<tag[x])
		{
			nownum+=h[nowdeep=tag[x]-1];
			if (nownum>=inf) return 0;
			fd(i,MX,nowdeep) if (h[i]<inf) h[i]-=h[nowdeep];
		}
		nowdeep++;
		fd(i,MX,nowdeep) h[i]=h[i-1];
		if (y!=-1 && son[y][0]==x && son[y][1])
		{
			int r=son[y][1], t=max(tag[r],nowdeep-1);
			if (f[r][t]>=inf) return 0;
			nownum+=f[r][t];
			fd(i,MX,nowdeep+1)
			{
				h[i]=min(h[i-1]+f[r][i]-f[r][t],h[i]+f[r][i-1]-f[r][t]);
				if (h[i]>n) h[i]=inf;
			}
			h[nowdeep]=0;
		} else if (y!=-1 && son[y][1]==x && son[y][0])
		{
			int l=son[y][0];
			if (nowdeep-(maxdeep[l]-deep[y])>1) return 0;
			nowdeep=max(nowdeep,maxdeep[l]-deep[y]);
			h[nowdeep]=0;
			nownum+=num[son[y][0]];
		}
	}
	return nownum<=k;
}

bool ans[maxn];
int main()
{
	scanf("%d %d",&n,&k);
	fo(i,1,n)
	{
		scanf("%d",&p[i]);
		if (p[i]==-1) root=i; else son[p[i]][(i>p[i])]=i;
	}
	
	memset(f,31,sizeof(f));
	fo(i,0,n) f[i][0]=0;
	deep[root]=1;
	dfs_dp(root);
	
	fo(i,1,n) if (check(i))
	{
		ans[i]=1;
		Make_tag(i);
	}
	
	fo(i,1,n) putchar(ans[i] ?'1' :'0');
	puts("");
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值