平衡树
FHQ-Treap
非旋 Treap,代码短,好理解。
首先,平衡树维护两个值:权值(下文代码中用 \(key\) 表示),键值(下文代码中用 \(val\) 表示(以后闲了可能会改过来))。
- 键值:
维护的是这个平衡树的深度不会超过 \(\log n\)(其他方面没什么用)
- 权值:
有两种主要的代表形式:
-
该节点在序列中的位置;
-
该点维护的数的大小(该点权值)
平衡树总满足其中序遍历为原序列(即左-根-右的顺序)
两种 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,于是我们可以通过“回收”已经删除过的点作为新点。
具体操作:
-
建一个栈。
-
将删除之后的点加入栈内
-
新建点的时候把所有信息初始化为新节点信息(根据题意)
-
将已经回收过的点弹出栈
具体代码:
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)\);

浙公网安备 33010602011771号