SPLAY,LCT学习笔记(一)

写了两周数据结构,感觉要死掉了,赶紧总结一下,要不都没学明白。

SPLAY专题:

例:NOI2005 维修数列

典型的SPLAY问题,而且综合了SPLAY常见的所有操作,特别适合新手入门学习(比如我这种蒟蒻)

题目要求很多,我们一步一步来分析

首先,区间翻转是SPLAY一个很基础的操作,我们以他为基础分析这个SPLAY

例:luogu文艺平衡树

题目:读入一个序列,进行多次区间翻转操作,请你输出操作后的序列

我们怎么处理呢?

首先要明确一点,就是SPLAY是可以当做区间树来使用的(就像线段树一样)

所以,我们首先把原来读进来的序列建起一个SPLAY,建树方法类似于线段树

void buildtree(int l,int r,int f)
{
	int mid=(l+r)>>1;
	if(l==r)
	{
		tree[l].val=a[l];
		tree[l].huge=1;
		tree[l].ttag=0;
		tree[l].fa=f;
		tree[l].lson=tree[l].rson=0;
	}
	if(l<mid)
	{
		buildtree(l,mid-1,mid);
	}
	if(r>mid)
	{
		buildtree(mid+1,r,mid);
	}
	tree[mid].val=a[mid];
	tree[mid].fa=f;
	tree[mid].ttag=0;
	update(mid);
	if(mid<f)
	{
		tree[f].lson=mid;
	}else
	{
		tree[f].rson=mid;
	}
}

注意到其中有一个ttag,这是翻转区间的标记,暂时先不用关心

这样我们就建起了一个SPLAY

然后我们把SPLAY的根置成整个区间的中点即可

还有一个要点,就是SPLAY中一个节点既是序列中的一个位置,也是树中一个根节点,换言之,如果你建立了一棵SPLAY,根节点对应的是原序列中4位置,那么他的左子树就要维护1-3位置,右子树要维护5-7位置,而4位置交给根节点自己,不能放进子树中

这也是他与线段树少数的区别。

所以如果我们有一个序列1,2,3,4,5,6,7

建起的SPLAY大概会长这个样子

接下来,我们来尝试做一下区间翻转:

假设我要翻转区间[2,4],我要怎么办呢?

现在我希望找到一棵子树,使得这棵子树正好对应了[2,4]这个区间,然后我们把这个区间整体打上标记就可以了

怎么找?

这才是SPLAY真正要解决的问题

最后我们得出了一个结论:假设我们要翻转区间[2,4],那么我们首先把1转到SPLAY的根上(别忘了,这是一棵伸展树,是可以旋转的)

那么旋转规则和treap类似,最后大概长这样吧:

至于怎么转的,暂时还不必关心,一会就提到

接下来,我们把5节点旋转到根节点的右儿子处,长这样:

 (原谅本蒟蒻的画图技术)

发现什么了吗?

当5旋转到右子节点以后,你所需要的区间[2,4]自然出现在了5的左子树上!

这样我们只对这棵树打一个标记就好

总结:如果我想修改区间[l,r],那么我只需将节点l-1转至SPLAY的根节点上,将节点r+1转至根节点的右儿子上,那么这个右儿子的左子树就是你想要的区间[l,r]!

至于证明,由于SPLAY本质是一个二叉搜索树,所以要满足搜索树的性质(这也是所有基于二叉搜索树变形出的数据结构所有旋转,伸展等等诡异操作的原理)

而搜索树的性质:左子树<根<右子树

那就好办啦,把要修改的区间前驱放到根上,这样整个区间一定在根的右子树内

再把区间的后继放到根的右子树上,这样整个区间一定在这个节点的左子树内

这样不就分出来这段区间了吗。

等等,这里面是不有点问题?

是啊,如果l=1呢?

这样的话,l-1就为0了,而在SPLAY里,找的到这样一个点吗?

似乎找不到诶

或者,如果r=n的话,那r+1就等于n+1了,好像同样不存在诶

那我们怎么玩?

一句话:插入两个空节点!

为什么呢?

假如我们将整个区间由[1,n]变为[2,n+1],然后加入1和n+2两个空节点,这样不就可以实现上面所说的所有要求了吗?

这就很妙啦

于是,我们来解决一下最后这个问题:怎么旋转?

结论如下(这里使用双旋splay,常数小一些)

void rotate(int st,int &ed)
{
	int ltyp;
	int v1=tree[st].fa;
	int v2=tree[v1].fa;
	if(tree[v1].rson==st)
	{
		ltyp=1;
	}else
	{
		ltyp=0;
	}
	if(v1==ed)
	{
		ed=st;
	}else
	{
		if(tree[v2].lson==v1)
		{
			tree[v2].lson=st;
		}else
		{
			tree[v2].rson=st;
		}
	}
	if(ltyp)
	{
		tree[tree[st].lson].fa=v1;
		tree[v1].fa=st;
		tree[v1].rson=tree[st].lson;
		tree[st].lson=v1;
		tree[st].fa=v2;
	}else
	{
		tree[tree[st].rson].fa=v1;
		tree[v1].fa=st;
		tree[v1].lson=tree[st].rson;
		tree[st].rson=v1;
		tree[st].fa=v2;
	}
	update(v1);
	update(st);
}
void splay(int st,int &ed)
{
	while(st!=ed)
	{
		int v1=tree[st].fa,v2=tree[v1].fa;
		if(v1!=ed)
		{
			if((tree[v1].lson==st&&tree[v2].lson!=v1)||(tree[v1].rson==st&&tree[v2].rson!=v1))
			{
				rotate(st,ed);
			}else
			{
				rotate(v1,ed);
			}
		}
		rotate(st,ed);
	}
}

双旋,按类似treap的方法反复旋转即可

最后,每次操作的时候都需要下传标记,然后想输出的话对这棵树进行中序遍历即可

全代码(文艺平衡树):

#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
#define ls tree[rt].lson
#define rs tree[rt].rson
using namespace std;
struct SPLAY
{
	int lson;
	int rson;
	int val;
	int fa;
	bool ttag;
	int huge;
}tree[100005];
int a[100005];
int rot;
int n,m;
void update(int rt)
{
	tree[rt].huge=tree[ls].huge+tree[rs].huge+1;
}
void pushdown(int rt)
{
	if(tree[rt].ttag)
	{
		tree[rt].ttag=0;
		tree[ls].ttag^=1;
		tree[rs].ttag^=1;
		swap(tree[ls].lson,tree[ls].rson);
		swap(tree[rs].lson,tree[rs].rson);
	}
}
void buildtree(int l,int r,int f)
{
	int mid=(l+r)>>1;
	if(l==r)
	{
		tree[l].val=a[l];
		tree[l].huge=1;
		tree[l].ttag=0;
		tree[l].fa=f;
		tree[l].lson=tree[l].rson=0;
	}
	if(l<mid)
	{
		buildtree(l,mid-1,mid);
	}
	if(r>mid)
	{
		buildtree(mid+1,r,mid);
	}
	tree[mid].val=a[mid];
	tree[mid].fa=f;
	tree[mid].ttag=0;
	update(mid);
	if(mid<f)
	{
		tree[f].lson=mid;
	}else
	{
		tree[f].rson=mid;
	}
}
int findf(int rt,int v)
{
	pushdown(rt);
	if(tree[ls].huge+1==v)
	{
		return rt;
	}else if(tree[ls].huge>=v)
	{
		return findf(ls,v);
	}else
	{
		return findf(rs,v-1-tree[ls].huge);
	}
}
void rotate(int st,int &ed)
{
	int ltyp;
	int v1=tree[st].fa;
	int v2=tree[v1].fa;
	if(tree[v1].rson==st)
	{
		ltyp=1;
	}else
	{
		ltyp=0;
	}
	if(v1==ed)
	{
		ed=st;
	}else
	{
		if(tree[v2].lson==v1)
		{
			tree[v2].lson=st;
		}else
		{
			tree[v2].rson=st;
		}
	}
	if(ltyp)
	{
		tree[tree[st].lson].fa=v1;
		tree[v1].fa=st;
		tree[v1].rson=tree[st].lson;
		tree[st].lson=v1;
		tree[st].fa=v2;
	}else
	{
		tree[tree[st].rson].fa=v1;
		tree[v1].fa=st;
		tree[v1].lson=tree[st].rson;
		tree[st].rson=v1;
		tree[st].fa=v2;
	}
	update(v1);
	update(st);
}
void splay(int st,int &ed)
{
	while(st!=ed)
	{
		int v1=tree[st].fa,v2=tree[v1].fa;
		if(v1!=ed)
		{
			if((tree[v1].lson==st&&tree[v2].lson!=v1)||(tree[v1].rson==st&&tree[v2].rson!=v1))
			{
				rotate(st,ed);
			}else
			{
				rotate(v1,ed);
			}
		}
		rotate(st,ed);
	}
}
int split(int st,int ed)
{
	int v1=findf(rot,st-1);
	int v2=findf(rot,ed+1);
	splay(v1,rot);
	splay(v2,tree[v1].rson);
	return tree[v2].lson;
}
void reverse(int st,int ed)
{
	int v1=split(st,ed);
	tree[v1].ttag^=1;
	swap(tree[v1].lson,tree[v1].rson);
	update(tree[v1].fa);
	update(tree[tree[v1].fa].fa);
} 
void print(int rt)
{
	if(!rt)
	{
		return;
	}
	pushdown(rt);
	print(ls);
	printf("%d ",tree[rt].val);
	print(rs);
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		a[i+1]=i;
	}
	buildtree(1,n+2,0);
	rot=(n+3)>>1;
	for(int i=1;i<=m;i++)
	{
		int st,ed;
		scanf("%d%d",&st,&ed);
		reverse(st+1,ed+1);
	}
	int v1=split(2,n+1);
	print(v1);
	return 0;
}

这样SPLAY的基础知识就完成了

剩下的留待下一篇更新

可持久化splay是一种数据结构,它是对splay树进行修改和查询的一种扩展。在传统的splay树中,对树的修改操作会破坏原有的树结构,而可持久化splay树则允许我们对树进行修改、查询,并且可以保存修改后的每个版本的树结构。 在可持久化splay树中,我们不会直接对原树进行修改,而是通过复制每个节点来创建新的版本。这样,每个版本都可以独立地修改和查询,保留了原有版本的结构和状态。每个节点保存了其左子树和右子树的引用,使得可以在不破坏原有版本的情况下进行修改和查询。 为了实现可持久化splay树,我们可以使用一些技巧,比如引用中提到的哨兵节点和假的父节点和孩子节点。这些技巧可以帮助我们处理根节点的旋转和其他操作。 此外,可持久化splay树还可以与其他数据结构相结合,比如引用中提到的可持久化线段树。这种结合可以帮助我们解决更复杂的问题,比如区间修改和区间查询等。 对于可持久化splay树的学习过程,可以按照以下步骤进行: 1. 理解splay树的基本原理和操作,包括旋转、插入、删除和查找等。 2. 学习如何构建可持久化splay树,包括复制节点、更新版本和保存历史版本等。 3. 掌握可持久化splay树的常见应用场景,比如区间修改和区间查询等。 4. 深入了解与可持久化splay树相关的其他数据结构和算法,比如可持久化线段树等。 在解决问题时,可以使用二分法来确定答案,一般称为二分答案。通过对答案进行二分,然后对每个答案进行检查,以确定最终的结果。这种方法可以应用于很多问题,比如引用中提到的在线询问问题。 综上所述,可持久化splay是一种对splay树进行修改和查询的扩展,可以通过复制节点来创建新的版本,并且可以与其他数据结构相结合解决更复杂的问题。学习过程中可以按照一定的步骤进行,并且可以使用二分法来解决一些特定的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [[学习笔记]FHQ-Treap及其可持久化](https://blog.csdn.net/weixin_34283445/article/details/93207491)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [可持久化数据结构学习笔记](https://blog.csdn.net/weixin_30376083/article/details/99902410)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值