平衡树2---splay

目录

splay是如何保证高度是logn?

那么它是如何把某个点旋转到树根的呢?

splay的操作 

splay如何去维护信息?


splay和set、map中的红黑树都是一类平衡树,在所有平衡树中,treap好写效率高但局限性较高,红黑树比较难写,在竞赛中,最常用的还是splay,它的特点是:代码不算太长,非常灵活,支持很多操作,除了平衡树的几个基本操作,splay还可以支持对区间操作

splay 是一棵平衡二叉树,有左旋右旋操作

左旋右旋不会改变原树的中序遍历,同时可以调整树的高度

 

splay是如何保证高度是logn?

维护方式:某一次对某一个点操作,如插入查询操作,操作完之后都将此节点变到树根

那么它是如何把某个点旋转到树根的呢?

 splay(x,k)函数表示将点x旋转至点k下面(如果k为0表示将x旋转到根)

将旋转情况分为两类(每一类都有对称情况,不一一列举):

一:

二:

 

splay的操作 

 插入操作:

1.插到尾部,直接插入,再旋转到根节点维护

2.将一个序列插到y的后面

【1】找到y的后继,设为z

【2】将y转到根

【3】将z转到splay的下面

【4】将要插入的序列接到z的左子树上(z是y的后继,所以旋转后z的左子树为空,可以直接接上

删除操作:

删除[L,R]的一段

【1】找到 L - 1 和 R + 1

【2】将L - 1转到根节点

【3】将R + 1转到根节点下面

【4】将R + 1左子树置成空

 其他操作如找前驱后继同其他平衡树一样

splay如何去维护信息?

以一道例题来引入:

题目来源:Acwing算法进阶课模板题

活动 - AcWing

题目概述:将区间中某一段翻转

我们要实现翻转,需要找到第k个数,所以需要维护一个size,每次需要翻转一个区间,需要类似线段树,维护一个懒标记

size 的维护通过 push up 实现(size可以代表一类信息,如总和都是用push up实现)

懒标记 通过pushdown 实现,将懒标记下传

细节都注释在代码中

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>

using namespace std;
const int N = 100010;

int n,m;
struct node
{
	int s[2],p,v;//左右儿子,父节点,编号
	int size,flag;//子树大小,懒标记
	
	void init(int _v,int _p)
	{
		v = _v,p = _p;
		size = 1;
	}
}tr[N];
int root,idx;//地址,指针

void pushup(int x)
{
	tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + 1;
}

void pushdown(int x)
{
	if(tr[x].flag)
	{
		swap(tr[x].s[0],tr[x].s[1]);
		tr[tr[x].s[0]].flag ^= 1;
		tr[tr[x].s[1]].flag ^= 1;
		tr[x].flag = 0;
	}
}

void rotate(int x)//通过判断语句将左旋右旋统一为一种写法
{
	int y = tr[x].p,z = tr[y].p;
	int k = tr[y].s[1] == x;//k表示为左儿子0还是右儿子1
    //以一种情况考虑就行,需要变三条边的关系
    /*以上方旋转图右边为例
        先变x与z的关系,将z的儿子变为x,x的父节点变为z
        再变y和B之间的边,y的左儿子(B)等于x的右儿子,x的右儿子的父节点为y
        最后变x和y之间的关系,x的右儿子等于y,y的父节点等于x
    */
	tr[z].s[tr[z].s[1] == y] = x,tr[x].p = z;
	tr[y].s[k] = tr[x].s[k^1],tr[tr[x].s[k^1]].p = y;
	tr[x].s[k^1] = y,tr[y].p = x;
	pushup(y),pushup(x);
}

void output(int u)//输出中序遍历
{
	pushdown(u);//有递归就需要pushdown
	if(tr[u].s[0])
	output(tr[u].s[0]);
	if(tr[u].v >= 1 && tr[u].v <= n)
	printf("%d ",tr[u].v);
	if(tr[u].s[1])
	output(tr[u].s[1]);
}

void splay(int x,int k)
{
	while(tr[x].p != k)
	{
		int y = tr[x].p,z = tr[y].p;
		if(z != k)
		{
            //判断是否是折线关系,一个是0一个是1为折线,否则……
			if((tr[y].s[1] == x) ^ (tr[z].s[1] == y))
			rotate(x);
			else
			rotate(y);
		}
		rotate(x);
	}
	if(!k)
	root = x;
}

void insert(int v)
{
	int u = root,p = 0;
	while(u)//应该插到哪个点
	p = u,u = tr[u].s[v > tr[u].v];//v小于当前点插入左儿子,返回0,反之右儿子,返回1
	u = ++idx;//新分配一个点
	if(p)
	tr[p].s[v > tr[p].v] = u;
	tr[u].init(v,p);
	splay(u,0);
}

int get_k(int k)
{
	int u = root;
	while(true)
	{
		pushdown(u);
		if(tr[tr[u].s[0]].size >= k)
		u = tr[u].s[0];
		else if(tr[tr[u].s[0]].size + 1 == k)
		return u;
		else k -= tr[tr[u].s[0]].size + 1,u = tr[u].s[1];
	}
	return -1;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i = 0; i <= n + 1; i ++)//防止越界,插入两个哨兵
	insert(i);
	while(m --)
	{
		int l,r;
		scanf("%d%d",&l,&r);
        //找第l - 1个数和第r + 1个数
		l = get_k(l),r = get_k(r + 2);
        //左端点转到根,右端点到根节点下面
		splay(l,0),splay(r,l);
		tr[tr[r].s[0]].flag ^= 1;
	}
	output(root);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值