文艺平衡树

文艺平衡树

之前我用分块乱搞过文艺平衡树,今天一起来看看如何用平衡树实现文艺平衡树。

题目描述:

给你长度为 \(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 后记

这写的啥啊,自己都看不懂。

没看明白并且毫无帮助?那就对了。

上一篇:字典树 检索字符串 维护异或极值


下一篇:【题解】bzoj3252 攻略