平衡树

FHQ-Treap

非旋 Treap,代码短,好理解。

洛谷日报

首先,平衡树维护两个值:权值(下文代码中用 \(key\) 表示),键值(下文代码中用 \(val\) 表示(以后闲了可能会改过来))。

  • 键值:

维护的是这个平衡树的深度不会超过 \(\log n\)(其他方面没什么用)

  • 权值:

有两种主要的代表形式:

  1. 该节点在序列中的位置;

  2. 该点维护的数的大小(该点权值)

平衡树总满足其中序遍历为原序列(即左-根-右的顺序)

两种 split 的方式:

1. 该点权值代表它所在位置时:

inline void split(ll pos,ll x,ll &l,ll &r) {
	if (!pos) {l=r=0;return;}
	pushdown(pos);
	ll u=tree[tree[pos].l].siz+1;
	
	if (u<=x) {
		l=pos;
		split(tree[pos].r,x-u,tree[pos].r,r);
		pushup(l);
	}
	else {
		r=pos;
		split(tree[pos].l,x,l,tree[pos].l);
		pushup(r);
	}
	
}

2. 该点代表的是数的大小:

inline void split(ll pos,ll x,ll &l,ll &r) {
	if (!pos) {l=r=0;return;}
	if (tree[pos].key<=x) {
		l=pos;
		split(tree[l].r,x,tree[l].r,r);
	}
	else {
		r=pos;
		split(tree[r].l,x,l,tree[r].l);
	}
	pushup(pos);
}

  • 节点信息上传

每次改变某个节点的时候都需要进行上传来保证节点信息的准确性

把我们要维护的信息直接上传即可,像线段树一样。

如下是维护区间最大子段和及区间和的代码:

inline void pushup(ll pos) {
	if (!pos) return;
	tree[pos].siz=tree[tree[pos].l].siz+tree[tree[pos].r].siz+1;
	ll zero=0;
	tree[pos].sum=tree[tree[pos].l].sum+tree[tree[pos].r].sum+tree[pos].key;
	tree[pos].ml=max(tree[tree[pos].l].ml,max(tree[tree[pos].l].sum+tree[pos].key+tree[tree[pos].r].ml,zero));
	tree[pos].mr=max(tree[tree[pos].r].mr,max(tree[tree[pos].r].sum+tree[pos].key+tree[tree[pos].l].mr,zero));
	tree[pos].most=max(tree[pos].key,tree[pos].key+tree[tree[pos].l].mr+tree[tree[pos].r].ml);
	if (tree[pos].l) tree[pos].most=max(tree[pos].most,tree[tree[pos].l].most);
	if (tree[pos].r) tree[pos].most=max(tree[pos].most,tree[tree[pos].r].most);
}
  • 在下传的时候一定要这样(不然大红大紫让你崩溃):

非可持久化:

inline void reverse(ll pos) {
//	进行交换/权值增加……
//  对该节点打标记!!!
}
if (tree[pos].tage) {
	if (tree[pos].l) reverse(tree[pos].l);
	if (tree[pos].r) reverse(tree[pos].r);
	tree[pos].tage=0;
}

可持久化:


inline ll clone(ll pos) {
	tree[++tot]=tree[pos];
	return tot;
}
inline void reverse(ll pos) {
//	进行交换/权值增加……
//  对该节点打标记!!!
}
if (tree[pos].tage) {
	if (tree[pos].l) tree[pos].l=clone(tree[pos].l);
	if (tree[pos].r) tree[pos].r=clone(tree[pos].r);
	if (tree[pos].l) reverse(tree[pos].l);
	if (tree[pos].r) reverse(tree[pos].r);
	tree[pos].tage=0;
}



  • 回收站——空间换时间

我们新建点的时候,如果新建的点数量极多,一个一个新建的话可能会 MLE,于是我们可以通过“回收”已经删除过的点作为新点。

具体操作:

  1. 建一个栈。

  2. 将删除之后的点加入栈内

  3. 新建点的时候把所有信息初始化为新节点信息(根据题意)

  4. 将已经回收过的点弹出栈

具体代码:

inline ll get(ll x) {
	if (top) {
		ll zero=0;
		tree[st[top]].siz=1;tree[st[top]].key=x;tree[st[top]].sum=x;
		tree[st[top]].l=tree[st[top]].r=tree[st[top]].cov=tree[st[top]].tage=tree[st[top]].lazy=0;
		tree[st[top]].val=rand();
		tree[st[top]].most=x;
		tree[st[top]].ml=tree[st[top]].mr=max(zero,x);
		top--;
		return st[top+1];
	}
	else return getrand(x);
}

inline void del(ll pos) {
	if (!pos) return;
	st[++top]=pos;
}
  • 区间二分建点

比如我们要在 \(pos\) 节点之后插入 \(x\) 个数,可以这样操作:

\(c\) 为我要插入的数在 \(c\) 位置之后,\(cnt\) 表示插入数字的个数)


inline ll insert(ll l,ll r) {
	if (l==r) return get(w[l]);//get函数内容与上文提到的“回收站”建点代码相同
	ll mid=(l+r)>>1;
	return merge(insert(l,mid),insert(mid+1,r));
}

ll c=in(),cnt=in();
for (ll j=1;j<=cnt;++j) w[j]=in();
split(root,c,dl,dr);
root=merge(merge(dl,insert(1,cnt)),dr);
  • 区间删点

\(c\) 为我要删除的数从 \(c\) 位置开始,\(cnt\) 表示删除数字的个数)


inline void del(ll pos) {
	if (!pos) return;
	st[++top]=pos;
	if (tree[pos].l) del(tree[pos].l);
	if (tree[pos].r) del(tree[pos].r);
}

ll c=in(),cnt=in();
split(root,c-1,dl,dr);split(dr,cnt,temp,dr);
del(temp);
root=merge(dl,dr);
  • 输出原序列:

根据中序遍历顺序 递归输出:

inline void output(ll pos) {
	pushdown(pos);
	if (tree[pos].l) output(tree[pos].l);
	printf("%lld ",tree[pos].key);
	if (tree[pos].r) output(tree[pos].r);
}

以上各种操作的应用参见【典例】中 T0。

  • 注意区分 key 值和 val 值!

典例:

0. [NOI2005] 维护数列

大大大大大大 细节毒瘤题

说多了都是泪:

代码点题目链接看吧……

1. [HNOI2012] 永无乡

启发式合并平衡树:将结点较少的平衡树一个节点一个节点的插入节点较多的平衡树

2. 银河英雄传说V2

不记录根平衡树好题

主要思想:合并平衡树!

初始状态下我们可以把每个节点作为一个平衡树,接下来按照题意进行合并即可(这点其他题解说的很清楚。)

因为如果记录每一个平衡树的根的话非常难维护,所以我们不妨不记录每棵树的根节点,当用到根节点的时候直接现找。

为了更方便的找一棵树的根和某个节点的位置以及其他更多信息,我们记录每个节点的父节点。

  • 如何更新父节点;

push up 的时候顺带上传即可:

inline void pushup(ll pos) {
	tree[pos].siz=tree[tree[pos].l].siz+tree[tree[pos].r].siz+1;
	tree[pos].sum=tree[tree[pos].l].sum+tree[tree[pos].r].sum+tree[pos].key;
	if (tree[pos].l) tree[tree[pos].l].fa=pos;
	if (tree[pos].r) tree[tree[pos].r].fa=pos;
}
  • split 之后每个节点的父节点需要重新初始化如下:
inline void split(ll pos,ll x,ll &l,ll &r) {
	if (!pos) {l=r=0;return;}
	ll u=tree[tree[pos].l].siz+1;
	if (u<=x) {
		l=pos;
		split(tree[l].r,x-u,tree[l].r,r);
	}
	else {
		r=pos;
		split(tree[r].l,x,l,tree[r].l);
	}
	tree[l].fa=l,tree[r].fa=r;
	pushup(pos);
}
  • 如何通过父节点信息找到该节点对应树的根:
inline ll getfa(ll pos) {
	while (tree[pos].fa!=pos) pos=tree[pos].fa;
	return pos;
}
  • 如何通过父节点找到该节点处在该树的位置:
inline ll getrank(ll pos) {
	ll cnt=tree[tree[pos].l].siz+1;
	while (tree[pos].fa!=pos) {
		if (tree[tree[pos].fa].r==pos) cnt+=tree[tree[tree[pos].fa].l].siz+1;
		pos=tree[pos].fa;
	}
	return cnt;
}
  • 为什么不需要向上题一样启发式合并?

上文有提到两种平衡树的 split 方式,因为 T1 中节点维护的是该数的大小,所以为了保证我们日后仍能顺利地查询到某个数的排名,所以我们要把它拆开放到合适的位置;

而此题要求将 \(x\) 序列放到 \(y\) 序列之后,我们直接 merge,虽然这样不保证以后分裂的时候左边权值和一定大于右边的权值和,但是注意到我判断分裂方式的语句是:

ll u=tree[tree[pos].l].siz+1;
if (u<=x) 

即我是通过判断左子树大小来进行分裂的,与权值无关,所以我们可以以此来假装我们修改了权值来达到一样的效果

  • 要点:把 \(x\) 加到 \(y\) 后,是:\(merge(y,x)\)
posted @ 2023-07-10 02:29  Pwtking  阅读(42)  评论(0)    收藏  举报