【学习笔记】Treap和无旋Treap

概述

一种平衡树。有人说它常数大,有人说它比 S p l a y \rm Splay Splay 快。或许两者都对

优点

好写。好想。简单。

有关它的一个故事:最强的 H a n d I n D e v i l \sf HandInDevil HandInDevil 第一天说:“有 AVL \text{AVL} AVL 还学什么 Treap \text{Treap} Treap 啊!” 第二天说:“ Treap \text{Treap} Treap 打着好简单啊,为什么还要学其他平衡树啊?”

思想

名字の内涵

不难发现 Treap = Tree + Heap \text{Treap}=\text{Tree}+\text{Heap} Treap=Tree+Heap

对于键值 key \text{key} key 来说,和其他所有平衡树一样,中序遍历单调不降。

对于我们自己定义的值 prio \text{prio} prio 来说,它满足堆性质:父节点大于子节点。

平衡方式

对于任何平衡树,最重要的就是它怎么保证高度是 O ( log ⁡ n ) \mathcal O(\log n) O(logn) 的。伸展树并不是平衡树

由于它同时具有堆性质和二叉搜索树性质, T r e a p \rm Treap Treap 实际上是一个 笛卡尔树。如果我们先把节点按照键值 k e y \rm key key 排序,那么树根就是当前区间优先级 p r i o \rm prio prio 的最大值点,然后两个儿子是被分割出的子区间递归建树。

i i i 个点是第 j j j 个点的祖先,当且仅当第 i i i 个点的管控区间包含 j j j,即 [ i , j ] [i,j] [i,j] p r i o \rm prio prio 的最大值是 i i i 。概率显然是 1 j − i + 1 \frac{1}{j-i+1} ji+11 。那么一个点的期望深度就是 ∑ j = 1 n 1 ∣ i − j ∣ + 1 = O ( ln ⁡ n ) \sum_{j=1}^{n}\frac{1}{|i-j|+1}=\mathcal O(\ln n) j=1nij+11=O(lnn),所以树高就是期望 O ( ln ⁡ n ) \mathcal O(\ln n) O(lnn) 的。

当然,对于一组给定的 p r i o \rm prio prio,你会发现 T r e a p \rm Treap Treap 的形态是唯一的。

怎么做

使用旋转来调整树的形态。

版本一:旋转

节点定义

像这样——

template < typename T >
struct Node{
	T data;
	unsigned cnt, size;
	int prio;
	Node *son[2];
	Node(const T &__data):data(__data){
		prio = rand(), cnt = size = 1;
		son[0] = son[1] = nullptr;
	}
	bool operator < (const Node &that) const {
		return prio < that.prio;
	}
	void maintain(){
		size = cnt;
		if(son[0] != nullptr)
			size += son[0]->size;
		if(son[1] != nullptr)
			size += son[1]->size;
	}
};
节点更新

更新节点上的信息。可视作树形 D P \Bbb{DP} DP

void pushUp(Node* o){
	o->size = o->cnt;
	if(o->son[0] != NULL)
		o->size += o->son[0]->size;
	if(o->son[1] != NULL)
		o->size += o->son[1]->size;
}
旋转

Splay \text{Splay} Splay 比较像,但是没有 Zig-Zig \text{Zig-Zig} Zig-Zig 这些东西。直接旋转即可。

sample figure
图示为一种旋转。读者不妨自己验证新的树是否仍然满足 key \text{key} key 中序遍历不降。

void rotate(Node* &o,int d){
	Node* k = o->son[d]; // 谁要篡位 
	o->son[d] = k->son[d^1];
	// 相当于图中的B的叛逃 
	if(o->son[d] != NULL)
		o->son[d]->fa = o;
	k->son[d^1] = o; // 儿子今天当爹啦! 
	k->fa = o->fa, o->fa = k;
	pushUp(o), pushUp(k);
	o = k; // 用引用传递,可以直接改变当前点 
}
插入

首先,按照普通二叉查找树的方法,找到 x x x 应该在的地方。

递归返回时,检查是否满足堆性质,不满足则旋转。

void insert(T __data,unsigned val,Node* &o){
	if(o == nullptr){
		o = new Node(__data);
		o->cnt = o->size = val;
		return ;
	}
	if(o->data == __data){
		o->cnt += val, o->size += val;
		return ;
	}
	int d = o->data < __data;
	insert(__data,val,o->son[d]);
	if(*o < *(o->son[d]))
		rotate(o,d);
	o->maintain();
}
删除

首先,找到它。如果删掉后不影响树的结构(个数大于 1 1 1 或者是叶子节点),就直接干掉。

否则,如果只有一个儿子,把这个儿子旋转上来;如果有两个儿子, prio \text{prio} prio 大的儿子上来。

然后递归删除即可。

void erase(T __data,unsigned val,Node* &o){
	if(o == nullptr)
		return ; // fail to erase 
	int d;
	if(o->data == __data){
		if(o->cnt > val){
			o->cnt -= val, o->size -= val;
			return ;
		}
		if(o->son[0] == nullptr and o->son[1] == nullptr){
			delete o; o = nullptr; return ;
		}
		if(o->son[0] == nullptr or o->son[1] == nullptr)
			d = (o->son[1] != nullptr);
		else d = (*(o->son[0]) < *(o->son[1]));
		rotate(o,d);
		erase(__data,val,o->son[d^1]);
	}
	else{
		d = o->data < __data;
		erase(__data,val,o->son[d]);
	}
	o->maintain();
}
排名查询
unsigned getRank(T __data){
		unsigned res = 0; Node* o=root;
		for(int d; o!=nullptr; o=o->son[d]){
			d = o->data < __data or o->data == __data;
			unsigned lsize = o->son[0] != nullptr ? o->son[0]->size : 0;
			res += d*(lsize+o->cnt);
			if(o->data == __data){ res -= (o->cnt-1); break; }
		}
		return res;
	}
查询排名为 k k k 的元素
T kthElement(unsigned k){ Node* o;
		for(o=root; o!=nullptr; ){
			unsigned lsize = (o->son[0] == nullptr ? 0 : o->son[0]->size);
			if(k > lsize){
				k -= lsize;
				if(k <= o->cnt) break;
				k -= o->cnt, o = o->son[1];
			}else o = o->son[0];
		}
		return o->data;
	}
询问个数

本质就是要找到那个节点。

unsigned count(T __data){ Node* o;
	for(o=root; o!=nullptr; o=o->son[o->data < __data])
		if(o->data == __data) break;
	return o->cnt;
}
完整代码
template < typename T >
class Treap{
	struct Node{
		T data;
		unsigned cnt, size;
		int prio;
		Node *son[2];
		Node(const T &__data):data(__data){
			prio = rand(), cnt = size = 1;
			son[0] = son[1] = nullptr;
		}
		bool operator<(const Node &that)const{
			return prio < that.prio;
		}
		void maintain(){
			size = cnt;
			if(son[0] != nullptr)
				size += son[0]->size;
			if(son[1] != nullptr)
				size += son[1]->size;
		}
	};
	Node* root;
	void rotate(Node* &o,int d){
		Node* k = o->son[d];
		o->son[d] = k->son[d^1];
		k->son[d^1] = o;
		o->maintain(), k->maintain();
		o = k; // change root 
	}
	void insert(T __data,unsigned val,Node* &o){
		if(o == nullptr){
			o = new Node(__data);
			o->cnt = o->size = val;
			return ;
		}
		if(o->data == __data){
			o->cnt += val, o->size += val;
			return ;
		}
		int d = o->data < __data;
		insert(__data,val,o->son[d]);
		if(*o < *(o->son[d]))
			rotate(o,d);
		o->maintain();
	}
	void erase(T __data,unsigned val,Node* &o){
		if(o == nullptr)
			return ; // err to erase 
		int d;
		if(o->data == __data){
			if(o->cnt > val){
				o->cnt -= val, o->size -= val;
				return ;
			}
			if(o->son[0] == nullptr and o->son[1] == nullptr){
				delete o; o = nullptr; return ;
			}
			if(o->son[0] == nullptr or o->son[1] == nullptr)
				d = (o->son[1] != nullptr);
			else d = (*(o->son[0]) < *(o->son[1]));
			rotate(o,d);
			erase(__data,val,o->son[d^1]);
		}
		else{
			d = o->data < __data;
			erase(__data,val,o->son[d]);
		}
		o->maintain();
	}
	void dispose(Node* &o){
		if(o == nullptr)
			return ;
		dispose(o->son[0]);
		dispose(o->son[1]);
		delete o; o = nullptr;
	}
public:
	void dispose(){
		dispose(root);
	}
	void clear(){
		root = nullptr;
	}
	Treap(){ clear(); }
	void insert(T __data,unsigned val=1){
		insert(__data,val,root);
	}
	void erase(T __data,unsigned val=1){
		erase(__data,val,root);
	}
	unsigned getRank(T __data){
		unsigned res = 0; Node* o=root;
		for(int d; o!=nullptr; o=o->son[d]){
			d = o->data < __data or o->data == __data;
			unsigned lsize = o->son[0] != nullptr ? o->son[0]->size : 0;
			res += d*(lsize+o->cnt);
			if(o->data == __data){ res -= (o->cnt-1); break; }
		}
		return res;
	}
	T kthElement(unsigned k){ Node* o;
		for(o=root; o!=nullptr; ){
			unsigned lsize = (o->son[0] == nullptr ? 0 : o->son[0]->size);
			if(k > lsize){
				k -= lsize;
				if(k <= o->cnt) break;
				k -= o->cnt, o = o->son[1];
			}else o = o->son[0];
		}
		return o->data;
	}
	unsigned count(T __data){ Node* o;
		for(o=root; o!=nullptr; o=o->son[o->data < __data])
			if(o->data == __data) break;
		return o->cnt;
	}
	void monitor(){
		printf("tree:");
		deBug(root);
		putchar('\n');
	}
};

版本二:无旋

只需要两个核心操作:分裂与合并。

我在写无旋 Treap \text{Treap} Treap 的指针版本时心态爆炸了。然后就换成数组了。

节点定义

然鹅并没有节点。

int prio[N], data[N];
int son[N][2], root;
unsigned size[N], cnt[N];
vector<int> pool; // 内存池 
void newTreap(){
	for(int i=1; i<N; ++i)
		pool.push_back(i);
	root = cnt[0] = size[0] = prio[0] = 0;
	// 0 等效于 NULL 
}
int newNode(int x){
	int id = pool.back();
	pool.pop_back();
	data[id] = x, size[id] = cnt[id] = 1;
	son[id][0] = son[id][1] = 0, prio[id] = rand();
	return id;
}
void deleteNode(int id){
	pool.push_back(id);
}
分裂

按照某个 key \text{key} key x x x 进行分裂。返回值为 pair \text{pair} pair,左边的树满足 key ≤ x \text{key}\le x keyx,另一边则相反。

只需要考虑根节点分到了哪一边。如果根节点是右边的,显然其右子树也在右边;如果根节点是左边的,显然其左子树也在左边。

递归的合并另一边(没有跟根节点一起走的子树)。

pair<int,int> split(int bound,int o){
	// (x <= bound) and (bound < x)
	if(not o) return make_pair(o,o);
	pair<int,int> p;
	if(data[o] <= bound){
		p = split(bound,son[o][1]);
		son[o][1] = p.first;
		p.first = o;
	}
	else{
		p = split(bound,son[o][0]);
		son[o][0] = p.second;
		p.second = o;
	}
	pushUp(o); return p;
}
合并

假设左边的树的 key \text{key} key 严格小于右边的树的 key \text{key} key

直接考虑谁是根节点——可以用 prio \text{prio} prio 判断。剩下的事情就是递归了——要满足 key \text{key} key 中序遍历不降。

int merge(int a,int b){
	if(not a or not b) return a+b;
	if(prio[a] < prio[b]) son[b][0] = merge(a,son[b][0]);
	else son[a][1] = merge(son[a][1],b);
	pushUp(a), pushUp(b);
	return prio[a] < prio[b] ? b : a;
}

为了让代码更短的函数:

void decrease(int o){
	-- cnt[o], -- size[o];
}
void increase(int o){
	++ cnt[o], ++ size[o];
}
插入

直接把树裂开,再暴力合并两次。

void insert(int x){
	pair<int,int> rPart = split(x,root);
	pair<int,int> lPart = split(x-1,rPart.first);
	if(lPart.second) increase(lPart.second);
	else lPart.second = newNode(x);
	root = merge(merge(lPart.first,lPart.second),rPart.second);
}
删除

还是把树裂开,剩下的合并上去。

void erase(int x){
	pair<int,int> rPart = split(x,root);
	pair<int,int> lPart = split(x-1,rPart.first);
	if(cnt[lPart.second] > 1) decrease(lPart.second);
	else deleteNode(lPart.second), lPart.second = 0;
	root = merge(merge(lPart.first,lPart.second),rPart.second);
}
其他函数
unsigned getRank(int x){
	// 前驱的排名;(前驱<=x) 
	unsigned res = 0;
	for(int o=root,d; o; o=son[o][d]){
		d = data[o] <= x;
		res += d*(cnt[o]+size[son[o][0]]);
		if(data[o] == x){ res += 1-cnt[o]; break; }
	}
	return res;
}
int kthElement(unsigned x){ int o;
	for(o=root; o; ){
		if(x > size[son[o][0]]){
			x -= size[son[o][0]];
			if(x <= cnt[o]) break;
			x -= cnt[o], o = son[o][1];
		}else o = son[o][0];
	}
	return data[o];
}
unsigned count(int x){ int o;
	for(o=root; o; o=son[o][data[o]<x])
		if(data[o] == x) break;
	return cnt[o];
}
完整代码
# define T int // 可以换成你自己需要的类型 
const int MaxN = 200000;
namespace Treap{
	int prio[MaxN]; T data[MaxN];
	int son[MaxN][2], root;
	unsigned size[MaxN], cnt[MaxN];
	vector<int> pool;
	void newTreap(){
		for(int i=1; i<MaxN; ++i)
			pool.push_back(i);
		root = cnt[0] = size[0] = prio[0] = 0;
	}
	int newNode(T __data){
		int id = pool.back();
		pool.pop_back();
		data[id] = __data;
		size[id] = cnt[id] = 0;
		son[id][0] = son[id][1] = 0;
		prio[id] = rand();
		return id;
	}
	void deleteNode(int id){
		pool.push_back(id);
	}
	void pushUp(int o){
		size[o] = cnt[o]+size[son[o][0]]+size[son[o][1]];
	}
	void change(int o,int addv){
		cnt[o] += addv, size[o] += addv;
	}
	pair<int,int> split(T bound,int o){
		// (x <= bound) and (bound < x)
		if(not o) return make_pair(0,0);
		pair<int,int> p;
		if(data[o] <= bound){
			p = split(bound,son[o][1]);
			son[o][1] = p.first, p.first = o;
		}
		else{
			p = split(bound,son[o][0]);
			son[o][0] = p.second, p.second = o;
		}
		pushUp(o); return p;
	}
	int merge(int a,int b){
		if(not a or not b) return a+b;
		if(prio[a] < prio[b]) son[b][0] = merge(a,son[b][0]);
		else son[a][1] = merge(son[a][1],b);
		pushUp(a), pushUp(b);
		return prio[a] < prio[b] ? b : a;
	}
	void insert(T __data,unsigned val=1){
		pair<int,int> rPart = split(__data,root);
		pair<int,int> lPart = split(__data-1,rPart.first);
		if(not lPart.second) lPart.second = newNode(__data);
		change(lPart.second,val);
		root = merge(merge(lPart.first,lPart.second),rPart.second);
	}
	void erase(T __data,unsigned val=1){
		pair<int,int> rPart = split(__data,root);
		pair<int,int> lPart = split(__data-1,rPart.first);
		if(cnt[lPart.second] > val) change(lPart.second,-val);
		else deleteNode(lPart.second), lPart.second = 0;
		root = merge(merge(lPart.first,lPart.second),rPart.second);
	}
	unsigned getRank(T __data){
		// 前驱的排名;(前驱<=x) 
		unsigned res = 0;
		for(int o=root,d; o; o=son[o][d]){
			d = data[o] <= __data;
			res += d*(cnt[o]+size[son[o][0]]);
			if(data[o] == __data){ res += 1-cnt[o]; break; }
		}
		return res;
	}
	T kthElement(unsigned x){ int o;
		for(o=root; o; ){
			if(x > size[son[o][0]]){
				x -= size[son[o][0]];
				if(x <= cnt[o]) break;
				x -= cnt[o], o = son[o][1];
			}else o = son[o][0];
		}
		return data[o];
	}
	unsigned count(T __data){ int o;
		for(o=root; o; o=son[o][data[o] < __data])
			if(data[o] == __data) break;
		return cnt[o];
	}
};
大小版

显然 T r e a p \rm Treap Treap s p l i t \rm split split 也可以基于 s i z e size size 。再实现求排名与求第 k k k 小之后,就可以实现所有功能了。

namespace Treap{
	mt19937 rander;
	const int MaxM = 500005;
	unsigned prio[MaxM], siz[MaxM], cnt[MaxM];
	int son[MaxM][2], data[MaxM];
	void pushUp(int o){
		rep(j,siz[o]=0,1)
			siz[o] += siz[son[o][j]];
		siz[o] += cnt[o];
	}
	int merge(int a,int b){
		if(!a || !b) return a^b;
		if(prio[a] > prio[b]){
			son[a][1] = merge(son[a][1],b);
			pushUp(a); return a;
		}
		else{
			son[b][0] = merge(a,son[b][0]);
			pushUp(b); return b;
		}
	}
	PII split(int o,unsigned k){
		if(!o) return PII(o,o);
		int d = (k > siz[son[o][0]]);
		if(d) k -= siz[son[o][0]]+cnt[o];
		PII p = split(son[o][d],k);
		son[o][d] = p[d^1], p[d^1] = o;
		pushUp(o); return p;
	}

	int cntNode;
	int newNode(int D){
		int &o = ++ cntNode;
		son[o][0] = son[o][1] = 0;
		prio[o] = rander();
		cnt[o] = 1; data[o] = D;
		pushUp(o); return o;
	}

	int rt;
	unsigned getRank(int x){
		int o = rt; unsigned res = 1;
		for(int d; o; o=son[o][d]){
			d = (data[o] <= x);
			res += (siz[son[o][0]]+cnt[o])*d;
			if(data[o] == x)
				return res-cnt[o];
		}
		return res;
	}
	int kthElement(unsigned k){
		int o = rt; // start from root
		for(int d=0; o; o=son[o][d],d=0){
			if(k > siz[son[o][0]]){
				k -= siz[son[o][0]], d = 1;
				if(k <= cnt[o])
					return data[o];
				else k -= cnt[o];
			}
		}
		return data[0]; // what's that?
	}

	void insert(int x){
		unsigned l = getRank(x)-1;
		unsigned r = getRank(x+1);
		PII rp = split(rt,r-1);
		PII lp = split(rp[0],l);
		if(!lp[1]) lp[1] = newNode(x);
		else ++ cnt[lp[1]], ++ siz[lp[1]];
		rt = merge(merge(lp[0],lp[1]),rp[1]);
	}
	void erase(int x){
		unsigned l = getRank(x)-1;
		unsigned r = getRank(x+1);
		PII rp = split(rt,r-1);
		PII lp = split(rp[0],l);
		-- cnt[lp[1]], -- siz[lp[1]];
		if(!cnt[lp[1]]) lp[1] = 0;
		rt = merge(merge(lp[0],lp[1]),rp[1]);
	}
}

版本二点五:笛卡尔树

名字听着很高大上,其实 狗屁不是 很通俗的。

如果我们按照 key \text{key} key 递增的顺序插入每一个点,想想我们 insert \text{insert} insert 函数递归到最底层的时候,这个点在哪里?

对了,最右边!因为此时它是整棵树中最大的一个。它一定在最右边。

如果我们需要进行 rotate \text{rotate} rotate 旋转,将它旋转上去,显然,只会涉及最右边的一条链。

所以,我们用一个栈存储最右边的那一条链上面的点。显然,插入这个点之后,这个点一定是栈顶,所以我们可以放心的弹栈。

void init(int n){ // 插入1-n的所有key 
	vector<int> v; v.clear(); newTreap();
	for(int i=1,o,k; i<=n; ++i){
		o = newNode(i);
		while(not v.empty()){
			k = v.back();
			if(prio[k] < prio[o])
				pushUp(son[o][0] = k);
			else{
				son[k][1] = o; break;
			}
			v.pop_back();
		}
		if(v.empty()) root = o;
		// 别忘了更新root 
		v.push_back(o);
	}
	while(not v.empty())
		pushUp(v.back()), v.pop_back();
}

例题

板子

传送门 to luogu。就是板题普通平衡树

#include <cstdio>
#include <iostream>
#include <vector>
#include <cstdlib>
using namespace std;
inline int readint(){
	int a = 0, f = 1; char c = getchar();
	for(; c<'0' or c>'9'; c=getchar())
		if(c == '-') f = -1;
	for(; '0'<=c and c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
inline void writeint(int x){
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) writeint(x/10);
	putchar(x%10+'0');
}

const int N = 500000;
namespace Treap{
	int prio[N], data[N];
	int son[N][2], root;
	unsigned size[N], cnt[N];
	vector<int> pool;
	void newTreap(){
		for(int i=1; i<N; ++i)
			pool.push_back(i);
		root = cnt[0] = size[0] = prio[0] = 0;
	}
	int newNode(int x){
		int id = pool.back();
		pool.pop_back();
		data[id] = x, size[id] = cnt[id] = 1;
		son[id][0] = son[id][1] = 0, prio[id] = rand();
		return id;
	}
	void deleteNode(int id){
		pool.push_back(id);
	}
	void pushUp(int o){
		size[o] = cnt[o]+size[son[o][0]]+size[son[o][1]];
	}
	void decrease(int o){
		-- cnt[o], -- size[o];
	}
	void increase(int o){
		++ cnt[o], ++ size[o];
	}
	pair<int,int> split(int bound,int o){
		// (x <= bound) and (bound < x)
		if(not o) return make_pair(o,o);
		pair<int,int> p;
		if(data[o] <= bound){
			p = split(bound,son[o][1]);
			son[o][1] = p.first;
			p.first = o;
		}
		else{
			p = split(bound,son[o][0]);
			son[o][0] = p.second;
			p.second = o;
		}
		pushUp(o); return p;
	}
	int merge(int a,int b){
		if(not a or not b) return a+b;
		if(prio[a] < prio[b]) son[b][0] = merge(a,son[b][0]);
		else son[a][1] = merge(son[a][1],b);
		pushUp(a), pushUp(b);
		return prio[a] < prio[b] ? b : a;
	}
	void insert(int x){
		pair<int,int> rPart = split(x,root);
		pair<int,int> lPart = split(x-1,rPart.first);
		if(lPart.second) increase(lPart.second);
		else lPart.second = newNode(x);
		root = merge(merge(lPart.first,lPart.second),rPart.second);
	}
	void erase(int x){
		pair<int,int> rPart = split(x,root);
		pair<int,int> lPart = split(x-1,rPart.first);
		if(cnt[lPart.second] > 1) decrease(lPart.second);
		else deleteNode(lPart.second), lPart.second = 0;
		root = merge(merge(lPart.first,lPart.second),rPart.second);
	}
	unsigned getRank(int x){
		// 前驱的排名;(前驱<=x) 
		unsigned res = 0;
		for(int o=root,d; o; o=son[o][d]){
			d = data[o] <= x;
			res += d*(cnt[o]+size[son[o][0]]);
			if(data[o] == x){ res += 1-cnt[o]; break; }
		}
		return res;
	}
	int kthElement(unsigned x){ int o;
		for(o=root; o; ){
			if(x > size[son[o][0]]){
				x -= size[son[o][0]];
				if(x <= cnt[o]) break;
				x -= cnt[o], o = son[o][1];
			}else o = son[o][0];
		}
		return data[o];
	}
	unsigned count(int x){ int o;
		for(o=root; o; o=son[o][data[o]<x])
			if(data[o] == x) break;
		return cnt[o];
	}
}using namespace Treap;

int main(){
	srand(5201314); newTreap();
	for(int T=readint(); T; --T){
		int opt = readint(), x = readint();
		switch(opt){
			case 1:insert(x); break;
			case 2:erase(x); break;
			case 3:writeint(getRank(x)); putchar('\n'); break;
			case 4:writeint(kthElement(x)); putchar('\n'); break;
			case 5:{
				int ans = kthElement(getRank(x)-(count(x)?1:0));
				writeint(ans); putchar('\n'); break;
			}
			case 6:{
				int ans = kthElement(getRank(x)+max(count(x),1u));
				writeint(ans); putchar('\n'); break;
			}
			default:puts("怎么可能?你在想peach"); break;
		}
	}
	return 0;
}
书架

点此跳转。感觉不是很难 但是也不会

文艺平衡树

电磁跳转。似乎不是很难?

后记

我两个版本都出现了同一个错误——

算排名的时候, data=x \text{data=x} data=x时,我直接 res++ \text{res++} res++就跑了。忘记了左子树的 size \text{size} size

要不是这一点,我都不想写这该死的【学习笔记】博客

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值