可以支持的操作:
- 插入 xxx 数
- 删除 xxx 数(若有多个相同的数,因只删除一个)
- 查询 xxx 数的排名(排名定义为比当前数小的数的个数 +1 。若有多个相同的数,因输出最小的排名)
- 查询排名为 xxx 的数
- 求 xxx 的前驱(前驱定义为小于 xxx ,且最大的数)
- 求 xxx 的后继(后继定义为大于 xxx ,且最小的数)
7.来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1
0.预备:
struct有:siz,sum(该点出现次数),fa,val,以及ch[0],ch[1]
const int N=100000+10; const int inf=0x3f3f3f3f; int n,m; struct node{ int siz,sum,val,fa; int ch[2]; }t[N]; int pc,dc,dp[N]; int root;
1.pushup,新节点,回收节点
void pushup(int x){ t[x].siz=t[t[x].ch[0]].siz+t[t[x].ch[1]].siz+t[x].sum; } int newnode(int v,int f){ int r=dc?dp[dc--]:++pc; memset(t+r,0,sizeof (node)); t[r].siz=1,t[r].sum=1,t[r].fa=f,t[r].val=v; return r; } void del(int x){ dp[++dc]=x; }
2.rotate(x),与父亲旋转。注意每一步的转移,注意最后pushup(y)
void rotate(int x){ int y=t[x].fa,d=t[y].ch[1]==x; t[t[y].ch[d]=t[x].ch[d^1]].fa=y; t[t[x].fa=t[y].fa].ch[t[t[y].fa].ch[1]==y]=x; t[x].ch[d^1]=y,t[y].fa=x; pushup(y); }
3.splay(x,f),旋转到f的儿子,双旋操作,一次考虑两个节点。注意,无论如何,rotate(x),无论如何,pushup(x),如果f=0,root=x
void splay(int x,int f){ while(t[x].fa!=f){ int y=t[x].fa,z=t[y].fa; if(z!=f){ rotate(((t[z].ch[0]==y)==(t[y].ch[0]==x))?y:x); } rotate(x); } pushup(x); if(f==0) root=x; }
4.insert(val),注意找到相同的值之后,break掉,注意每次都要splay到根(虽然数据水,不旋更快),理论保证树高。
void insert(int val){ if(!root){ root=newnode(val,0); return; } int u=root; while(1){ t[u].siz++; if(t[u].val==val){ t[u].sum++; break; } int d=t[u].val<val; if(!t[u].ch[d]){ t[u].ch[d]=newnode(val,u); u=t[u].ch[d]; break; } u=t[u].ch[d]; } //splay(u,0); }
5.dele(val),步骤:先找到val,旋转到根,再找到根节点右子树最小值,即root的后继,splay到根的儿子(必然是右儿子),再删掉root,改root为右儿子
需要特判:右子树不存在?直接将左二子当做新根。删掉的是最后一个值?我这个代码不怕,root会直接变成0
void dele(int val){ int goal=root,son=0; while(1){ if(t[goal].val==val) break; goal=t[goal].ch[t[goal].val<val]; } t[goal].sum--; splay(goal,0); t[goal].siz--; if(t[goal].sum>0) return; son=t[goal].ch[1]; while(son&&t[son].ch[0]){ son=t[son].ch[0]; } if(son){ splay(son,root); t[son].ch[0]=t[root].ch[0]; t[t[root].ch[0]].fa=son; del(root); root=son; t[son].fa=0; pushup(son); } else{ root=t[goal].ch[0]; t[root].fa=0; del(goal); } }
6.rank(x),同treap
7.kth(k),同treap
8.front(x),同treap
9.back(x),同treap
int rank(int val){ int u=root; int ret=1; while(u){ if(t[u].val<val) { ret+=t[t[u].ch[0]].siz+t[u].sum; u=t[u].ch[1]; } else if(t[u].val==val) {ret+=t[t[u].ch[0]].siz;break;} else u=t[u].ch[0]; } return ret; } int kth(int k){ int u=root; while(1){ if(!u) return 0; int d=k-t[t[u].ch[0]].siz; if(d<=0) u=t[u].ch[0]; else if(d>=1&&d<=t[u].sum) return t[u].val; else { k=d-t[u].sum; u=t[u].ch[1]; } } } int front(int val){ int u=root; int ret=-inf; while(u){ if(t[u].val<val){ ret=max(ret,t[u].val); u=t[u].ch[1]; } else { u=t[u].ch[0]; } } return ret; } int back(int val){ int u=root; int ret=inf; while(u){ if(t[u].val>val){ ret=min(ret,t[u].val); u=t[u].ch[0]; } else{ u=t[u].ch[1]; } } return ret; }
10.pushdown(x),翻转其实就是不停地交换节点的两个子树。因为在每次访问到这个节点的时候,尤其在splay的时候,必然会启动pushdown,所以,翻转最终一定会都实现。下放旋转标记,经常勤调用,要记得。
11.work(l,r),处理旋转操作,这里,为了避免l=1,r=n的情况,放置1,n+2两个哨兵,每个节点的编号实际上是编号减一。最后输出要减一。
这个方法是针对1~n的排列,如果不是,最好是特判,其实加哨兵也是可以的。
实现的时候,先找到kth(l)也就是第k大的值,体现在中序遍历里就是这个点的编号。当然,其实找的是l-1,但是由于编号是从2开始的。
将l旋转到根。
同理,r=kth(r+2)。
将r旋转到根节点的儿子(必然是右儿子)
这样,根节点的右儿子的左子树就是所求区间。
12.write(o),记得pushdown,输出的编号一定要在[2,n+1]内,因为1,n+2都是哨兵。按照中序遍历输出即可
对于一个需要维护区间操作的平衡树来说,每个节点的排序方式是编号大小,也就是中序遍历的节点编号一定是一个从1~n的单增序列。保证可以取l-1到根节点,r+1到根节点的儿子,就可以取出[l,r]了。
注意事项:
1.rotate里面的压行,不要记错,不行就手写。其实,第一行换儿子,第二行改父亲,第三行改两点之间的关系。
2.插入元素的时候,如果用while1循环,记得每到一个节点, 那么这个节点的总的siz必然要加一,不论是新赋值,还是多了一个新值。
3.插入元素的时候,如果找到了这个值,那么就要break,不能继续走了。
4.删除的时候找右子树中的最大值,为了防止哨兵出了问题,应改写为:
while(son&&t[son].ch[0]){ son=t[son].ch[0]; }
5.区间反转的时候,kth,write,以及各种访问到这个点的时候,都要pushdown