平衡树小结
引用一句百经: 在计算机科学中,平衡树能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。平衡树,概括来说是一个一般化的二叉查找树(binary search tree),可以拥有多于2个子节点。与自平衡二叉查找树不同,B树为系统大块数据的读写操作做了优化。B树减少定位记录时所经历的中间过程,从而加快存取速度。B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。
所以看一下二叉查找树的性质。
将序列 \(N\) 构造二叉查找树,则该树的中序遍历就是序列 \(N\)。
换言之,在二叉查找树 \(T\) 中,\(\forall p\in T,val_{p}>val_{lson_p},val_p<val_{rson_p}\)。如果序列中有重复元素,使用数组 \(cnt_i\) 表示第 \(i\) 个节点出现的数量。
这告诉我们:平衡树就是维护一个递增序列,在其上进行各种操作查询。
平衡树的复杂度为 \(\Theta(n\ logn)\),单词操作复杂度 \(\Theta(logn)\)。
Splay
关于 Splay:
别名伸展树。
struct TREE{
int fa,son[2];
int val,cnt,siz;
}tree[MAXN];
\(tree_i\) 数组为 Splay 的主体,所代表的是树上的一个节点,维护以下元素:
- \(fa_i:\) 节点 \(i\) 的父亲节点。
- \(son_{i,0/1}:\) 节点 \(i\) 的左右儿子节点。
- \(val_i:\) 当前节点所代表的权值。
- \(cnt_i:\) 当前权值个数
- \(siz_i:\) 当前节点所在子树大小。
现在说两个核心操作:\(rotate(),splay()\)
\(rotate()\),即旋转,分为左旋和右旋。
旋转的本质是将某个节点上移一个单位,且必须保证:
- 旋转后的 Splay 中序遍历不变。
- 旋转影响到的节点信息正确有效。
- 节点 \(root\) 指向旋转后的根节点。
设等待旋转至根节点的节点为 \(x\),其父节点为 \(y\),爷爷节点为 \(z\)。现在要求旋转节点 \(x\),即维护信息与中序遍历不变的情况下将 \(x\) 移至 \(y\) 的位置。
多次试验后发现规律:旋转节点 \(x\) 后,三点的父子关系:\(fa_x=z,fa_y=x,z=root\),左右儿子关系:令原先 \(x\) 为 \(y\) 的 \(S_a\),\(y\) 为 \(z\) 的 \(S_b\),则新关系下 \(x\) 为 \(z\) 的 \(S_b\),\(y\) 为 \(x\) 的 \(S_a \ ^\wedge 1\)。
旋转后子树大小 \(siz_i\) 会有变化,不过子树大小的获取方式是固定的:
旋转时重新分配节点父子关系,之后更新有变动的节点 \(x,y\) 即可。
节点 \(x\) 的儿子应分配给 \(y\) 且位置为原先 \(y,x\) 的父子关系取反,自己推一推就知道了。
注:此处的父子关系是指左/右儿子。
根据这个规律得到 \(rotate(x):\)旋转节点 \(x\)。
inline void update(int p){
tree[p].siz=tree[tree[p].son[0]].siz+tree[tree[p].son[1]].siz+tree[p].cnt;
}//重新统计当前节点子树信息。
inline void rotate(int p){
int f1=tree[p].fa;
int f2=tree[f1].fa;
int k=tree[f1].son[1]==p;//令son[0]代表左节点,son[1]为右节点。
//这样可以获取当前节点在父节点中的位置。
tree[f2].son[tree[f2].son[1]==f1]=p;
tree[p].fa=f2;//重新分配 x 为 z 的儿子,位置不变。
tree[f1].son[k]=tree[p].son[k^1];
tree[tree[p].son[k^1]].fa=f1;
tree[p].son[k^1]=f1;
tree[f1].fa=p;//重新分配 y 为 x 的儿子,位置取反。
update(f1),update(p);
}
你要问我如果不存在 \(z\) 节点怎么办,那说明 \(y\) 为根节点,\(z\) 为祖先节点 \(0\),还是一样的代码。
Splay 函数时基于 \(rotate(p)\) 实现的,格式:\(splay(p,king)\)。作用是旋转节点 \(p\) 至节点 \(king\) 的某个儿子,具体是哪个儿子就要看中序遍历了。
特别的,\(splay(p,0)\) 意为将节点 \(p\) 变为根节点。
我们考虑实现。
如果 \(king\) 离 \(p\) 很远,我们不妨把问题分割。还是从刚才的 \(x,y,z\) 入手。我们假设 \(king\) 节点就在 \(z\) 节点上方,于是子问题成了:如何将 \(x\) 移动至 \(z\) 的位置。
- 重复进行以下操作直到 \(x\) 的父亲就是 \(king\)
- 终止条件,如果 \(king\) 是 \(x\) 的爷爷节点即 \(z\),旋转 \(x\) 即可。
又令 \(y\) 为 \(z\) 的 \(S_a\) 子节点,\(x\) 为 \(y\) 的 \(S_b\) 节点。
- \(S_a \wedge S_b=1\),需连续旋转两次 \(x\)
- \(S_a \wedge S_b=0\),需先旋转 \(y\),再旋转 \(x\)
我们使用图片方便理解这个过程。
然后就可以看着写出代码。
inline void splay(int p,int king){
while(tree[p].fa!=king){
int f1=tree[p].fa,f2=tree[f1].fa;
if(f2!=king)(tree[f2].son[0]==f1)^(tree[f1].son[0]==p)?rotate(p):rotate(f1);//上文提到的处理方法。
rotate(p);//无论如何当前节点(x)是一定会旋转一次的。
}
if(!king)root=p;//特判上文提到的换根操作。
}
根据这两个操作写出 \(find(p)\) 操作:将节点 \(p\) 旋转到根节点。
Q:为什么要将 \(p\) 旋转到根节点?
A:我们回归二叉搜索树的性质,当前节点 \(p\) 的左儿子 \(lson_p\) 严格小于 \(p\),右儿子 \(rson_p\) 严格大于 \(p\),将节点 \(p\) 旋转到根节点本质是以 \(p=mid\) 开展二分查找。
\(find()\) 步骤:
- 令递归用节点 \(u=root\)
- 在当前 \(BT\) 的结构上二分查找 \(p\) 的位置。
- 找到之后将 \(p\) 旋转至根节点。
inline void find(int p){
int u=root;
if(!u)return;
while(tree[u].son[p>tree[u].val]&&p!=tree[u].val)u=tree[u].son[p>tree[u].val];
splay(u,0);
}
现在来看例题:需要实现以下操作:
- 插入一个元素 \(x\)
- 删除一个元素 \(x\)
- 查询元素 \(x\) 的排名
- 查询第 \(k\) 大元素
- 查询 \(x\) 的前驱,后继。
插入元素:
由于这是一颗二叉搜索树,所以直接从根节点向下查询 \(tree[u].val\),\(p>tree[u].val\) 就往右子树走,否则就往左子树走,如果遇见相等就地 \(++cnt_u\)
如果发现这是个新点,那么考虑使用动态开点的思想。就地赋予点编号 \(u\),权值 \(val\) 等。
inline void insert(int p){
int u=root,fa=0;
while(u&&tree[u].val!=p){
fa=u;
u=tree[u].son[p>tree[u].val];
}
if(u)++tree[u].cnt;
else{
u=++tot;
if(fa)tree[fa].son[p>tree[fa].val]=u;
tree[u].son[0]=tree[u].son[1]=0;
tree[tot].fa=fa;
tree[tot].val=p;
tree[tot].cnt=tree[tot].siz=1;
}
splay(u,0);
}
寻找前驱/后继:
出于二叉搜索树的优秀性质,我们把 \(p\) 旋转到根节点后,\(p\) 的前驱显然是它左儿子的最右儿子,后继就是它右儿子的最左儿子。
注意到本题中节点 \(p\) 可能是不存在的。这不重要,先把节点旋转到根,此时用 \(p\) 与根节点权值对比即可。
inline int nxt(int p,int f){//f是操作类型 0前驱,1后继
find(p);
int u=root;
if((tree[u].val>p&&f)||(tree[u].val<p&&!f))return u;
u=tree[u].son[f];//找到对应儿子
while(tree[u].son[f^1])u=tree[u].son[f^1];
//左儿子的最右儿子与右儿子的最左儿子
return u;
}
删除节点:
我们需要用到当前节点 \(p\) 的前驱 $nxt_0 $ 后继 \(nxt_1\)。
把 \(nxt_0\) 旋转为根节点,再把 \(nxt_1\) 旋转成 \(nxt_0\) 的儿子。后继是大于前驱的,因此一定是它的右儿子。我们可以把 \(p\) 理解为 \(nxt_1\) 的前驱,然而此时 \(nxt_0\) 已经是根节点了,所以 \(nxt_1\) 的左子树有且仅有一个节点 \(p\)。
对找到的 \(cnt_p--\) 即可,把他旋转成根节点方便操作。特别地,\(cnt_p=0\) 时不应旋转。
此操作可以扩展到一段数,之后说。
inline void Delete(int p){
int last=nxt(p,0),next=nxt(p,1);
splay(last,0);splay(next,last);
int del=tree[next].son[0];
if(tree[del].cnt>1){
--tree[del].cnt;
splay(del,0);
}
else tree[next].son[0]=0;
}
区间第k大
之前维护的 \(siz\) 现在有用了,参考权值线段树查询区间第 \(k\) 大的写法。
\(k>siz_{root}\),无解。
将 \(k\) 分割比对,当前值为 \(k_0\),\(k_0>siz_{lson}+cnt_{u}\) 说明在 \(u\) 节点的右儿子,否则在左儿子。最后既不在左儿子又不在右儿子说明找到了。
inline int kth(int x){
int u=root;
if(tree[u].siz<x)return 0;
while(1){
int lson=tree[u].son[0];
if(x>tree[lson].siz+tree[u].cnt){
x-=tree[lson].siz+tree[u].cnt;
u=tree[u].son[1];
}
else{
if(tree[lson].siz>=x)u=lson;
else{
splay(u,0);
return tree[u].val;
}
}
}
}
如何查询 \(x\) 的排名?
BT.find(x);
printf("%d\n",BT.tree[BT.tree[BT.root].son[0]].siz);
放一下完整码子。
#include<bits/stdc++.h>
#define MAXN 200005
using namespace std;
const int inf=1e9;
struct Splay_Tree{
struct TREE{
int fa,son[2];
int val,cnt,siz;
}tree[MAXN];
int root,tot;
inline void update(int p){
tree[p].siz=tree[tree[p].son[0]].siz+tree[tree[p].son[1]].siz+tree[p].cnt;
}
inline void rotate(int p){
int f1=tree[p].fa;
int f2=tree[f1].fa;
tree[f2].son[tree[f2].son[1]==f1]=p;
tree[p].fa=f2;
int k=tree[f1].son[1]==p;
tree[f1].son[k]=tree[p].son[k^1];
tree[tree[p].son[k^1]].fa=f1;
tree[p].son[k^1]=f1;
tree[f1].fa=p;
update(f1),update(p);
}
inline void splay(int p,int king){
while(tree[p].fa!=king){
int f1=tree[p].fa,f2=tree[f1].fa;
if(f2!=king)(tree[f2].son[0]==f1)^(tree[f1].son[0]==p)?rotate(p):rotate(f1);
rotate(p);
}
if(!king)root=p;
}
inline void find(int p){
int u=root;
if(!u)return;
while(tree[u].son[p>tree[u].val]&&p!=tree[u].val)u=tree[u].son[p>tree[u].val];
splay(u,0);
}
inline void insert(int p){
int u=root,fa=0;
while(u&&tree[u].val!=p){
fa=u;
u=tree[u].son[p>tree[u].val];
}
if(u)++tree[u].cnt;
else{
u=++tot;
if(fa)tree[fa].son[p>tree[fa].val]=u;
tree[u].son[0]=tree[u].son[1]=0;
tree[tot].fa=fa;
tree[tot].val=p;
tree[tot].cnt=tree[tot].siz=1;
}
splay(u,0);
}
inline int nxt(int p,int f){
find(p);
int u=root;
if((tree[u].val>p&&f)||(tree[u].val<p&&!f))return u;
u=tree[u].son[f];
while(tree[u].son[f^1])u=tree[u].son[f^1];
return u;
}
inline void Delete(int p){
int last=nxt(p,0),next=nxt(p,1);
splay(last,0);splay(next,last);
int del=tree[next].son[0];
if(tree[del].cnt>1){
--tree[del].cnt;
splay(del,0);
}
else tree[next].son[0]=0;
}
inline int kth(int x){
int u=root;
if(tree[u].siz<x)return 0;
while(1){
int lson=tree[u].son[0];
if(x>tree[lson].siz+tree[u].cnt){
x-=tree[lson].siz+tree[u].cnt;
u=tree[u].son[1];
}
else{
if(tree[lson].siz>=x)u=lson;
else return tree[u].val;
}
}
}
}BT;
int n;
int main(){
scanf("%d",&n);
BT.insert(inf);
BT.insert(-inf);
while(n--){
int opt,x;
scanf("%d%d",&opt,&x);
if(opt==1)BT.insert(x);
if(opt==2)BT.Delete(x);
if(opt==3){
BT.find(x);
printf("%d\n",BT.tree[BT.tree[BT.root].son[0]].siz);
}
if(opt==4)printf("%d\n",BT.kth(x+1));
if(opt==5)printf("%d\n",BT.tree[BT.nxt(x,0)].val);
if(opt==6)printf("%d\n",BT.tree[BT.nxt(x,1)].val);
}
return 0;
}
记得往里头先加两个极值避免查找前驱后继时溢出。
因为出现了简便做法导致蓝降黄。
提供原先的平衡树做法。
按时间加入元素,在这之前找它的前驱后继比对求和。
#include<bits/stdc++.h>
#define MAXN 200005
using namespace std;
const int inf=1e9;
struct Splay_Tree{
struct TREE{
int fa,son[2];
int val;
}tree[MAXN];
int root,tot;
inline void rotate(int p){
int f1=tree[p].fa;
int f2=tree[f1].fa;
tree[f2].son[tree[f2].son[1]==f1]=p;
tree[p].fa=f2;
int k=tree[f1].son[1]==p;
tree[f1].son[k]=tree[p].son[k^1];
tree[tree[p].son[k^1]].fa=f1;
tree[p].son[k^1]=f1;
tree[f1].fa=p;
}
inline void splay(int p,int king){
while(tree[p].fa!=king){
int f1=tree[p].fa,f2=tree[f1].fa;
if(f2!=king)(tree[f2].son[0]==f1)^(tree[f1].son[0]==p)?rotate(p):rotate(f1);
rotate(p);
}
if(!king)root=p;
}
inline void find(int p){
int u=root;
if(!u)return;
while(tree[u].son[p>tree[u].val]&&p!=tree[u].val)u=tree[u].son[p>tree[u].val];
splay(u,0);
}
inline void insert(int p){
int u=root,fa=0;
while(u&&tree[u].val!=p){
fa=u;
u=tree[u].son[p>tree[u].val];
}
if(u)return;
else{
u=++tot;
if(fa)tree[fa].son[p>tree[fa].val]=u;
tree[u].son[0]=tree[u].son[1]=0;
tree[tot].fa=fa;
tree[tot].val=p;
}
splay(u,0);
}
inline int nxt(int p,int f){
find(p);
int u=root;
if((tree[u].val>=p&&f)||(tree[u].val<=p&&!f))return tree[u].val;
u=tree[u].son[f];
while(tree[u].son[f^1])u=tree[u].son[f^1];
return tree[u].val;
}
}BT;
int n,ans;
int main(){
scanf("%d",&n);
BT.insert(inf);
BT.insert(-inf);
for(int i=1,val;i<=n;i++){
scanf("%d",&val);
if(i==1)ans+=val;
else{
int k1=BT.nxt(val,1);
int k2=BT.nxt(val,0);
ans+=min(abs(val-k1),abs(val-k2));
}
BT.insert(val);
}
printf("%d",ans);
return 0;
}
把上面的板子挪下来就行。
发现还是找前驱后继,但是有两种物品即顾客和宠物,都有可能成为待寻找前驱后继的对象。
然注意到:一种物品存在于平衡树时,另一种物品一定不存在于其中。
所以可以只开一颗平衡树,通过外部维护当前树内物品类型进行操作。
规定 \(cnt\) 为当前物品状态,\(|cnt|\) 为物品个数,正负为顾客或宠物。
#include<bits/stdc++.h>
#define int long long
#define MAXN 80005
using namespace std;
int T,cnt,ans;
const int inf=1e18;
const int mod=1000000;
struct Splay_Tree{
#define ls(p) tree[p].son[0]
#define rs(p) tree[p].son[1]
int root,tot;
struct TREE{
int fa,son[2];
int val,cnt,siz;
}tree[MAXN];
inline void update(int p){
tree[p].siz=tree[ls(p)].siz+tree[rs(p)].siz+tree[p].cnt;
return ;
}
inline void rotate(int x){
int y=tree[x].fa,z=tree[y].fa;
int k=(rs(y)==x);
tree[z].son[rs(z)==y]=x;
tree[x].fa=z;
tree[y].son[k]=tree[x].son[k^1];
tree[tree[x].son[k^1]].fa=y;
tree[x].son[k^1]=y;
tree[y].fa=x;
update(y),update(x);
}
inline void splay(int x,int king){
while(tree[x].fa!=king){
int y=tree[x].fa,z=tree[y].fa;
if(z!=king)((rs(y)==x)^(rs(z)==y))?rotate(x):rotate(y);
rotate(x);
}
if(!king)root=x;
}
inline void insert(int x){
int u=root,fa=0;
while(u&&tree[u].val!=x){
fa=u;
u=tree[u].son[x>tree[u].val];
}
u=++tot;
if(fa)tree[fa].son[x>tree[fa].val]=u;
tree[u].fa=fa;
tree[u].val=x;
tree[u].cnt=tree[u].siz=1;
ls(u)=rs(u)=0;
splay(u,0);
}
inline void find(int x){
int u=root;
if(!u)return;
while(tree[u].son[x>tree[u].val]&&x!=tree[u].val)u=tree[u].son[x>tree[u].val];
splay(u,0);
}
inline int nxt(int x,int f){
find(x);
int u=root;
if((tree[u].val<x&&!f)||(tree[u].val>x&&f))return u;
u=tree[u].son[f];
while(tree[u].son[f^1])u=tree[u].son[f^1];
return u;
}
inline int txn(int x,int f){
find(x);
int u=root;
if((tree[u].val<=x&&!f)||(tree[u].val>x&&f))return u;
u=tree[u].son[f];
while(tree[u].son[f^1])u=tree[u].son[f^1];
return u;
}
inline void Del(int x){
int last=nxt(x,0),next=nxt(x,1);
splay(last,0),splay(next,last);
ls(next)=0;
}
}BT;
signed main(){
scanf("%lld",&T);
BT.insert(-inf),BT.insert(inf);
while(T--){
int opt,val;
scanf("%lld%lld",&opt,&val);
if(!cnt)BT.insert(val);
if(cnt<0){
if(!opt)BT.insert(val);
else{
int k0=BT.txn(val,0),k1=BT.txn(val,1);
int v0=BT.tree[k0].val,v1=BT.tree[k1].val;
if(val-v0<=v1-val)BT.Del(v0),ans=(ans+(val-v0)%mod)%mod;
else BT.Del(v1),ans=(ans+(v1-val)%mod)%mod;
}
}
if(cnt>0){
if(opt)BT.insert(val);
else{
int k0=BT.txn(val,0),k1=BT.txn(val,1);
int v0=BT.tree[k0].val,v1=BT.tree[k1].val;
if(val-v0<=v1-val)BT.Del(v0),ans=(ans+(val-v0)%mod)%mod;
else BT.Del(v1),ans=(ans+(v1-val)%mod)%mod;
}
}
cnt+=(opt?1:-1);
}
printf("%lld",ans);
return 0;
}
细节:规定了宠物间与顾客间的权值各不相同,但没有规定宠物与顾客间的权值不同,因此在 \(del()\) 函数与主函数中的 \(nxt()\) 略有不同,区别在于 \(>\) 和 \(≥\)
上文提到的 Splay 为 权值 Splay。事实上 Splay 也可以进行区间操作。
从序列中提取区间
当我们需要区间 \([l,r]\) 时,可以先用 \(kth\) 找到元素 \(l-1\) 将其旋转到根,再把 \(r+1\) 旋转到根的右儿子。此时 \(r+1\) 的左子树中序遍历就是 \([l,r]\)
inline int split(int l,int r){
l=kth(l),r=kth(r+2);
splay(l,0);
splay(r,l);
return tree[r].son[0];
}
由于树中已经有节点 \(inf,-inf\),所以 \(l-1,r+1\) 的实际编号为 \(l,r+2\)
区间修改
参考线段树,首先需要一个 \(push\_up()\)。
也就是上文板子中的 \(update()\)。
修改可以使用线段树同款懒标记 \(tag\)。这就意味着需要有节点承载子节点信息,就像 \(siz_i\) 那种。下放标记的过程和线段树一样,这里以区间和为例。
inline void spread(int p){
if(tree[p].tag){
tree[ls(p)].val+=tree[p].tag*tree[ls(p)].siz;
tree[rs(p)].val+=tree[p].tag*tree[rs(p)].siz;
tree[ls(p)].tag=1;
tree[rs(p)].tag=1;
tree[p].tag=0;
}
}
给定一个序列与 \(m\) 个区间 \([l,r]\),要求依次将区间内元素反转后输出最终序列。
我们发现将序列中的节点安排给一些父节点后,反转一段区间就是把管辖他们的所有父亲节点的左右儿子反转。
规定 \(tag_i\) 为节点 \(i\) 的反转标记。反转两次等于没反转,使用异或打 \(tag\)。
inline void spread(int p){
if(tree[p].tag){
swap(ls(p),rs(p));
tree[ls(p)].tag^=1;
tree[rs(p)].tag^=1;
tree[p].tag=0;
}
}
由于没有查询操作,\(tag\) 只需要在最终的输出中下放,反转区间时将其 \(split()\) 出来,按照上文的结论,给节点 \(r+1\) 的左儿子打 \(tag\) 即可。
#include<bits/stdc++.h>
#define MAXN 200005
using namespace std;
int n,m;
struct Splay_Tree{
#define ls(p) tree[p].son[0]
#define rs(p) tree[p].son[1]
int root,tot;
struct TREE{
int fa,son[2];
int val,siz;
int tag;
}tree[MAXN];
inline void push_up(int p){
tree[p].siz=tree[ls(p)].siz+tree[rs(p)].siz+1;
}
inline void spread(int p){
if(tree[p].tag){
swap(ls(p),rs(p));
tree[ls(p)].tag^=1;
tree[rs(p)].tag^=1;
tree[p].tag=0;
}
}
inline void rotate(int p){
int f1=tree[p].fa;
int f2=tree[f1].fa;
int k=tree[f1].son[1]==p;
tree[f2].son[rs(f2)==f1]=p;
tree[p].fa=f2;
tree[f1].son[k]=tree[p].son[k^1];
tree[tree[p].son[k^1]].fa=f1;
tree[p].son[k^1]=f1;
tree[f1].fa=p;
push_up(f1),push_up(p);
}
inline void splay(int p,int king){
while(tree[p].fa!=king){
int f1=tree[p].fa,f2=tree[f1].fa;
if(f2!=king)(tree[f2].son[1]==f1)^(tree[f1].son[1]==p)?rotate(p):rotate(f1);
rotate(p);
}
if(!king)root=p;
}
inline void insert(int p){
int u=root,fa=0;
while(u&&tree[u].val!=p){
fa=u;
u=tree[u].son[p>tree[u].val];
}
u=++tot;
if(fa)tree[fa].son[p>tree[fa].val]=u;
tree[u].son[0]=tree[u].son[1]=0;
tree[u].fa=fa;
tree[u].siz=1;
tree[u].val=p;
splay(u,0);
}
inline int kth(int x){
int u=root;
if(tree[u].siz<x)return 0;
while(1){
spread(u);
if(x>tree[ls(u)].siz+1){
x-=tree[ls(u)].siz+1;
u=rs(u);
}
else{
if(tree[ls(u)].siz>=x)u=ls(u);
else return tree[u].val;
}
}
}
inline void Reserve(int l,int r){//split 函数可以就地包进别的函数里
l=kth(l),r=kth(r+2);
splay(l,0);
splay(r,l);
tree[ls(rs(root))].tag^=1;
}
inline void print(int p){
spread(p);
if(ls(p))print(ls(p));
if(tree[p].val>1&&tree[p].val<n+2)printf("%d ",tree[p].val-1);
if(rs(p))print(rs(p));
}
}BT;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n+2;i++)BT.insert(i);
for(int i=1,l,r;i<=m;i++){
scanf("%d%d",&l,&r);
BT.Reserve(l,r);
}
BT.print(BT.root);
return 0;
}
题意:维护一种数据结构,能够添加权值为 \(val\) 的元素,对全体元素权值进行增减,并实时删除权值低于 \(min\) 的元素,能够实现区间第 \(k\) 大查询。
注意到只有扣工资时才可能有员工离职,且员工的工资是同升同降的。
不妨转而考虑 \(min\),涨工资 \(k\) 等效于 \(min-=k\),扣工资 \(k\) 等效于 \(min+=k\)。我们维护 \(min\) 的增长量 \(\Delta\),新人来公司先给权值 \(val+=\Delta\) 这时就和原始的 \(min\) 平齐了,直接比对以实现:
如果某个员工的初始工资低于最低工资标准,那么将不计入最后的答案内。
如何实现踢人?每次减工资使用 \(find()\) 函数把工资压线的旋转到根,从左儿子二分查找到第一个 \(val_u<min+\Delta\),然后把 \(u\) 及其左子树删除即可。这就是上文提到的节点删除的序列扩展。
规定变量 \(tmp\) 于每次成功加入员工时递增,\(ans=tmp-tree[root].siz\)。
#include<bits/stdc++.h>
#define MAXN 300005
#define int long long
using namespace std;
int n,nval,delta,tmp;
const int inf=1e18;
struct Splay_Tree{
#define ls(p) tree[p].son[0]
#define rs(p) tree[p].son[1]
struct TREE{
int fa,son[2];
int val,siz,cnt;
}tree[MAXN];
int tot,root;
inline void update(int p){
tree[p].siz=tree[ls(p)].siz+tree[rs(p)].siz+tree[p].cnt;
return;
}
inline void rotate(int x){
int y=tree[x].fa,z=tree[y].fa;
int k=(rs(y)==x);
tree[z].son[rs(z)==y]=x;
tree[x].fa=z;
tree[y].son[k]=tree[x].son[k^1];
tree[tree[x].son[k^1]].fa=y;
tree[x].son[k^1]=y;
tree[y].fa=x;
update(y),update(x);
}
inline void splay(int x,int king){
while(tree[x].fa!=king){
int y=tree[x].fa,z=tree[y].fa;
if(z!=king)((rs(z)==y)^(rs(y)==x))?rotate(x):rotate(y);
rotate(x);
}
if(!king)root=x;
}
inline void insert(int x){
int u=root,fa=0;
while(u&&tree[u].val!=x){
fa=u;
u=tree[u].son[x>tree[u].val];
}
if(u)++tree[u].cnt;
else{
u=++tot;
if(fa)tree[fa].son[x>tree[fa].val]=u;
tree[u].fa=fa;
tree[u].val=x;
tree[u].siz=tree[u].cnt=1;
ls(u)=rs(u)=0;
}
splay(u,0);
}
inline void find(int x){
int u=root;
if(!u)return;
u=tree[u].son[x>tree[u].val];
while(tree[u].son[x>tree[u].val])u=tree[u].son[x>tree[u].val];
splay(u,0);
}
inline int kth(int x){
int u=root;
if(tree[u].siz<x)return 0;
while(1){
if(x>tree[ls(u)].siz+tree[u].cnt){
x-=tree[ls(u)].siz+tree[u].cnt;
u=rs(u);
}
else{
if(tree[ls(u)].siz>=x)u=ls(u);
else{
splay(u,0);
return tree[u].val;
}
}
}
}
inline void Del(){
int x=root,u=root,val=nval-delta;
while(x){
if(tree[x].val<val)x=rs(x);
else u=x,x=ls(x);
}
if(tree[u].val<val){
root=0;
return ;
}
splay(u,0);
ls(u)=0;//不用一个一个点删,直接断掉儿子就行了
update(u);
}
}BT;
signed main(){
scanf("%lld%lld",&n,&nval);
for(int i=1,val;i<=n;i++){
char opt[5];
scanf("%s%lld",opt+1,&val);
if(opt[1]=='I'){
if(val>=nval){
++tmp;
BT.insert(val-delta);
}
}
if(opt[1]=='A')delta+=val;
if(opt[1]=='S')delta-=val,BT.Del();
if(opt[1]=='F'){
if(val>BT.tree[BT.root].siz)printf("-1\n");
else printf("%lld\n",BT.kth(BT.tree[BT.root].siz-val+1)+delta);
}
}
printf("%lld",tmp-BT.tree[BT.root].siz);
return 0;
}
题意:给定一个字符串,要求实现以下操作:替换或添加一个字符,求两个字符的最长公共前缀
序列比对考虑 hash,然求最长,注意到单调性,考虑二分。
序列上的操作显然是平衡树,如何用平衡树维护 hash?
考虑区间 Splay。中序遍历不变则左子树维护当前位前的字符,右子树维护当前位后。
关于替换操作:将待转换位 \(x\) 旋转到根,直接更改 \(hash_x,val_x\) 即可。
如何插入新点:由于要插到 \(x\) 位后,将 \(x\) 旋转到根并将 \(x+1\) 位旋转到儿子,显然 \(x+1\) 的左儿子就是待插入位。
注意插入两个极值。
还有一个大坑:出现插入操作后 \(n=strlen(str+1)\) 就不是序列总长了!!!正确的表示方法是 \(BT.tot-2\)。因为这个弱智问题卡了好久...
#include<bits/stdc++.h>
#define MAXN 150005
#define ull unsigned long long
#define int long long
using namespace std;
char str[MAXN];
ull pw[MAXN];
const int p=131;
int n,m;
struct Splay_Tree{
#define ls(p) tree[p].son[0]
#define rs(p) tree[p].son[1]
int tot,root;
struct TREE{
int fa,son[2];
int val,siz;
ull has;
}tree[MAXN];
inline void update(int p){
tree[p].siz=tree[ls(p)].siz+tree[rs(p)].siz+1;
tree[p].has=tree[rs(p)].has+(ull)tree[p].val*pw[tree[rs(p)].siz]+tree[ls(p)].has*pw[tree[rs(p)].siz+1];
}
inline void rotate(int x){
int y=tree[x].fa,z=tree[y].fa;
int k=(rs(y)==x);
tree[x].fa=z;
tree[z].son[rs(z)==y]=x;
tree[tree[x].son[k^1]].fa=y;
tree[y].son[k]=tree[x].son[k^1];
tree[y].fa=x;
tree[x].son[k^1]=y;
update(y);
update(x);
}
inline void splay(int x,int king){
while(tree[x].fa!=king){
int y=tree[x].fa,z=tree[y].fa;
if(z!=king)((rs(y)==x)^(rs(z)==y))?rotate(x):rotate(y);
rotate(x);
}
if(!king)root=x;
}
inline void build(int l,int r,int x){
if(l>r)return;
int mid=l+r>>1;
tree[x].son[mid>=x]=mid;
tree[mid].fa=x,tree[mid].siz=1;
if(l==r)return;
build(l,mid-1,mid);
build(mid+1,r,mid);
update(mid);
}
inline int kth(int x){
int u=root;
if(tree[u].siz<x)return 0;
while(1){
if(x==tree[ls(u)].siz+1)return u;
else if(x>tree[ls(u)].siz+1)x-=tree[ls(u)].siz+1,u=rs(u);
else u=ls(u);
}
}
inline ull gethas(int l,int r){
int k0=kth(l),k1=kth(r+2);
splay(k0,0),splay(k1,k0);
return tree[ls(rs(root))].has;
}
inline void insert(char ch){
ls(rs(root))=++tot;
tree[tot].fa=rs(root);
tree[tot].val=tree[tot].has=ch;
splay(tot,0);
}
}BT;
signed main(){
pw[0]=1;
for(int i=1;i<MAXN;i++)pw[i]=pw[i-1]*p;
scanf("%s%lld",str+1,&m);
n=strlen(str+1);
for(int i=2;i<=n+1;i++)BT.tree[i].val=BT.tree[i].has=str[i-1];
BT.build(1,n+2,BT.root);
BT.root=(n+3)>>1;
BT.tot=n+2;
for(int i=1,x,y;i<=m;i++){
char opt[3],ch[3];
scanf("%s",opt+1);
if(opt[1]=='Q'){
scanf("%lld%lld",&x,&y);
if(x>y)swap(x,y);
int l=0,r=(BT.tot-2-y+1),res=0;//就是这块有坑!
while(r>=l){
int mid=l+r>>1;
if(BT.gethas(x,x+mid-1)==BT.gethas(y,y+mid-1))res=mid,l=mid+1;
else r=mid-1;
}
printf("%lld\n",res);
}
if(opt[1]=='R'){
scanf("%lld%s",&x,ch+1);
int k=BT.kth(x+1);
BT.splay(k,0);
BT.tree[k].val=ch[1];
BT.update(k);
}
if(opt[1]=='I'){
scanf("%lld%s",&x,ch+1);
int k0=BT.kth(x+1),k1=BT.kth(x+2);
BT.splay(k0,0);
BT.splay(k1,k0);
BT.insert(ch[1]);
}
}
return 0;
}
改多测获取双倍经验。
update:2024.2.28。
关于平衡树的建树 \(\text{build()}\) 函数:
当初始时已经给出序列,可以使用 \(\text{build()}\) 构造一棵平衡的 Splay。
注意到线段树中的递归建树技巧是可以沿袭的,对于区间 \([l,r]\),可以让节点 \(mid=(l+r)/2\) 管辖,建树函数中保存当前节点父亲 \(x\),创建新点后比对权值并安排儿子位置。
inline void build(int l,int r,int x){
if(l>r)return;
int mid=l+r>>1;
tree[mid].fa=x;
tree[x].son[mid>=x]=mid;
tree[mid].val=mid;
tree[mid].siz=1;
tree[mid].rev=0;
if(l==r)return;
build(l,mid-1,mid);
build(mid+1,r,mid);
update(mid);
}
另一核心操作 \(\text{split()}\):
上文的 \(\text{split()}\) 函数扩展性极强,可以直接腾出任意一段区间供我们操作。
提取区间 \([l,r]\) 需要将 \(l-1,r+1\) 分别旋转,我们发现当 \(l=r+1\) 时,正好将点 \(x+1\) 旋转到了 \(x\) 的右儿子,这导致点 \(x+1\) 是沒有左儿子的,如果此时令节点 \(p\) 成为其左儿子,相当于在 \(x\),\(x+1\) 间插入了新点 \(p\)。
可以改写 \(\text{insert()}\) 写法。在序列位次与权值无关时,可以用这种写法添加新节点。
inline void insert(int x,int p){
int k0=kth(x),k1=kth(x+1);
splay(k0,0),splay(k1,k0);
ls(rs(root))=++tot;
tree[tot].fa=k1;
tree[tot].val=p,tree[tot].siz=1;
...
}
Treap
\(treap = tree + heap\)。我们可以叫它 树堆(?
与Splay不同,Treap 的特点是能实现可持久化,但是时间复杂度看脸,大多数情况保持在 \(\Theta(n\ logn)\)。
Treap 分为 有旋 Treap 和 无旋 Treap。
上文对 Splay 的介绍中可以发现:同一中序遍历对应的树有很多棵。
平衡树为了保证查询节点的时间复杂度,会通过一些操作使得任意节点的子树高度差不超过 1。Splay 的 \(\text{rotate}()\) 与 \(\text{splay}()\) 目的就在于此,而 Treap 对这种平衡性的实现使用了随机化。
Treap 在保证 BST 结构的基础之上,通过给节点随机权值实现随机安排父子关系,由于数据的随机性导致了权值大小分布一定程度上平衡。
因此 Treap 实际的复杂度显然会大于 \(\Theta(logn)\)。不过由于 Splay 也存在不小的常数,两者速度差别不大。
我在网上没有找到 Treap 时间复杂度的严格证明。不过想当然地,只要随机出的序列不是基本严格递增的,那么 Treap 的复杂度不可能退化到 \(\Theta(n^2)\),靠近 \(\Theta(log\ n)\)。
带旋 Treap 需要大量指针,我晕针,介绍无旋 Treap:FHQ_Treap
权值 FHQ_Treap:例题。
核心操作:\(\text{split}(val)\)。以 \(val\) 为界限将整棵树分裂成两棵树。
我们按照 \(val=14\) 分裂。
则分裂后的平衡树如下:
分裂后可以获得两棵子树的根节点编号,研究如何分裂:
考虑从根节点递归分裂,将点权小于 \(val\) 的节点 \(p\) 归给根 \(x\)。出于 BST 的性质,\(p\) 的左子树也一定被归于 \(x\),不过右子树就不一定了,这导致节点 \(p\) 的右子树父子关系可能发生改变,且这种改变不能在当前情况下预知。
于是使用取址符递归传参。具体地,规定 \(x,y\) 为分裂出的两棵平衡树在当前层的子树根节点。因此每次处理节点 \(p\) 时,我们要更新父亲节点 \(fa\) 的儿子情况,上文 \(val_p<val\) 所以把 \(p\) 也归进 \(x\),又因为 \(p\) 的左子树是一定被连同着归入 \(x\) 的,所以我们只需递归处理 \(p\) 的右子树 \(son_p\),由于其父亲是左子树的成员,我们把 \(son_p\) 赋作下一层中平衡树 \(x\) 的预定成员。
此时 \(y\) 是要被保留的,因为之后可能会出现 \(val_{p0}\ge val\) 的情况,\(p_0\) 是 \(p\) 右子树中的某个节点。这时就要把 \(p_0\) 归入平衡树 \(y\) 了。也就实现了图中将节点 \(15\) 归于节点 \(17\) 的过程。
\(val_p\ge val\) 的情况是相反的。
特别地,如果当前阶段的分裂已经完成,也就是 \(p=0\) 时,注意清零 \(x,y\)。由于取地址的原因,这里存储的是上一个节点的儿子,不过显然 \(p\) 已经是空节点了。
提供代码。
inline void split(int val,int p,int &x,int &y){
if(!p){
x=y=0;
return;
}
if(val>=tree[p].val){
x=p;
split(val,rs(p),rs(p),y);
}
else if(val<tree[p].val){
y=p;
split(val,ls(p),x,ls(p));
}
update(p);
}
核心操作:\(\text{merge}(x,y)\)。将以节点 \(x,y\) 为根的两棵平衡树合为一棵。
显然也是要递归合并的,由于分裂时已经按照权值分裂了,所以依次合并时 BST 的性质是不变的,\(\text{merge}()\) 操作在合并的同时也要维护树的平衡性,这时候就要用到之前提到的随机值了。通过比对随机值安排父子关系以维护平衡性。
写法和线段树合并很像。
inline int merge(int x,int y){//x 为左节点,y右节点
if(!x||!y)return x^y;
if(tree[x].key>tree[y].key){
rs(x)=merge(rs(x),y);
update(x);
return x;
}
else{
ls(y)=merge(x,ls(y));
update(y);
return y;
}
}
这里注意,平衡树 \(x\) 的所有节点权值严格小于平衡树 \(y\) 的所有节点权值,所以 \(key_x>key_y\) 就让 \(y\) 变成 \(x\) 的右儿子,否则让 \(x\) 变成 \(y\) 的左儿子。可见必须先 \(\text{split}()\) 后 \(\text{merge}()\),要不然 \(key\) 就开始乱合了,且 \(\text{split}()\) 后必 \(\text{merge}()\)。
现在来看例题:需要实现以下操作:
- 插入一个元素 \(x\)
- 删除一个元素 \(x\)
- 查询元素 \(x\) 的排名
- 查询第 \(k\) 大元素
- 查询 \(x\) 的前驱,后继。
莫名熟悉(?
插入元素:
FHQ_Treap 的码量优势在此体现。插入权值 \(val\) 时,先把整棵树按 \(val\) 分裂,然后手动造一个新点。
inline int newnode(int val){
tree[++tot].val=val;
tree[tot].lson=tree[tot].rson=0;
tree[tot].siz=1;
tree[tot].key=rng();
return tot;
}
造好之后把点编号 \(tot\) 返回,按照分裂的原理,平衡树 \(y\) 的权值严格大于等于 \(val\),所以直接 \(\text{merge}(tot,y)\),之后得到的新树 \(y\) 又严格大于 \(x\),再 \(\text{merge}(x,y)\) 即可。
inline void insert(int val){
int x=0,y=0;
split(val,root,x,y);
root=merge(x,merge(newnode(val),y));
}
删除节点:
不难想,把树按照删除值 \(val\) 与 \(val-1\) 分裂,分裂成三棵权值递增的树 \(x,y,z\)。
并且此时 \(y\) 的权值都是 \(val\),我们只需要删除一个点就够了,那就删除根 \(y\)。然后把 \(y\) 的左右儿子合并成新 \(y\),依次合并 \(x,y,z\) 即可。
inline void Delete(int val){
int x=0,y=0,z=0;
split(val,root,x,y);
split(val-1,root,x,y);
split(val,y,y,z);
y=merge(ls(y),rs(y));
root=merge(x,merge(y,z));
}
区间第k大
只要是一颗 BST 那么查找方式固定,跟着维护 \(siz\) 即可,照搬 Splay 的方法。
查询 \(x\) 的排名
\(\text{rk}()\) 函数在平衡树中都很好实现,Splay 中我们把待查询元素旋转到根,输出左子树大小即可。FHQ_Treap 中则直接按 \(val\) 分裂,输出 \(x\) 的大小即可。
inline int getrk(int val){
int x=0,y=0,res;
split(val-1,root,x,y);
res=tree[x].siz+1;
root=merge(x,y);
return res;
}
寻找前驱/后继:
很容易发现,\(\text{kth}()\) 与 \(\text{nxt}()\) 可以相互求出,单独的求法参考 Splay 写法即可。
在 FHQ_Treap 中,寻找前驱可以按 \(val-1\) 分裂,找 \(x\) 的最右儿子,后继则按照 \(val\) 分裂,找 \(y\) 的最左儿子。
inline int nxt(int val,int f){//0 q 1 h
int v[2]={0,0};
split(val-(f^1),root,v[0],v[1]);
int u=v[f];
while(f?ls(u):rs(u))u=f?ls(u):rs(u);
int res=tree[u].val;
root=merge(v[0],v[1]);
return res;
}
提供完整代码。
#include<bits/stdc++.h>
#define int long long
#define MAXN 100005
using namespace std;
int n,m;
std::mt19937 rng(114514);
struct FHQ{
#define ls(p) tree[p].lson
#define rs(p) tree[p].rson
struct TREE{
int lson,rson;
int val,siz,key;
}tree[MAXN];
int tot,root;
inline void update(int p){
tree[p].siz=tree[ls(p)].siz+tree[rs(p)].siz+1;
}
inline int newnode(int val){
tree[++tot].val=val;
tree[tot].lson=tree[tot].rson=0;
tree[tot].siz=1;
tree[tot].key=rng();
return tot;
}
inline void split(int val,int p,int &x,int &y){
if(!p){
x=y=0;
return;
}
if(val>=tree[p].val){
x=p;
split(val,rs(p),rs(p),y);
}
else if(val<tree[p].val){
y=p;
split(val,ls(p),x,ls(p));
}
update(p);
}
inline int merge(int x,int y){
if(!x||!y)return x^y;
if(tree[x].key>tree[y].key){
rs(x)=merge(rs(x),y);
update(x);
return x;
}
else{
ls(y)=merge(x,ls(y));
update(y);
return y;
}
}
inline void insert(int val){
int x=0,y=0;
split(val,root,x,y);
root=merge(x,merge(newnode(val),y));
}
inline void Delete(int val){
int x=0,y=0,z=0;
split(val-1,root,x,y);
split(val,y,y,z);
y=merge(ls(y),rs(y));
root=merge(x,merge(y,z));
}
inline int getrk(int val){
int x=0,y=0,res;
split(val-1,root,x,y);
res=tree[x].siz+1;
root=merge(x,y);
return res;
}
inline int kth(int x){
int u=root;
if(tree[u].siz<x)return 0;
while(1){
if(tree[ls(u)].siz+1==x)return tree[u].val;
else if(tree[ls(u)].siz>=x)u=ls(u);
else x-=tree[ls(u)].siz+1,u=rs(u);
}
}
inline int nxt(int val,int f){//0 q 1 h
int v[2]={0,0};
split(val-(f^1),root,v[0],v[1]);
int u=v[f];
while(f?ls(u):rs(u))u=f?ls(u):rs(u);
int res=tree[u].val;
root=merge(v[0],v[1]);
return res;
}
}BT;
int T;
signed main(){
scanf("%lld",&T);
while(T--){
int opt,x;
scanf("%lld%lld",&opt,&x);
if(opt==1)BT.insert(x);
else if(opt==2)BT.Delete(x);
else if(opt==3)printf("%lld\n",BT.getrk(x));
else if(opt==4)printf("%lld\n",BT.kth(x));
else if(opt==5)printf("%lld\n",BT.nxt(x,0));
else printf("%lld\n",BT.nxt(x,1));
}
return 0;
}
码量明显少于 Splay。
同理地,Treap 也可以实现区间操作。
不难想,在 FHQ_Treap 中提取一段区间 \([l,r]\),只需要从 \(root\) 按 \(r\) 分裂到 \(a,c\),再从 \(a\) 按 \(l-1\) 分裂到 \(a,b\)。此时子树 \(b\) 的中序遍历就是 \([l,r]\) 在上面打 \(tag\) 即可。
注意到一旦左右儿子进行翻转,BST 的性质会被破坏,之后再跑的时候就会出问题。
引入技巧:考虑按子树大小分裂。
我们需要把当前中序遍历下的前 \(val\) 个点分裂出一棵子树,不妨使用 \(\text{kth}()\) 函数的思想,将 \(val\) 分裂成众多左子树大小之和,将那些左子树合成一棵平衡树即可。
inline void split(int val,int p,int &x,int &y){
if(!p){
x=y=0;
return;
}
spread(p);
if(tree[ls(p)].siz+1<=val){
x=p;
split(val-tree[ls(p)].siz-1,rs(p),rs(p),y);
}
else{
y=p;
split(val,ls(p),x,ls(p));
}
update(p);
}
别的不变。
例题
显然对于每次于 \(x\) 新添加的点 \(val_i\),有答案:
其中 \(dp[x]\) 是指当前序列到 \(x\) 位结束时的最长上升子序列。
不过序列是动态的,这意味着不能使用 \(dp_i\) 数组,而是用数据结构维护 LIS。
注意到 \(val_i\) 是递增的,这意味着,序列的前 \(x\) 位一定严格小于 \(val_i\),这意味着,在计算 \(dp_i\) 时,不会也不应该出现 \(val_j>val_i,j<i\) 的情况。
于是考虑先用平衡树构造完整序列,获得每个点的添加次序,开线段树维护 \(dp_i\)。
#include<bits/stdc++.h>
#define int long long
#define MAXN 100005
using namespace std;
int n;
std::mt19937 rng(1919810);
struct FHQ_Treap{
#define ls(p) tree[p].lson
#define rs(p) tree[p].rson
struct TREE{
int lson,rson;
int val,siz;
int key;
}tree[MAXN];
int root,tot;
inline void update(int p){
tree[p].siz=tree[ls(p)].siz+tree[rs(p)].siz+1;
}
inline int newnode(int val){
tree[++tot].val=val;
tree[tot].siz=1;
tree[tot].key=rng();
ls(tot)=rs(tot)=0;
return tot;
}
inline void split(int val,int p,int &x,int &y){
if(!p){
x=y=0;
return;
}
if(tree[ls(p)].siz+1<=val){
x=p;
split(val-tree[ls(p)].siz-1,rs(p),rs(p),y);
}
else{
y=p;
split(val,ls(p),x,ls(p));
}
update(p);
}
inline int merge(int x,int y){
if(!x||!y)return x^y;
if(tree[x].key>=tree[y].key){
ls(y)=merge(x,ls(y));
update(y);
return y;
}
else{
rs(x)=merge(rs(x),y);
update(x);
return x;
}
}
inline void insert(int loc,int val){
int x=0,y=0;
split(loc,root,x,y);
root=merge(x,merge(newnode(val),y));
}
inline int kth(int x){
int u=root;
if(tree[u].siz<x)return 0;
while(1){
if(x==tree[ls(u)].siz+1)return u;
else if(x<tree[ls(u)].siz+1)u=ls(u);
else x-=tree[ls(u)].siz+1,u=rs(u);
}
}
}BT;
int Loc[MAXN];
int ans;
struct Segment_Tree{
#define lsn(p) p<<1
#define rsn(p) p<<1|1
#define push_up(p) tree[p].val=max(tree[lsn(p)].val,tree[rsn(p)].val)
struct TREE{
int l,r;
int val;
}tree[MAXN<<2];
inline void build(int l,int r,int p){
tree[p].l=l,tree[p].r=r,tree[p].val=0;
if(l==r)return;
int mid=l+r>>1;
build(l,mid,lsn(p));
build(mid+1,r,rsn(p));
}
inline void update(int x,int k,int p){
if(tree[p].l==tree[p].r){
tree[p].val=k;
return;
}
int mid=tree[p].l+tree[p].r>>1;
if(x<=mid)update(x,k,lsn(p));
else update(x,k,rsn(p));
push_up(p);
}
inline int query(int l,int r,int p){
if(tree[p].l>=l&&tree[p].r<=r)return tree[p].val;
int mid=tree[p].l+tree[p].r>>1;
int res=0;
if(l<=mid)res=max(res,query(l,r,lsn(p)));
if(r>mid)res=max(res,query(l,r,rsn(p)));
return res;
}
}ST;
signed main(){
// #define wzw sb
#ifdef wzw
freopen("1.in","r",stdin);
#endif
scanf("%lld",&n);
for(int i=1,x;i<=n;i++){
scanf("%lld",&x);
BT.insert(x,i);
}
for(int i=1;i<=n;i++)Loc[BT.tree[BT.kth(i)].val]=i;
ST.build(1,n,1);
for(int i=1;i<=n;i++){
if(Loc[i]==1)ans=max(ans,(int)1),ST.update(1,1,1);
else{
int tmp=ST.query(1,Loc[i]-1,1);
ans=max(ans,tmp+1);
ST.update(Loc[i],tmp+1,1);
}
printf("%lld\n",ans);
}
return 0;
}