zoukankan      html  css  js  c++  java
  • 指针实现 Treap

    前置知识:二叉排序树,堆。

    应用场景:平衡树。


    〇、导入

    1. 二叉排序树

    我们都知道,二叉排序树就是满足“(lch<now<rch)”(左儿子小于根节点,右儿子大于根节点)的二叉树,一般情况下插入、删除和搜索的时间复杂度都为 (Theta(log n)) ,非常快。但在特殊情况下,二叉排序树可能会退化成链,时间复杂度也会变为 (Theta(n)) 。只有当二叉排序树平衡时速度最优。

    2. 堆

    堆也很简单,就是根节点大于子节点的完全二叉树。

    3. 平衡树

    当二叉排序树退化成链时速度会大打折扣,因此很多毒瘤出题者都喜欢卡它。但是既然能有这种题目,那么肯定就有能解决的方法,当然也不排除出题者自己都没有做出来。二叉排序树平衡时时间复杂度为 (Theta(log n)) 。所以,若要保证二叉排序树的最大时间复杂度为 (Theta(log n)) ,就要保证该二叉排序树平衡。这就是是平衡树。

    平衡树有很多种,本文就不再赘述,仅讨论 Treap

    4. Treap

    二叉排序树和堆都是二叉树。既然堆就是完全二叉树,何不利用它这个性质来保证二叉排序树平衡呢?

    于是 Tree(二叉排序树)+Heap(堆)=TreapTreap 横空出世!

    一、操作&实现

    1. 存储

    这里我定义了一个结构体 Treap 来封装,然后再 Treap 内又定义了结构体 node ,表示节点。

    struct Treap
    {
    	struct node
    	{
    		int val,size,times;
    		unsigned rd;
    		node *ch[2];
    	}e[100005],*root,*cnt;
    };
    

    解读:

    • Treap:表示 Treap
      • node:表示节点。
        • val:表示该节点所储存的数值。
        • size:表示以该节点为根的子树的大小(节点总数)。
        • times:表示该节点的数值的存在数量。
        • rd:随机优先值。
        • ch:指向子节点的指针。
          • ch[0]:指向左儿子的指针。
          • ch[1]:指向右儿子的指针。
      • e:储存所有节点。
      • root:指向根节点的指针。
      • cnt:指向最后一个插入的节点的指针。

    如果没能看懂各变量的作用也没有关系,在后面的操作中会为您一一解答。

    2. 操作

    2-0 node 的操作

    2-0-1 构造函数

    node(){}
    // 插入节点时用
    node(const int &x){val=x,size=times=1,rd=rand(),ch[0]=ch[1]=NULL;}
    
    优化

    C++ 自带的 rand 函数速度较慢,我们可以自己写一个 mrand 函数来取代:

    inline unsigned mrand()
    {
    	static unsigned long long tr=431322;
    	return unsigned(tr=(tr*76717)%0x100000000);
    }
    

    大家可能 static 用得较少,这里就不阐述原理了,感兴趣的可以自行百度。

    然后构造函数如下:

    node(){}
    // 插入节点时用
    node(const int &x){val=x,size=times=1,rd=mrand(),ch[0]=ch[1]=NULL;}
    

    2-0-2 更新节点信息

    不解释:

    inline void pushup()
    {
    	size=times;
    	if(ch[0])	size+=ch[0]->size;
    	if(ch[1])	size+=ch[1]->size;
    }
    

    现在 node 实现如下:

    struct node
    {
    	int val,size,times;
    	unsigned rd;
    	node *ch[2];
    	node(){}
    	node(const int &x){val=x,size=times=1,rd=mrand(),ch[0]=ch[1]=NULL;}
    	inline void pushup()
    	{
    		size=times;
    		if(ch[0])	size+=ch[0]->size;
    		if(ch[1])	size+=ch[1]->size;
    	}
    };
    

    下面实现操作的函数均为 Treap 的成员函数。

    附:调试函数,输出当前树的详细信息:

    // now 表示所操作子树的根节点指针引用,indent 表示当前缩进长度
    void output(node *&now,const int &indent)
    {
    	putchar('>'),putchar(' ');
    	for(int i=0;i<indent;++i)	putchar('|'),putchar(' ');
    	if(!now)
    	{
    		puts("NULL");
    		return;
    	}
    	// 输出当前节点的详细信息,可以更改
    	printf("%ld:%d %d,%d %u
    ",now-e,now->val,now->size,now->times,now->rd);
    	output(now->ch[0],indent+1);
    	output(now->ch[1],indent+1);
    }
    // 初始函数(方便调用)
    inline void output(){output(root,0);}
    

    2-1 旋转

    旋转是很多平衡树常见的操作,分为左旋和右旋。

    左旋的操作如下:


    右旋的操作与此类似,仅方向不同。事实上,右图中的树右旋后即可得到左图。

    实现的代码也很简单:

    // now 表示所操作子树的根节点指针引用,d 表示旋转方向(0 表示右旋,1 表示左旋)
    // 这里 now 为引用类型,便于更改。
    inline void rotate(node *&now,const bool &d)
    {
    	node *tmp=now->ch[d];			// tmp 指向将成为新的根节点的节点
    	now->ch[d]=tmp->ch[!d];
    	tmp->ch[!d]=now;
    	tmp->pushup(),now->pushup();	// 更新节点信息
    	now=tmp;						// tmp 指向的节点成为新的根节点
    }
    

    那么为什么要旋转呢?因为每个节点都会有一个随机优先值,而 Treap 的每个节点的优先值都比其子节点的大,利用堆的思想,使得 Treap 相对平衡。

    2-2 插入值为 (x) 的节点

    Treap 的插入其实就是在二叉排序树的插入的基础上通过旋转保证 Treap 堆的性质。

    // now 表示指向当前节点的指针的引用,x 表示要插入的值
    // 这里 now 为引用类型,便于更改。
    void insert(node *&now,const int &x)
    {
    	// 如果为空指针
    	if(!now)
    	{
    		*(now=++cnt)=x;		// 插入新节点
    		return;
    	}
    	++(now->size);			// 因为新节点在以 now 指向的节点为根节点的子树内,所以当前子树的节点数+1
    	// 如果当前节点的数值不等于 x
    	if(now->val!=x)
    	{
    		bool d=now->val<x;	// d 为插入的方向(0 为左,1 为右)
    		insert(now->ch[d],x);
    		// 如果当前节点的子节点的优先值大于当前节点的优先值(即不符合堆的性质)
    		if(now->ch[d]->rd>now->rd)	rotate(now,d);	// 旋转以维护堆的性质
    	}
    	// 否则说明当前节点的数值等于 x
    	else	++(now->times);	// 当前节点的数值的存在数量+1
    	now->pushup();			// 更新当前节点(之前我没有加上,导致我调了好久的 BUG)
    }
    // 初始函数(方便调用)
    inline void insert(const int &x){insert(root,x);}
    

    2-3 删除值为 (x) 的节点

    首先找到要删除的节点,然后通过旋转将其下移,直到其没有子节点时之间再将其直接删除。

    // now 表示指向当前节点的指针的引用,x 表示要删除的值
    // 这里 now 为引用类型,便于更改。
    void remove(node *&now,const int &x)
    {
    	// 如果当前节点不为要删除的节点
    	if(now->val!=x)	remove(now->ch[now->val<x],x);	// 继续向下寻找
    	// 否则说明要删除当前节点
    	// 如果当前节点有左儿子并且左儿子的优先值大于右儿子的优先值
    	// 则右旋使左儿子代替被删除的节点的位置
    	else if(now->ch[0] && (!now->ch[1] || now->ch[0]->rd>now->ch[1]->rd))	rotate(now,0),remove(now->ch[1],x);
    	// 否则如果当前节点有右儿子
    	// 则左旋使右儿子代替被删除的节点的位置
    	else if(now->ch[1])	rotate(now,1),remove(now->ch[0],x);
    	// 否则说明当前节点没有子节点
    	else
    	{
    		--(now->size);
    		// 如果该节点的存在数量清零了
    		if(!--(now->times))	now=NULL;	// 直接删除
    		return;
    	}
    	now->pushup();// 更新当前节点
    }
    // 初始函数(方便调用)
    inline void remove(const int &x){remove(root,x);}
    

    2-4 查询数值为 (x) 的节点的排名

    这个与二叉排序树的操作一样。

    // now 表示指向当前节点的指针的引用,x 表示要查询的值
    int getrank(node *&now,const int &x)
    {
    	// 如果为空指针,说明改数不存在于树中
    	if(!now)	return 0;							// 直接返回 0
    	// 如果当前节点的值大于 x
    	if(now->val>x)	return getrank(now->ch[0],x);	// 继续搜索左子树
    	// 如果当前节点的值小于 x
    	// 继续搜索右子树,返回值增加左子树节点数+当前节点存在数量
    	if(now->val<x)	return getrank(now->ch[1],x)+(now->ch[0]?now->ch[0]->size:0)+now->times;
    	// 否则说明当前节点的值等于 x
    	return (now->ch[0]?now->ch[0]->size:0)+1;		// 返回左子树节点数+1
    }
    // 初始函数(方便调用)
    inline int getrank(const int &x){return getrank(root,x);}
    

    2-5 查询排名为 (x) 的节点的数值

    // now 表示指向当前节点的指针的引用,x 表示要查询的排名
    int getval(node *&now,const int x)
    {
    	static int tmp;	// 临时变量,用于储存左子树的节点数+当前节点存在数量,为了节省空间就使用静态变量了
    	// 如果当前节点有左儿子且左子树的节点数不小于 x
    	// 继续搜索左子树
    	if(now->ch[0] && now->ch[0]->size>=x)	return getval(now->ch[0],x);
    	// 否则如果当前节点有右儿子且左子树的节点数+当前节点存在数量小于 x
    	// 继续搜索右子树中排名为 x-tmp 的节点
    	if(now->ch[1] && ((tmp=(now->ch[0]?now->ch[0]->size:0)+now->times)<x))	return getval(now->ch[1],x-tmp);
    	// 否则说明当前节点即要查询的节点
    	return now->val;// 返回当前节点的数值
    }
    // 初始函数(方便调用)
    inline int getval(const int &x){return getval(root,x);}
    

    2-6 查询数值为 (x) 的节点的前驱

    // now 表示指向当前节点的指针的引用,x 表示要查询的数值
    int getprev(node *&now,const int &x)
    {
    	// 如果为空指针
    	if(!now)	return -0x80000000;						// 返回负无穷
    	// 如果当前节点的数值不小于 x
    	// 说明前驱在左子树内
    	if(now->val>=x)	return getprev(now->ch[0],x);		// 搜索左子树
    	// 否则说明前驱为当前节点或在右子树内
    	else	return max(now->val,getprev(now->ch[1],x));	// 搜索右子树
    }
    // 初始函数(方便调用)
    inline int getprev(const int &x){return getprev(root,x);}
    

    2-7 查询数值为 (x) 的节点的后继

    与查询前驱思路相同。

    // now 表示指向当前节点的指针的引用,x 表示要查询的数值
    int getnext(node *&now,const int &x)
    {
    	// 如果为空指针
    	if(!now)	return 0x7fffffff;						// 返回负无穷
    	// 如果当前节点的数值不大于 x
    	// 说明后继在左子树内
    	if(now->val<=x)	return getnext(now->ch[1],x);		// 搜索左子树
    	// 否则说明后继为当前节点或在右子树内
    	else	return min(now->val,getnext(now->ch[0],x));	// 搜索右子树
    }
    // 初始函数(方便调用)
    inline int getnext(const int &x){return getnext(root,x);}
    

    二、例题

    1. 洛谷 P3369 【模板】普通平衡树

    题目链接:https://www.luogu.com.cn/problem/P3369

    这是一道模板题,没什么好说的,直接上代码:

  • 相关阅读:
    codeforces570D Tree Requests
    codeforces600E Lomsat gelral
    BZOJ2001 [Hnoi2010]City 城市建设
    BZOJ2565 最长双回文串
    BZOJ4031 [HEOI2015]小Z的房间
    BZOJ2467 [中山市选2010]生成树
    SPOJ104 HIGH
    爆零系列—补题A
    DP一直是自己的弱势 开始练滚动数组——HDOJ4502
    HDOJ4550 卡片游戏 随便销毁内存的代价就是wa//string类的一些用法
  • 原文地址:https://www.cnblogs.com/createsj/p/treap.html
Copyright © 2011-2022 走看看