平衡树小结

引用一句百经: 在计算机科学中,平衡树能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。平衡树,概括来说是一个一般化的二叉查找树(binary search tree),可以拥有多于2个子节点。与自平衡二叉查找树不同,B树为系统大块数据的读写操作做了优化。B树减少定位记录时所经历的中间过程,从而加快存取速度。B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。

所以看一下二叉查找树的性质。

将序列 N 构造二叉查找树,则该树的中序遍历就是序列 N

换言之,在二叉查找树 T 中,pT,valp>vallsonp,valp<valrsonp。如果序列中有重复元素,使用数组 cnti 表示第 i 个节点出现的数量。

这告诉我们:平衡树就是维护一个递增序列,在其上进行各种操作查询。

平衡树的复杂度为 Θ(n logn),单词操作复杂度 Θ(logn)

Splay

普通平衡树

关于 Splay:

别名伸展树。

	struct TREE{
		int fa,son[2];
		int val,cnt,siz;
	}tree[MAXN];

treei 数组为 Splay 的主体,所代表的是树上的一个节点,维护以下元素:

  • fai: 节点 i 的父亲节点。
  • soni,0/1: 节点 i 的左右儿子节点。
  • vali: 当前节点所代表的权值。
  • cnti: 当前权值个数
  • sizi: 当前节点所在子树大小。

现在说两个核心操作:rotate(),splay()

rotate(),即旋转,分为左旋和右旋。

旋转的本质是将某个节点上移一个单位,且必须保证:

  • 旋转后的 Splay 中序遍历不变。
  • 旋转影响到的节点信息正确有效。
  • 节点 root 指向旋转后的根节点。

设等待旋转至根节点的节点为 x,其父节点为 y,爷爷节点为 z。现在要求旋转节点 x,即维护信息与中序遍历不变的情况下将 x 移至 y 的位置。

多次试验后发现规律:旋转节点 x 后,三点的父子关系:fax=z,fay=x,z=root,左右儿子关系:令原先 xySayzSb,则新关系下 xzSbyxSa 1

旋转后子树大小 sizi 会有变化,不过子树大小的获取方式是固定的:

sizu=sizlson u+sizrson u+cntu

旋转时重新分配节点父子关系,之后更新有变动的节点 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 变为根节点。

我们考虑实现。

如果 kingp 很远,我们不妨把问题分割。还是从刚才的 x,y,z 入手。我们假设 king 节点就在 z 节点上方,于是子问题成了:如何将 x 移动至 z 的位置。

  • 重复进行以下操作直到 x 的父亲就是 king
  • 终止条件,如果 kingx 的爷爷节点即 z,旋转 x 即可。

又令 yzSa 子节点,xySb 节点。

  • SaSb=1,需连续旋转两次 x
  • SaSb=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 的左儿子 lsonp 严格小于 p,右儿子 rsonp 严格大于 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].valp>tree[u].val 就往右子树走,否则就往左子树走,如果遇见相等就地 ++cntu

如果发现这是个新点,那么考虑使用动态开点的思想。就地赋予点编号 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 的前驱 nxt0 后继 nxt1

nxt0 旋转为根节点,再把 nxt1 旋转成 nxt0 的儿子。后继是大于前驱的,因此一定是它的右儿子。我们可以把 p 理解为 nxt1 的前驱,然而此时 nxt0 已经是根节点了,所以 nxt1 的左子树有且仅有一个节点 p

对找到的 cntp 即可,把他旋转成根节点方便操作。特别地,cntp=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>sizroot,无解。

k 分割比对,当前值为 k0k0>sizlson+cntu 说明在 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 找到元素 l1 将其旋转到根,再把 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,所以 l1,r+1 的实际编号为 l,r+2

区间修改

参考线段树,首先需要一个 push_up()

也就是上文板子中的 update()

修改可以使用线段树同款懒标记 tag。这就意味着需要有节点承载子节点信息,就像 sizi 那种。下放标记的过程和线段树一样,这里以区间和为例。

	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],要求依次将区间内元素反转后输出最终序列。

我们发现将序列中的节点安排给一些父节点后,反转一段区间就是把管辖他们的所有父亲节点的左右儿子反转。

规定 tagi 为节点 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 的增长量 Δ,新人来公司先给权值 val+=Δ 这时就和原始的 min 平齐了,直接比对以实现:

如果某个员工的初始工资低于最低工资标准,那么将不计入最后的答案内。

如何实现踢人?每次减工资使用 find() 函数把工资压线的旋转到根,从左儿子二分查找到第一个 valu<min+Δ,然后把 u 及其左子树删除即可。这就是上文提到的节点删除的序列扩展。

规定变量 tmp 于每次成功加入员工时递增,ans=tmptree[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 旋转到根,直接更改 hashx,valx 即可。

如何插入新点:由于要插到 x 位后,将 x 旋转到根并将 x+1 位旋转到儿子,显然 x+1 的左儿子就是待插入位。

注意插入两个极值。

还有一个大坑:出现插入操作后 n=strlen(str+1) 就不是序列总长了!!!正确的表示方法是 BT.tot2。因为这个弱智问题卡了好久...

#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。

关于平衡树的建树 build() 函数:

当初始时已经给出序列,可以使用 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);
	}

另一核心操作 split()

上文的 split() 函数扩展性极强,可以直接腾出任意一段区间供我们操作。

提取区间 [l,r] 需要将 l1,r+1 分别旋转,我们发现当 l=r+1 时,正好将点 x+1 旋转到了 x 的右儿子,这导致点 x+1 是沒有左儿子的,如果此时令节点 p 成为其左儿子,相当于在 xx+1 间插入了新点 p

可以改写 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 的特点是能实现可持久化,但是时间复杂度看脸,大多数情况保持在 Θ(n logn)

Treap 分为 有旋 Treap 和 无旋 Treap。

上文对 Splay 的介绍中可以发现:同一中序遍历对应的树有很多棵。

平衡树为了保证查询节点的时间复杂度,会通过一些操作使得任意节点的子树高度差不超过 1。Splay 的 rotate()splay() 目的就在于此,而 Treap 对这种平衡性的实现使用了随机化。

Treap 在保证 BST 结构的基础之上,通过给节点随机权值实现随机安排父子关系,由于数据的随机性导致了权值大小分布一定程度上平衡。

因此 Treap 实际的复杂度显然会大于 Θ(logn)。不过由于 Splay 也存在不小的常数,两者速度差别不大。

我在网上没有找到 Treap 时间复杂度的严格证明。不过想当然地,只要随机出的序列不是基本严格递增的,那么 Treap 的复杂度不可能退化到 Θ(n2),靠近 Θ(log n)

带旋 Treap 需要大量指针,我晕针,介绍无旋 Treap:FHQ_Treap

权值 FHQ_Treap:例题

核心操作:split(val)。以 val 为界限将整棵树分裂成两棵树。

我们按照 val=14 分裂。

则分裂后的平衡树如下:

分裂后可以获得两棵子树的根节点编号,研究如何分裂:

考虑从根节点递归分裂,将点权小于 val 的节点 p 归给根 x。出于 BST 的性质,p 的左子树也一定被归于 x,不过右子树就不一定了,这导致节点 p 的右子树父子关系可能发生改变,且这种改变不能在当前情况下预知。

于是使用取址符递归传参。具体地,规定 x,y 为分裂出的两棵平衡树在当前层的子树根节点。因此每次处理节点 p 时,我们要更新父亲节点 fa 的儿子情况,上文 valp<val 所以把 p 也归进 x,又因为 p 的左子树是一定被连同着归入 x 的,所以我们只需递归处理 p 的右子树 sonp,由于其父亲是左子树的成员,我们把 sonp 赋作下一层中平衡树 x 的预定成员。

此时 y 是要被保留的,因为之后可能会出现 valp0val 的情况,p0p 右子树中的某个节点。这时就要把 p0 归入平衡树 y 了。也就实现了图中将节点 15 归于节点 17 的过程。

valpval 的情况是相反的。

特别地,如果当前阶段的分裂已经完成,也就是 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);
	}

核心操作:merge(x,y)。将以节点 x,y 为根的两棵平衡树合为一棵。

显然也是要递归合并的,由于分裂时已经按照权值分裂了,所以依次合并时 BST 的性质是不变的,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 的所有节点权值,所以 keyx>keyy 就让 y 变成 x 的右儿子,否则让 x 变成 y 的左儿子。可见必须先 split()merge(),要不然 key 就开始乱合了,且 split() 后必 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,所以直接 merge(tot,y),之后得到的新树 y 又严格大于 x,再 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));
	}

删除节点:

不难想,把树按照删除值 valval1 分裂,分裂成三棵权值递增的树 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 的排名

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

寻找前驱/后继:

很容易发现,kth()nxt() 可以相互求出,单独的求法参考 Splay 写法即可。

在 FHQ_Treap 中,寻找前驱可以按 val1 分裂,找 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],只需要从 rootr 分裂到 a,c,再从 al1 分裂到 a,b。此时子树 b 的中序遍历就是 [l,r] 在上面打 tag 即可。

注意到一旦左右儿子进行翻转,BST 的性质会被破坏,之后再跑的时候就会出问题。

引入技巧:考虑按子树大小分裂。

我们需要把当前中序遍历下的前 val 个点分裂出一棵子树,不妨使用 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 新添加的点 vali,有答案:

ansi=max(ansi1,dp[x]+1)

其中 dp[x] 是指当前序列到 x 位结束时的最长上升子序列。

不过序列是动态的,这意味着不能使用 dpi 数组,而是用数据结构维护 LIS。

注意到 vali 是递增的,这意味着,序列的前 x 位一定严格小于 vali,这意味着,在计算 dpi 时,不会也不应该出现 valj>vali,j<i 的情况。

于是考虑先用平衡树构造完整序列,获得每个点的添加次序,开线段树维护 dpi

ansi=max(ansi1,max{dp[j]}+1),j<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;
}
posted @   Cl41Mi5deeD  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示