【模板】文艺平衡树 无旋FHQ Treap

题面链接
※本题思路来自他人启发,并非个人思路还有这是个魔鬼模板题吧!唉,是我太菜了


最开始…

啥都没想到,请教LJZ大神,又听老师讲解了一下,恍然大悟。


正文开始

按题目推荐的做法,就用平衡树。由于本题要求的操作是区间翻转,考虑到普通Treap的旋转令我很是尴尬:
例如序列1 2 3 4 5,区间翻转1~4变为4 3 2 1 5
无法旋转
能做到的大佬请在评论区留言,教教本蒟蒻!!!


区间翻转在树上操作的特性

所以普通Treap想不出,就用不需要旋转的FHQ TreapSplay我还没学会
由于平衡树满足BST的性质,在这个翻转的序列里,只有位置是不变的,位置上的数是变化的,所以我们要维护位置的有序性,而不是维护数字的有序性。
所以可以想到,大概要实现这样的操作:
序列1 2 3 4 5,1~5翻转得到5 4 3 2 1(红色是位置,蓝色是数值)
期望做法
仔细观察,可以发现,翻转的本质应是交换左右儿子,正如这一个全序列翻转,只要将树中所有根结点的左右儿子交换,新树的中序遍历就是答案。再多画几个图,我们可以发现,如果有一棵BST按上面的规定存起翻转的区间,并交换树中每个根结点的左右儿子,这个区间就翻转成功。


初步想法

由上面可以想到一个初步做法:数组装起初始序列,每一次给出一个区间,将这一区间的数字装进一棵平衡树,将树中所有根节点的左右儿子交换,通过中序遍历更新这一区间的序列。
算法示意图

[ L , R ] [L,R] [L,R]的区间中,给平衡树插入 R − L R-L RL个数(估算: Θ ( log ⁡ ( R − L + 1 ) ) \Theta (\log (R-L+1)) Θ(log(RL+1)),最坏结果 Θ ( log ⁡ n ) \Theta (\log n) Θ(logn)),又交换所有根结点的左右儿子(估算:最坏结果 Θ ( n 2 ) \Theta (\frac{n}{2}) Θ(2n) Θ ( n ) \Theta (n) Θ(n)),并中序遍历一次(估算: O ( n ) O(n) O(n)),更新一次序列(估算: O ( n ) O(n) O(n)),估算一下,一次翻转大概是 O ( log ⁡ n + n ) O(\log n + n) O(logn+n)em…大概是这样吧… m m m次操作下来就大概是 O ( log ⁡ n m + n m ) O(\log nm + nm) O(lognm+nm)本蒟蒻不是很会算,如果有错敬请指正
然而:

【数据范围】
对于 100% 的数据, 1 ≤ n , m ≤ 100000 , 1 ≤ l ≤ r ≤ n 1 \le n, m \leq 100000,1 \le l \le r \le n 1n,m1000001lrn

爆了!爆了!


数据结构优化

首先,为了避免每次建树花费的大量时间,我们可以只建一棵树,在每一次操作的时候,将这棵树用FHQ Treap的核心操作Split将原树拆成三棵树:储存着区间 [ 1 , L − 1 ] [1,L-1] [1,L1], [ L , R ] [L,R] [L,R], [ R − 1 , n ] [R-1,n] [R1,n],只对 [ L , R ] [L,R] [L,R]这棵树操作就行了 (当时天真的我认为一个Merge就能打天下,结果碰上Split把我急了半天,最后又被自己打脸,发现Merge真的可以打天下,因为我徒手敲了个基于Merge的Split,哈哈哈) 操作完以后再用FHQ Treap的核心操作Merge把这三棵树再次合并,就得到新序列了。

为了减小我们的操作时间,看这个区间,看这个翻转,是不是有种线段树的感觉?为了节省翻转所需的时间,我们不妨继承线段树的优点:Tag。当时想到这里的我,还是一脸蒙蔽,打标记?真的有用吗?怎么用标记啊?

我们不妨取出翻转区间,给此区间的根结点打上标记,如果再次访问到这一结点就对它的左右儿子进行翻转,并把标记下传给它的儿子。这样我们就不用每次都翻转,在最后输出答案时一次性把剩下该翻转的都反转了,就能大大减小时间复杂度了。

示意图
文字表述和图画表述还不是很方便、清晰啊!大家尽力理解一下吧!
更多具体解读详看代码注释!


代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<ctime>

using namespace std;

int n,leftn,rightn,m;

struct node{
	node *lson,*rson;
	int pri;//优先级
	int siz;//树的大小
	int val;//数值
	int mark;//翻转标记
	void getsize()//获取树的大小 
	{
		siz = 1;
		if(lson) siz += lson->siz;
		if(rson) siz += rson->siz;
	}
	void passmark()//下传标记
	{
		mark ^= 1;
		if(lson) lson->mark ^= 1;
		if(rson) rson->mark ^= 1;
	}
}*root , mem[23333333],  *now = mem;

node *newnode(int num)//新建结点
{
	node *tmp = now++;
	tmp->lson = tmp->rson = NULL;
	tmp->val = num;
	tmp->pri = rand();
	tmp->siz = 1;
	tmp->mark = 0;
	return tmp;
}

void Reverse(node *&T)//翻转
{
	//等价于swap(T->lson,T->rson);
	node *tmp = T->lson;
	T->lson = T->rson;
	T->rson = tmp;
	T->passmark();//记得下传标记
	return ;
}

node *Merge(node *L,node *R)//核心操作Merge
{
	if(L == NULL) return R;
	if(R == NULL) return L;
	if(L->pri > R->pri)
	{
		if(L->mark) Reverse(L);//反转操作 
		L->rson = Merge(L->rson,R);
		L->getsize();
		return L;
	}
	else{
		if(R->mark) Reverse(R);//反转操作 
		R->lson = Merge(L,R->lson);
		R->getsize();
		return R;
	}
}

node *Insertx(node *&p,int num)//基于Merge的Insert操作
{
	if(p == NULL)
	{
		p = newnode(num);
		return p;
	}
	if(num < p->val)
	{
		node *newson = Insertx(p->lson,num);
		p->lson = NULL;
		p->getsize();
		return Merge(newson,p);
	}
	if(num > p->val)
	{
		node *newson = Insertx(p->rson,num);
		p->rson = NULL;
		p->getsize();
		return Merge(p,newson);
	}
}
//基于Merge的Split操作
//徒手敲时,为防止出错,详细写了一遍注释
void Split(node *&T,int k,node *&newt/*new tree*/,node *&remt/*remain tree*/)
//把一棵树,拆成由前k个数组成的树和剩下部分 
{
	int lsize = 0;
	if(T->mark) Reverse(T);反转操作(先)
	if(T->lson) lsize = T->lson->siz;//获取左子树大小用于计算排名(后)
	if(lsize >= k)//左子树大小比k大,第k个数一定在左子树里面
	{
		if(T->lson) Split(T->lson , k , newt, remt);//判断它有左子树,才在左子树拆树 
		T->lson = NULL;
		remt = Merge(remt,T);//将其本身和右子树并入右树 
		if(remt) remt->getsize();//记得更新子树大小,小心空指针
		return ;
	} 
	if(lsize + 1 < k)//在新树范围内,但还没到最后一个结点
	{
		if(T->rson)	Split(T->rson,k - lsize - 1,newt,remt);//在右子树继续拆树 
		T->rson = NULL; 
		//因为已经在右子树拆过树,newt中的所有结点都来自于T右子树,又根据BST性质,所以newt中的所有的值 都比 当前没有右儿子的T中的所有的值要大 
		newt = Merge(T ,newt );//把其本身左子树全部合并
		if(newt) newt->getsize();//记得更新子树大小
		return ;
	} 
	if(lsize + 1 == k)//找到最后一个结点拆树到此为止
	{
		node *tmp = T->rson;
		T->rson = NULL;
		newt = Merge(T,newt);//左儿子和它本身并入新树
		if(newt) newt->getsize(); //记得更新子树大小
		remt = Merge(remt,tmp);//右儿子并入旧树 
		if(remt) remt->getsize();//记得更新子树大小
		return ;
	} 
}

void travel(node *p)//中序遍历
{
	if(p->mark) Reverse(p);
	if(p->lson) travel(p->lson);
	cout<<p->val<<' ';
	if(p->rson) travel(p->rson);
}

int main()
{
	srand((unsigned)time(NULL));
	node *root = NULL*root1 = NULL*root2 = NULL*root3 = NULL*root4 = NULL ;
	cin >> n >> m;
	for(int i=1;i<=n;i++) root = Insertx(root , i);//插入原始序列
	for(int i=1;i<=m;i++)
	{
		root1 = root2 = root3 = root4 = NULL;//初始化
		cin >> leftn >> rightn;
		if(leftn != 1 && rightn != n)//emm....由于赶时间,直接特判
		{
			Split(root,leftn-1,root1,root2);//[1,n]分成[1,L-1],[L,n]
			Split(root2,rightn-leftn+1,root3,root4);//[L,n]分成[L,R],[R+1,n]
			root3->mark = 1;
			root = Merge(root1,root3);//合并回去
			root = Merge(root,root4);
		}
		else 
		{
			if(leftn == 1 && rightn == n)
			{
				root->mark = 1;
				Reverse(root);//反转操作
			}
			else
			{
				if(leftn == 1)
				{
					Split(root,rightn,root1,root2);
					root1->mark = 1;
					root = Merge(root1,root2);
				}
				else if(rightn == n)
			 	{
			 		Split(root ,leftn-1,root1,root2);
			 		root2->mark = 1 ;
			 		root = Merge(root1,root2);
				}
			}
		}
	}
	travel(root);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值