题目
题目背景
我身旁的大佬:“你知道肿么随机一个
a
v
i
\tt avi
avi 出来吗?”
题目描述
大家都知道
A
V
L
\rm AVL
AVL 是满足这种性质的二叉搜索树:左右子树的高度之差
⩽
1
\leqslant 1
⩽1 。高度定义为子树的根节点到子树内任意一个点的最远距离;空子树高度为
−
1
-1
−1 。
现在给出一棵 A V L \rm AVL AVL 树,如果只保留其中的 k k k 个节点(如果 x x x 的子节点被保留,则 x x x 必须被保留)仍然是一个 A V L \rm AVL AVL 树,可以写出这 k k k 个节点的权值排序后形成的整数序列。请求出字典序最小的序列。
数据范围与提示
0
⩽
k
<
n
⩽
5
×
1
0
5
0\leqslant k<n\leqslant 5\times 10^5
0⩽k<n⩽5×105 。提示:树的高度是
O
(
log
n
)
\mathcal O(\log n)
O(logn) 的。
思路
目前我们已经有了 O ( n log 2 n ) \mathcal O(n\log^2n) O(nlog2n) 的做法(见上)、 O ( n log n ) \mathcal O(n\log n) O(nlogn) 的做法,和一个 O ( n ) \mathcal O(n) O(n) 但是有常数的做法。
大体思路是类似于 I I I D X \rm IIIDX IIIDX 的,也就是贪心选择,然后 预订 一些点,来满足题目要求。在这道题里,我们选择了一个点,则需要一路向上:如果自己是左儿子,那么右儿子高度至少需要为 h − 1 h-1 h−1,要提前打好标记,预订好这些节点。
点名表扬 我们永远滴神 R a i n y b u n n y \rm \color{black}{R}\color{red}{ainybunny} Rainybunny,指出这里不需要判断自己是右儿子时可能过深——当字典序最小时,肯定是优先选左子树内的点,所以右子树不会有比左子树更深的机会。
高度至少为
h
h
h 时,点的数量是多少?其实就是
f
h
=
f
h
−
1
+
f
h
−
2
+
1
f_h=f_{h-1}+f_{h-2}+1
fh=fh−1+fh−2+1
跟树的形态无关。这可以通过归纳法很轻易地说明。类似地,我们还可以说明 f h f_h fh 选择的点可以是 f h − 1 f_{h-1} fh−1 的超集。
为了方便,我们先转化成 先序遍历 检查。正确性是显然的——没有当前点,谈何子树啊?所以先检查当前点也没问题。
如果就按照上面的 t a g tag tag 方法做,已经是 O ( n log n ) \mathcal O(n\log n) O(nlogn) 了,毕竟树高是 O ( log n ) \mathcal O(\log n) O(logn) 的。但是打上去的 t a g tag tag 无非就是当前子树内的 h e i g h t height height 嘛!一旦加入某个 d e p t h depth depth 的点,就会让 h e i g h t height height 增大,进而引起 t a g tag tag 变化。而且先序遍历保证了我们 已经走过了祖先节点,可以提前维护信息。
考虑维护 v [ d e p ] v[dep] v[dep] 为,加入一个深度为 d e p dep dep 的点时,哪些 t a g tag tag 会变化。那么我们可以 O ( 1 ) \mathcal O(1) O(1) 的检查当前点是否可以加入;如果可以,那么 v [ d e p ] v[dep] v[dep] 肯定会变为 v [ d e p + 1 ] v[dep+1] v[dep+1],修改一下即可。而 t a g tag tag 可以直接用 d f s \rm dfs dfs 回溯的留下的树的高度 h e i g h t height height 减 1 1 1 得到。
在往下递归时,首先要下放 t a g tag tag 。由于 f h − 1 f_{h-1} fh−1 选择的点是 f h f_h fh 选择的点的子集,优先在左子树内选择 f t a g − 1 f_{tag-1} ftag−1 肯定更好,对应的就是右子树至少要有 t a g − 2 tag-2 tag−2 的高度;除非左子树高度不够,只配得上 t a g − 2 tag-2 tag−2 。
往左子树递归时,当前的右子树就成为了 “到根节点路径上自己是左儿子” 需要考虑的一个右儿子。加入
v
v
v 数组即可,具体是多少需要自己想一想。毕竟代码因人而异嘛。回溯时记得删掉。
但是有什么东西是可以先加在一起,然后整体推进一个状态吗?矩阵 呗。这个比较像斐波的 t r i c k \rm trick trick 。
所以时间复杂度就是 O ( n ) \mathcal O(n) O(n) 了,所谓的常数就是矩阵乘法。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long int_;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
inline int readint(){
int a = 0, c = getchar(), f = 1;
for(; '0'>c||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(int x){
if(x > 9) writeint(x/10);
putchar((x-x/10*10)^48);
}
inline void getMax(int &x,const int &y){
if(x < y) x = y;
}
struct Matrix{
const static int N = 3;
int a[N][N];
void clear(){ memset(a,0,N*N<<2); }
Matrix operator * (const Matrix &b) const {
Matrix c; c.clear();
rep(i,0,N-1) rep(j,0,N-1) if(a[i][j])
rep(k,0,N-1) c.a[i][k] += a[i][j]*b.a[j][k];
return c; // no module
}
void operator += (const Matrix &b){
rep(i,0,N-1) rep(j,0,N-1)
a[i][j] += b.a[i][j];
}
void operator -= (const Matrix &b){
rep(i,0,N-1) rep(j,0,N-1)
a[i][j] -= b.a[i][j];
}
};
const int MAXN = 500005;
int fa[MAXN], son[MAXN][2];
const int LOGN = 30;
Matrix fibo[LOGN], step;
int heit[MAXN];
void scan(int x){
rep(d,0,1) if(son[x][d]){
scan(son[x][d]);
getMax(heit[x],heit[son[x][d]]);
}
++ heit[x]; // itself
}
int tag[MAXN], cur, k;
Matrix v[LOGN]; bool ans[MAXN];
void solve(int x,int dep=0){
if(tag[x]){ // counted
ans[x] = true; // always
if(tag[x] != 1){
bool d = (heit[son[x][0]] < tag[x]-1);
tag[son[x][d]] = tag[x]-1;
tag[son[x][d^1]] = tag[x]-2;
}
}
else{ // check & modify
Matrix nxt = v[dep]*step;
int zxy = nxt.a[0][0]-v[dep].a[0][0];
if(zxy+cur+1 > k) // too many nodes
return void(heit[x] = 0);
ans[x] = true; cur += zxy+1;
v[dep+1] += nxt; // into next level
v[dep].clear(); // removed
}
if(son[x][0]){ // recurse LSON
if(son[x][1]){
const int &p = tag[son[x][1]];
v[dep+p+2] += fibo[p];
}
solve(son[x][0],dep+1);
if(son[x][1]){
int &p = tag[son[x][1]];
getMax(p,heit[son[x][0]]-1);
v[dep+p+2] -= fibo[p]; // remove
}
}
if(son[x][1]) solve(son[x][1],dep+1);
heit[x] = max(heit[son[x][0]],heit[son[x][1]])+1;
}
int main(){
int n = readint(), rt = 0;
k = readint(); // global boundary
step.a[0][1] = step.a[2][2] = 1;
rep(i,0,2) step.a[i][0] = 1;
fibo[0].a[0][2] = 1;
rep(i,1,LOGN-1) // pre-compute
fibo[i] = fibo[i-1]*step;
for(int i=1,fa; i<=n; ++i){
fa = readint();
if(!(~fa)) rt = i;
else son[fa][fa < i] = i;
}
scan(rt), solve(rt);
rep(i,1,n) putchar(ans[i]^48);
putchar('\n');
return 0;
}
后记
点名批评 我们永远滴神 R a i n y b u n n y \rm \color{black}{R}\color{red}{ainybunny} Rainybunny,非要先打 O ( n log n ) \mathcal O(n\log n) O(nlogn) 的代码。
点名表扬 我们永远滴神 R a i n y b u n n y \rm \color{black}{R}\color{red}{ainybunny} Rainybunny,帮助我理清了思路。以及下面这件事中,他发挥了重要作用。
点名批评 O n e I n D a r k \sf OneInDark OneInDark,本来准备先优化 O ( n log n ) \mathcal O(n\log n) O(nlogn) 的代码,被雨兔骂醒了 T A T TAT TAT