fhq treap
fhq-treap 小结
粗浅地学习了这个神奇的数据结构,下面瞎写一些感受
首先fhq treap是一个基于分裂与合并的平衡树,那么怎么分裂,怎么合并呢
我们分两种情况考虑
一、权值平衡树(我自己取的名字)
所谓权值平衡树,就是任何操作都只与权值有关的平衡树
比如最基础的分裂,合并操作
分裂就是把平衡树按照权值\(k\)分成两半,一边所有点的权值\(\leq k\),另一边权值\(\gt k\)
怎么分裂呢
首先根据\(treap\)的定义,所有点的权值是一颗二叉搜索树(BST),就是左边比他小,右边比他大
那么分裂的时候,我们比较当前节点和\(k\)的大小,如果比\(k\)大,那么显然右子树内全部节点都比\(k\)大
所以我们把当前节点加上他的右子树连到第二棵树上,剩下的左子树继续分裂
如果比\(k\)小也是一样的
Example Code:
void split(int &x,int &y,int k,int nw){
//x表示第一棵子树上的某个节点的某个指针,y是第二棵子树上的,nw是当前节点
if(nw==0){x=y=0;return;}
if(tr[x].val>k){
y=nw;split(x,tr[nw].l,k,tr[nw].l);
}
else{
x=nw;split(tr[nw].r,y,k,tr[nw].r);
}
update(nw);
}
然后合并就更简单了
就是把两颗子树(默认其中有一棵每个点的权值都小于另一棵任何点)合成一个
因为有上面那个条件,所以只考虑那个随机值的大小使得树满足堆的性质即可
那么比较当前两棵树中的两个当前节点:
如果第一棵树的rand值小,那么这个点连到总树上,然后这个点的左子树肯定不受到第二棵子树的影响(权值小于第二棵子树),那么递归合并这个点的右子树和第二棵子树即可
反之亦然
Example Code:
int merge(int x,int y){
if(!x || !y) return x+y; //精妙的写法,避免一堆if
if(tr[x].rnd<tr[y].rnd){
tr[x].r=merge(tr[x].r,y);update(x);
return x;
}
else{
tr[y].l=merge(x,tr[y].l);update(y);
return y;
}
}
这两种操作都是\(O(深度)\)的,所以复杂度是\(\log\)级别的
那么其他的操作就是两个操作拼凑起来
比如插入一个数,就是先分裂成小于等于这个数的一部分,大于这个数的一部分,然后当前数当做第三部分,做\(merge(merge(l,nw),r)\)即可
比如删除一个数,就是先分裂成小于等于这个数的一部分,大于这个数的一部分,然后再把第一棵树分成等于这个数和小于这个数,然后在等于这个数的树里把根删掉(merge根的两个子树),最后全部合起来就可以了
再比如求排名,可以分出小于这个数的一部分,这个部分的size就是排名了
等等。。。
但是有一个问题,就是常数比较大
比如删除操作常数可能到5~6,比起什么线段树,树状数组之类的慢多了
但是在平衡树里面算快的了。。。(垃圾数据结构)
但是这样怎么处理区间赋值,区间加这种操作呢?没有办法。。。
因为你在处理的时候一直保持有序状态,顺序就乱套了,没法取出区间
所以我们引入第二类fhq-treap
二、序列操作平衡树(仍旧是我自己取的名字。。。)
这个平衡树的时间复杂度很奇怪,反正我觉得不是\(\log\)的,但是好像跑的挺快
有没有人会证复杂度的请留言,谢谢
好吧现在我大概会证了
其实这个跟上面那个是一个东西
我们大概可以理解为把下标作为val值,权值作为一个新的变量
然后因为下标肯定是连续的1~n,所以子树大小也就是下标了
所以我们不需要记录下标,可以直接用size当做下标使用,这样就和上面那个完全一样了
这个和上面那个的唯一区别就是他不满足BST的性质(或者满足但是我没有发现?)
首先我们观察他的build函数(就是把一个无序数组\(O(n)\)建成treap)
int build(int *data,int k){
int nw=0,pre=0;top=0;
rep(i,1,k){
nw=newnode(data[i]);pre=0;
while(top && rnd[sta[top]]>rnd[nw]){
update(sta[top]);
pre=sta[top];
sta[top--]=0;
}
if(top) son[sta[top]][1]=nw;
son[nw][0]=pre;sta[++top]=nw;
}
while(top) update(sta[top--]);
return sta[1];
}
其中data就是这个数组,k是他的长度
sta维护的是一个栈,top是栈顶指针
newnode(x)是新建一个权值为x的节点并且返回下标
这是什么意思呢
就是说我们只关心随机值,保持他是个堆
那么我们顺着插入每一个元素
在栈里存储最右链,栈顶存储最底下的元素,栈底存储根,那么rand值显然从栈顶到栈底递减,然后我们在栈里找,直到找到第一个rand小于当前rand的元素,那么这个元素就是当前元素的父亲
然后我们假装数组是有序的,那么新加进去的元素肯定最大,所以他就是他父亲的右儿子
他父亲原来的右子树变成他的左子树(权值比他小)
然后更新最右链
从中我们显然发现不满足BST的性质(或者说下标满足BST的性质,但是我们存储的值是权值而非下标,也就是说下标并没记录,在操作几次之后就全乱了),那么复杂度是怎么回事啊。。。
先不说复杂度的事了,我们先看有哪些操作
最基础的依旧是split和merge
void split(int &x,int &y,int k,int nw){
if(nw==0){x=y=0;return;}
pushdown(nw);
if(sz[son[nw][0]]>=k){
y=nw;split(x,son[nw][0],k,son[nw][0]);
}
else{
x=nw;split(son[nw][1],y,k-sz[son[nw][0]]-1,son[nw][1]);
}
update(nw);
}
int merge(int x,int y){
if(x) pushdown(x);if(y) pushdown(y);
if(!x || !y) return x+y;
if(rnd[x]<rnd[y]){
son[x][1]=merge(son[x][1],y);
update(x);return x;
}
else{
son[y][0]=merge(x,son[y][0]);
update(y);return y;
}
}
我们看跟之前的区别
实际上区别不太大,就是把split中val的比较变成了子树大小的比较,传进去的k指的是位置而非权值
然后注意到多了一个pushdown函数
什么鬼。。。这不是线段树操作吗。。。
好的,为了处理区间操作,我们引入了懒标记(好强啊),因为我们的treap不再按val分而是按下标分,所以一个节点的儿子也是他的子区间,和线段树类似,所以我们可以完成懒标记的下放操作
void pushdown(int x){
if(son[x][0]) change(son[x][0],lazy[x]);
if(son[x][1]) change(son[x][1],lazy[x]);
lazy[x]=0;
}
事实上现在我们就讲完了所有基础操作了,可以做很多题了,就是把这些操作拼拼凑凑即可
下面我们看一道例题:
bzoj1500 维修数列
请写一个程序,要求维护一个数列,支持以下 6 种操作:
请注意,格式栏 中的下划线‘ _ ’表示实际输入文件中的空格
我们一个个操作分析
首先插入操作
他并不插入一个数,他插入一堆数。。。
怎么办呢
首先插入总次数是有限制的,所以每次暴力插入应该是可行的,(不过因为复杂度本身就很奇怪所以t了也是正常的事)
但是我们有更好的插入方法
插入一个的时候,我们是merge三棵树,现在也一样嘛,就是先把这个序列建成一个treap,然后一起merge起来
因为他插入位置是连续的一段,所以merge是正确的
然后删除操作
就是拆成三颗子树,然后把中间的扔掉
然后修改
记录标记表示这个区间被修改成几了,如果为inf则没被修改
那么只要拆成三颗树,然后把根打一下标记就好了
翻转
先拆成三棵树
然后把中间的树每一层都翻转
这样就炸了
所以我们打上一个翻转标记,翻转一次就xor 1
求和
sum是很好维护的,因为修改的时候只要记录size就可以维护了,翻转的时候总的sum不变
求最大子序列
这个有点麻烦
维护三个量,首先lmx表示这个节点对应区间的左边一整段的最大和,rmx就是右边一整段的最大和,tmx表示经过这个节点的根的最大和
然后tmx[root]就是答案
更新详见代码
#include<stdio.h>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
#include<cmath>
#include<iostream>
#include<queue>
#include<string>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<long long,long long> pll;
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define rep(i,j,k) for(register int i=(int)(j);i<=(int)(k);i++)
#define rrep(i,j,k) for(register int i=(int)(j);i>=(int)(k);i--)
ll read(){
ll x=0,f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
const int inf=1000000000;
const int maxn=1000100;
queue<int> trash;int n,m,a[maxn];
int sta[maxn],son[maxn][2],sz[maxn],sum[maxn],val[maxn],tmx[maxn],lmx[maxn],rmx[maxn];
int cnt,lazy_revise[maxn],lazy_reverse[maxn],rnd[maxn],top,aa[maxn],root;
void update(int x){
int l=son[x][0],r=son[x][1];
if(l && r){
sz[x]=sz[l]+sz[r]+1;
sum[x]=sum[l]+sum[r]+val[x];
tmx[x]=max(tmx[l],tmx[r]);
tmx[x]=max(tmx[x],rmx[l]+val[x]+lmx[r]);
lmx[x]=max(lmx[l],sum[l]+val[x]+lmx[r]);
rmx[x]=max(rmx[r],rmx[l]+val[x]+sum[r]);
}
else if(l){
sz[x]=sz[l]+1;
sum[x]=sum[l]+val[x];
tmx[x]=max(tmx[l],rmx[l]+val[x]);
lmx[x]=max(max(lmx[l],sum[l]+val[x]),0);
rmx[x]=max(0,val[x]+rmx[l]);
}
else if(r){
sz[x]=sz[r]+1;
sum[x]=sum[r]+val[x];
tmx[x]=max(tmx[r],lmx[r]+val[x]);
rmx[x]=max(rmx[r],sum[r]+val[x]);
rmx[x]=max(0,rmx[x]);
lmx[x]=max(0,lmx[r]+val[x]);
}
else{
sz[x]=1;
sum[x]=tmx[x]=val[x];
lmx[x]=rmx[x]=max(val[x],0);
}
}
int newnode(int k){
int x=0;
if(!trash.empty()){
x=trash.front();trash.pop();
}
else x=++cnt;
son[x][0]=son[x][1]=lazy_reverse[x]=0;
lazy_revise[x]=inf;
rnd[x]=rand();sz[x]=1;val[x]=sum[x]=tmx[x]=k;
lmx[x]=rmx[x]=max(k,0);return x;
}
int build(int *data,int k){
int nw=0,pre=0;top=0;
rep(i,1,k){
nw=newnode(data[i]);pre=0;
while(top && rnd[sta[top]]>rnd[nw]){
update(sta[top]);
pre=sta[top];
sta[top--]=0;
}
if(top) son[sta[top]][1]=nw;
son[nw][0]=pre;sta[++top]=nw;
}
while(top) update(sta[top--]);
return sta[1];
}
void doit(int x){
if(x==0) return;
trash.push(x);
doit(son[x][0]),doit(son[x][1]);
}
void change(int x,int k){
val[x]=k;sum[x]=sz[x]*k;
lmx[x]=rmx[x]=max(sum[x],0);
tmx[x]=max(sum[x],val[x]);
lazy_revise[x]=k;
}
void flip(int x){
swap(son[x][0],son[x][1]);
swap(lmx[x],rmx[x]);
lazy_reverse[x]^=1;
}
void pushdown(int x){
if(lazy_revise[x]!=inf){
if(son[x][0]) change(son[x][0],lazy_revise[x]);
if(son[x][1]) change(son[x][1],lazy_revise[x]);
}
if(lazy_reverse[x]!=0){
if(son[x][0]) flip(son[x][0]);
if(son[x][1]) flip(son[x][1]);
}
lazy_revise[x]=inf;lazy_reverse[x]=0;
}
void split(int &x,int &y,int k,int nw){
if(nw==0){x=y=0;return;}
pushdown(nw);
if(sz[son[nw][0]]>=k){
y=nw;split(x,son[nw][0],k,son[nw][0]);
}
else{
x=nw;split(son[nw][1],y,k-sz[son[nw][0]]-1,son[nw][1]);
}
update(nw);
}
int merge(int x,int y){
if(x) pushdown(x);if(y) pushdown(y);
if(!x || !y) return x+y;
if(rnd[x]<rnd[y]){
son[x][1]=merge(son[x][1],y);
update(x);return x;
}
else{
son[y][0]=merge(x,son[y][0]);
update(y);return y;
}
}
void ins(){
int pos=read(),len=read();
rep(i,1,len) aa[i]=read();
int nw=build(aa,len);
int x,y;
split(x,y,pos,root);
root=merge(merge(x,nw),y);
}
void del(){
int pos=read(),len=read();
int x,y,z,w;
split(x,y,pos-1,root);
split(z,w,len,y);
root=merge(x,w);
doit(z);
}
void upd(){
int pos=read(),len=read(),k=read();
int x,y,z,w;
split(x,y,pos-1,root);
split(z,w,len,y);
change(z,k);root=merge(x,merge(z,w));
}
void rev(){
int pos=read(),len=read();
int x,y,z,w;
split(x,y,pos-1,root);
split(z,w,len,y);
flip(z);root=merge(x,merge(z,w));
}
void calc1(){
int pos=read();
int len=read();
int x,y,z,w;
split(x,y,pos-1,root);
split(z,w,len,y);
printf("%d\n",sum[z]);
root=merge(x,merge(z,w));
}
void calc2(){
printf("%d\n",tmx[root]);
}
int main(){
srand(20020709);
n=read();m=read();
rep(i,1,n) a[i]=read();
root=build(a,n);
while(m--){
string s;cin>>s;
if(s[0]=='I') ins();
else if(s[0]=='D') del();
else if(s[0]=='M' && s[2]=='K') upd();
else if(s[0]=='R') rev();
else if(s[0]=='G') calc1();
else calc2();
}
return 0;
}