Introduction to balanced tree

Keywords: data structure

Introduction to balanced tree

Preface: because \ (fhq \) is better than \ (splay \) in all aspects, mainly due to the small code size, the topic of this solution is \ (fhq\) \(treap \), that is, non rotating \ (treap \).

Template

Template question

Definition and nature

Balanced tree is a data structure composed of binary search tree and heap, so its name is \ (tree + heap \) that is \ (heap \).

In fact, the nature of heap and tree is conflicting. The binary search tree requires that the left son is less than the root node and less than the right son, while the heap satisfies that the root node is less than or equal to (or greater than or equal to) the left and right sons. Therefore, in \ (tree \), a single key value cannot be used as the data field of a node.

\Each node in (heap \) contains two values, which we set to \ (val \) and \ (key \).

  • \(val \): meet the properties of binary search tree.

  • \(key \): generated randomly, which meets the nature of heap, i.e. priority.

In short, the balanced tree adds a \ (key \) value to the binary search tree. We need to maintain it to meet the nature of the heap.

Node information

struct node{ 
	int l,r; //Left and right son 
	int val; //Point weight 
	int siz; //Subtree size 
	int key; //Tree random value 
}tr[N];

\The node information of (fhq \) is not fundamentally different from that of ordinary \ (heap \), but the number of nodes with the same weight is not recorded, that is, nodes with the same weight cannot be treated as one point. This difference wastes \ (fhq \) space, but reduces the difficulty of programming.

Create a new node

inline int build(int val)
{
	tr[++tot].val=val,tr[tot].key=random(INF);
	tr[tot].l=tr[tot].r=0,tr[tot].siz=1;
	return tot;
}

Where \ (tot \) records the total number of nodes, \ (val \) is the weight of the new node, and the \ (rondom \) function returns a random value.

Merge information

inline void pushup(int k) { tr[k].siz=tr[tr[k].l].siz+tr[tr[k].r].siz+1; }

division

inline void split(int k,int val,int &x,int &y)
{
	if(!k) { x=y=0; return; }
	if(tr[k].val<=val) x=k,split(tr[k].r,val,tr[k].r,y);
	else y=k,split(tr[k].l,val,x,tr[k].l);
	pushup(k);
}

The function \ (split(k,val,x,y) \) is equivalent to doing such a thing:

  • Split the subtree with \ (k \) as the root according to the weight:

    • If the weight is less than or equal to \ (val \), it will be added to the subtree with \ (x \) as the root.

    • If the weight is greater than \ (val \), it will return to the subtree with \ (y \) as the root.

Next, let's look at how it splits:

  • If the weight of this node is less than or equal to \ (val \), it means that the left subtree of node \ (k \) and node \ (k \) will be divided into subtree \ (x \), while the right subtree of \ (k \) has not been divided, so we need to divide the right subtree of \ (k \) recursively. Note that here we are recursive with reference, so if there is a node to be divided into \ (x \), just hang it directly

  • The same is true for cases greater than \ (val \), which will not be repeated here.

Of course, we can still split by size. According to different requirements of the topic, both splitting methods should be mastered. In fact, there is no big difference between the overall idea and splitting according to weight.

inline void split(int k,int s,int &x,int &y)
{
    if(!k) { x=y=0; return; }
    if(tr[tr[k].l].siz+1<=s) x=k,split(tr[k].r,s-tr[tr[k].l].siz-1,tr[x].r,y),pushup(x);
    else y=k,split(tr[k].l,s,x,tr[y].l),pushup(y);
}

merge

inline int merge(int x,int y)
{
	if(!x||!y) return x+y;
	if(tr[x].key>tr[y].key){
		tr[x].r=merge(tr[x].r,y),pushup(x);
		return x;
	}
	else{
		tr[y].l=merge(x,tr[y].l),pushup(y);
		return y;
	}
}

The operation realized by this code is to merge the subtree with \ (x \) as the root and the subtree with \ (y \) as the root. It should be noted that we ensure that the maximum weight of the subtree with \ (x \) as the root is less than the minimum weight of the subtree with \ (y \). At the same time, we need to constantly maintain the priority. Because of the above properties, we can merge directly without judging the weight of nodes. Finally, the value returned by this code is the root node after merging two subtrees.

Consider how to maintain priorities:

  • If the priority of \ (x \) is greater than that of \ (y \), we don't need to move \ (x \) and its left subtree. What we need to deal with is the merging of the right subtree of \ (x \) and \ (y \), which can be handled recursively.

  • Conversely, the priority of \ (y \) is higher than that of \ (x \). Similarly, we can still recursively process the left subtree of \ (y \) and \ (x \).

Follow this process to recurse all the time. When a subtree is empty, it returns \ (x+y \). Obviously, its correctness is not lost.

ps: splitting and merging are the key operations of \ (fhq \) \ (heap \), and the implementation of other operations is based on this and relatively simple.

insert

inline void insert(int val)
{
	int x,y;
	split(root,val-1,x,y);
	root=merge(merge(x,build(val)),y);
}

Insert a node with a weight of \ (val \) into the balance tree.

During implementation, split according to the weight \ (val-1 \). After splitting, all nodes with a weight less than \ (val-1 \) are in the \ (x \) subtree, and other nodes are in the \ (y \) subtree. First merge \ (x \) with the new node, and then merge the whole tree.

It is not difficult to understand that this process is actually to ensure the size and nature that we need to meet when merging.

delete

inline void delet(int val)
{
	int x,y,z;
	split(root,val,x,z),split(x,val-1,x,y);
	if(y) y=merge(tr[y].l,tr[y].r);
	root=merge(merge(x,y),z);
}

It is not difficult to find that after splitting, there are only nodes with weight equal to \ (val \) in the subtree with \ (y \) as the root. Merge the left and right subtrees and delete the root.

When the deletion is complete, the entire tree is merged again.

Query ranking

inline int getrank(int val)
{
	int x,y,ans;
	split(root,val-1,x,y);
	ans=tr[x].siz+1;
	root=merge(x,y);
	return ans;
}

The ranking of a number is actually the number of numbers \ (+ 1 \) smaller than him. After splitting, you can directly check the size of \ (x \) subtree.

Number of queries ranked \ (k \)

inline int getval(int rank)
{
	int k=root;
	while(k){
		if(tr[tr[k].l].siz+1==rank) break;
		else if(tr[tr[k].l].siz>=rank) k=tr[k].l;
		else rank-=tr[tr[k].l].siz+1,k=tr[k].r;
	}
	return !k?INF:tr[k].val;
}

Since our balanced tree satisfies the nature of binary search tree, we can carry out a process similar to bisection above, and discuss it based on this.

Precursors and successors

inline int getpre(int val)
{
	int x,y,k,ans;
	split(root,val-1,x,y);
	k=x;
	while(tr[k].r) k=tr[k].r;
	ans=tr[k].val;
	root=merge(x,y);
	return ans;
}
inline int getnext(int val)
{
	int x,y,k,ans;
	split(root,val,x,y);
	k=y;
	while(tr[k].l) k=tr[k].l;
	ans=tr[k].val;
	root=merge(x,y);
	return ans;
}

The query precursor splits the whole tree according to \ (val-1 \), and then takes the rightmost node of the \ (x \) subtree. Similarly, it will not be repeated later.

optimization

Because \ (fhq \) will waste space to some extent, an optimization based on this is to save the deleted nodes in a stack, and the newly inserted nodes give priority to the previously deleted nodes. This operation is not available in the example code, so I believe it is not difficult to implement.

CODE

#include <bits/stdc++.h>
using namespace std;
const int N=5e5+10,INF=1e9;
inline int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w*=-1; ch=getchar(); }
	while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
	return s*w;
}
int n;
struct node{ int l,r,val,siz,key; }tr[N];
inline int random(int lim) { return rand()*rand()%lim+1; }
struct Treap{ //fhq balanced tree 
	int tot,root;
	inline void pushup(int k) { tr[k].siz=tr[tr[k].l].siz+tr[tr[k].r].siz+1; }
	inline int build(int val){
		tr[++tot].val=val,tr[tot].key=random(INF);
		tr[tot].l=tr[tot].r=0,tr[tot].siz=1;
		return tot;
	}
	inline void split(int k,int val,int &x,int &y){
		if(!k) { x=y=0; return; }
		if(tr[k].val<=val) x=k,split(tr[k].r,val,tr[k].r,y);
		else y=k,split(tr[k].l,val,x,tr[k].l);
		pushup(k);
	}
	inline int merge(int x,int y){
		if(!x||!y) return x+y;
		if(tr[x].key>tr[y].key){
			tr[x].r=merge(tr[x].r,y),pushup(x);
			return x;
		}
		else{
			tr[y].l=merge(x,tr[y].l),pushup(y);
			return y;
		}
	}
	inline void insert(int val){
		int x,y;
		split(root,val-1,x,y);
		root=merge(merge(x,build(val)),y);
	}
	inline void delet(int val){
		int x,y,z;
		split(root,val,x,z),split(x,val-1,x,y);
		if(y) y=merge(tr[y].l,tr[y].r);
		root=merge(merge(x,y),z);
	}
	inline int getrank(int val){
		int x,y,ans;
		split(root,val-1,x,y);
		ans=tr[x].siz+1;
		root=merge(x,y);
		return ans;
	}
	inline int getval(int rank){
		int k=root;
		while(k){
			if(tr[tr[k].l].siz+1==rank) break;
			else if(tr[tr[k].l].siz>=rank) k=tr[k].l;
			else rank-=tr[tr[k].l].siz+1,k=tr[k].r;
		}
		return !k?INF:tr[k].val;
	}
	inline int getpre(int val){
		int x,y,k,ans;
		split(root,val-1,x,y);
		k=x;
		while(tr[k].r) k=tr[k].r;
		ans=tr[k].val;
		root=merge(x,y);
		return ans;
	}
	inline int getnext(int val){
		int x,y,k,ans;
		split(root,val,x,y);
		k=y;
		while(tr[k].l) k=tr[k].l;
		ans=tr[k].val;
		root=merge(x,y);
		return ans;
	}
}treap;
int main()
{
	n=read();
	for(register int i=1;i<=n;i++){
		int opt=read(),x=read();
		if(opt==1) treap.insert(x);
		else if(opt==2) treap.delet(x);
		else if(opt==3) printf("%d\n",treap.getrank(x));
		else if(opt==4) printf("%d\n",treap.getval(x));
		else if(opt==5) printf("%d\n",treap.getpre(x));
		else if(opt==6) printf("%d\n",treap.getnext(x));
	}
	return 0;
}

Posted by garyr on Wed, 03 Nov 2021 14:42:51 -0700