文艺平衡树
之前我用分块乱搞过文艺平衡树,今天一起来看看如何用平衡树实现文艺平衡树。
题目描述:
给你长度为 \(n\) 的序列 \(A\) ,初始时 \(A_i=i\) ,有 \(m\) 次操作,每次对给定区间 \([l,r]\) 进行翻转。
要求输出 \(m\) 次操作以后的序列。
实现文艺平衡树有三种思路:Splay,FHQ-Treap ,当然还有分块(划掉 。
因为我太弱,不会 FHQ-Treap 所以使用 Splay 来实现文艺平衡树。
Part 1 实现思路
由于我们需要输出元素反转后的顺序,而对元素本身的值不关心,这启示我们要对元素下标而不是元素的值建立 Splay 。
假设现在对元素下标建立了 Splay ,即这个 Splay 的中序遍历的第 \(i\) 个数代表 \(A_i\) 。
考虑如何进行“翻转”操作:想要翻转,先得找到翻转对象,这样才能有的放矢,假设要对区间 \([l,r]\) 进行翻转操作:
(图自皎月半撒花的博客,侵删)
可以把中序遍历第 \(l-1\) 个数伸展到根,第 \(r+1\) 个数伸展到根的右儿子(实际上就是第 \(l-1\) 名和第 \(r+1\) 名,利用 Splay 的查排名功能可以方便找到这两个节点),根据中序遍历 左->中->右 的性质,三角形所示子树就是翻转区间了。
把当前区间确定下来之后,我要开始进行翻转操作。按照中序遍历访问 Splay 的话,顺序是 左->中->右 。如果要翻转一个区间,顺序就要变成 右->中->左 。在不考虑变化遍历顺序的情况下,我们应该交换左右子树。即下图:
假如 u 及 u 的子树代表翻转区间,则需要把 u 及其子树内所有儿子的左右子树交换,不能只换 u ,道理很显然。但是如果我们每次都直接暴力交换 u 及 u 的子树的话,复杂度显然 \(O(n)\) 。
需要考虑优化,之前写分块和线段树的时候,曾经用到“打标记”的策略,每次修改只打标记,择机再下传这个标记。那么 Splay 可以打标记吗?
完全可以。
每次翻转操作,经过伸展后确定了反转区间构成的子树的根,把要翻转的子树的根节点打上一个标记,表示以这个节点为根的子树应该被翻转。在此之后,假如要旋转这个节点,就把翻转标记先下传到左右儿子,然后交换左右儿子,最后再旋转。和异或标记一样,如果一个区间翻转两次就相当于没有翻转,已经有标记的节点再打一个标记相当于没有标记。
至于其他操作,和普通 Splay 无二。
Part 2 Code
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<iomanip>
#include<cstring>
#include<utility>
#include<cstdio>
#include<queue>
#include<stack>
#include<ctime>
#include<cmath>
#include<list>
#include<set>
#include<map>
namespace zhang_tao{
#define inf 0x7f7f7f7f
#define ll long long
template <typename _T>
inline _T const& read(_T &x){
x=0;int fh=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')
fh=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-'0';
ch=getchar();
}
return x*=fh;
}
void write(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+48);
}
inline int _gcd(const int x,const int y){
return y?_gcd(y,x%y):x;
}
inline int _lcm(const int x,const int y){
return x*y/_gcd(x,y);
}
inline int max(int a,int b,int c=-inf){
return std::max(std::max(a,b),c);
}
inline int min(int a,int b,int c=inf){
return std::min(std::min(a,b),c);
}
// inline void swap(int &x,int &y){
// x^=y^=x^=y;
// }
} // namespace zhang_tao
using namespace zhang_tao;
int n,m;
struct Splay{
int value,tag,size;
Splay *son[2],*fa;
};
Splay *root,*null;
inline void swap(Splay* &x,Splay* &y){
Splay *tmp=x;x=y;y=tmp;
}//交换两个指针
inline void update(Splay *node){
node->size=node->son[0]->size+node->son[1]->size+1;
}//更新节点子树信息
inline bool get_which(Splay *node){
return node->fa->son[1]==node;
} //返回是左儿子还是右儿子
inline void push_down(Splay *node){
if(node->tag){
node->son[0]->tag^=1;
node->son[1]->tag^=1;//给两个儿子打标记
//其实这里应该特判,不然会修改null的标记,不过没啥影响就算了
node->tag=0;//清除自身标记
swap(node->son[0],node->son[1]);//交换左右子树
}//下传node节点的标记
}
void rotate(Splay *node){
Splay *fa=node->fa;
Splay *grandfa=fa->fa;
int which_son=get_which(node);
if(grandfa!=null)
grandfa->son[get_which(fa)]=node;
node->fa=grandfa;
fa->son[which_son]=node->son[which_son^1];
node->son[which_son^1]->fa=fa;
node->son[which_son^1]=fa;
fa->fa=node;
update(fa),update(node); //和普通Splay的rotete()函数一样
}
void splay(Splay *node,Splay *target){
while(node->fa!=target){
Splay *fa=node->fa;
Splay *grandfa=fa->fa;
if(grandfa!=target)
rotate( get_which(fa)^get_which(node) ? node : fa );
rotate(node);
}
if(target==null)
root=node;
}//和普通Splay的splay()函数一样
void insert(int x){
Splay *node=root,*fa=null;
while(node!=null){
fa=node;
node=node->son[x>node->value];
}
node=new Splay;
node->fa=fa;
if(fa!=null) fa->son[x>fa->value]=node;
node->son[0]=node->son[1]=null;
node->tag=0;
node->size=1;
node->value=x;
splay(node,null);
}//和普通Splay的insert()函数一样
//这里可以 O(n) 直接拔起一棵Splay来,但是我比较懒,直接insert了
Splay* get_kth(int k){
Splay *node=root;
while(1){
push_down(node);//这里下传标记,之后伸展的时候就不用下传了
if(node->son[0]->size>=k)
node=node->son[0];
else{
k-=(node->son[0]->size+1);
if(k==0) return node;
else node=node->son[1];//剩下的和普通查k小一样
}
}
}
void reverse(int L,int R){
Splay *pre=get_kth(L),*back=get_kth(R+2);
//实际上找L-1,R+1,因为把n+1当成n,变成了查L和R+2
splay(pre,null),splay(back,pre);
root->son[1]->son[0]->tag^=1;
}//上述方法伸展、打标记
void output(Splay *node){
push_down(node);//别忘了下传标记
if(node->son[0]!=null) output(node->son[0]);
if(node->value>1 && node->value<n+2) write(node->value-1),putchar(' ');
//因为多插入了两个点,输出值-1
if(node->son[1]!=null) output(node->son[1]);
}//中序遍历输出
signed main(){
null=new Splay;
null->fa=null->son[0]=null->son[1]=NULL;
null->size=null->tag=null->value=0;//初始化null节点
root=null;
read(n),read(m);
for(int i=1;i<=n+2;++i)
insert(i);
//如果反转的是[1,n]的话,需要找到1的前驱,n的后继,这里多插入两个点,然后把n+1当成n,不然会RE
for(int i=1,l,r;i<=m;++i){
read(l),read(r);
reverse(l,r);//翻转
}
output(root);//从根中序遍历输出
return 0;
}
Part 3 后记
这写的啥啊,自己都看不懂。
没看明白并且毫无帮助?那就对了。