平衡树小结

引用一句百经: 在计算机科学中,平衡树能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。平衡树,概括来说是一个一般化的二叉查找树(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\) 会有变化,不过子树大小的获取方式是固定的:

\[siz_u=siz_{lson\ u}+siz_{rson\ u}+cnt_u \]

旋转时重新分配节点父子关系,之后更新有变动的节点 \(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\),有答案:

\[ans_i=max(ans_{i-1},dp[x]+1) \]

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

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

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

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

\[ans_i=max(ans_{i-1},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 @ 2024-02-20 19:35  RVm1eL_o6II  阅读(11)  评论(0编辑  收藏  举报