洛谷P3369 Splay树模板

题意:

写一种数据结构满足:
1. 1. 1.插入 x x x
2. 2. 2.删除 x x x数(若有多个相同的数,因只删除一个)
3. 3. 3.查询 x x x数的排名(排名定义为比当前数小的数的个数 + 1 +1 +1。若有多个相同的数,因输出最小的排名)
4. 4. 4.查询排名为 x x x的数
5. 5. 5. x x x的前驱(前驱定义为小于 x x x,且最大的数)
6. 6. 6. x x x的后继(后继定义为大于 x x x,且最小的数)

平衡树模板题

平衡树有很多种,这里根据个人理解简述(乱讲)的是 s p l a y splay splay
通过玄学旋转 s p l a y splay splay可以做到均摊 O ( n l o g n ) O(nlogn) O(nlogn)的复杂度,证明建议问 T a r j a n Tarjan Tarjan

本题的 s p l a y splay splay用到的变量:
r o o t root root:根节点, t o t tot tot:总的节点数
s i z [ i ] siz[i] siz[i]:编号为 i i i的节点的子树权值个数(包括自己以及重复权值)
c n t [ i ] cnt[i] cnt[i]:编号为 i i i的节点的权值出现的次数
p r e [ i ] pre[i] pre[i]:编号为 i i i的节点的父亲节点
v a l [ i ] val[i] val[i]:编号为 i i i的节点的权值
c h [ i ] [ 2 ] ch[i][2] ch[i][2]:编号为 i i i节点的左右节点, c h [ i ] [ 0 ] ch[i][0] ch[i][0]表示 i i i的左节点, 1 1 1则表示右节点

需要用到的两个小操作:
1. 1. 1.更新 i i i节点的 s i z [ i ] siz[i] siz[i]

void pushup(int x) {
	siz[x] = siz[ch[x][0]] + siz[ch[x][1]] + cnt[x];
}

2. 2. 2.判断 x x x编号的点是父节点的左节点还是右节点

int get(int x) {
	return ch[pre[x]][1] == x;
}

I . I. I.重要的旋转操作:
s p l a y splay splay中,节点一般是从下往上旋转至某个位置(根或根的右节点)。
一个节点旋转要旋转至他的父亲节点通过这种方式旋转:
假设要旋转的该节点是父节点的左节点,父节点又是爷爷节点的左节点
1. 1. 1.则父节点的左节点变为父节点的右节点。
2. 2. 2.然后爷爷节点的左节点变为该节点。
3. 3. 3.接着该节点的右节点变为父节点
简而言之就是,看旋转的点是在父节点的左还是右,是左,则父节点的左节点变为旋转的点的右节点,是右,则父节点的右节点变为旋转的点的左节点。再看父节点在爷爷节点的左还是右,是左,则爷爷节点的左节点变为旋转的点,是右则反之。最后原本旋转的点在父节点的方向的反方向的节点变为父节点。有图就好了。

void rotate(int x) {
	int f = pre[x], gf = pre[f], k = get(x), w = ch[x][k ^ 1];//f是父节点,gf是爷爷节点
	ch[f][k] = w, pre[w] = f;//k是要旋转的点相对父节点的方向
	ch[gf][get(f)] = x, pre[x] = gf;
	ch[x][k ^ 1] = f, pre[f] = x;
	pushup(f), pushup(x);
}

I I . II. II. s p l a y splay splay操作
是将一个点旋转至指定位置的操作,每当插入一个节点或是删除一个节点都会 s p l a y splay splay一下让这个节点不断旋转成为根节点。根据实际需要还会 s p l a y splay splay一些点到特殊位置。
将一个点旋转到根一般用双旋转,然后还得看要旋转的点和父节点和爷爷节点的链长什么样。
如果这点链三点一线,则我们要先旋转父节点再旋转该点,只旋转该点会被卡。不断旋转旋转直到该点旋转成根节点的儿子,这个时候单旋转一次就可以了。

void splay(int x, int goal = 0) {//不传goal, 说明旋转成根节点
	int f, gf;
	while (pre[x] != goal) {
		f = pre[x], gf = pre[f];
		if (gf != goal) {
			if(get(x) == get(f)) rotate(f);
			else rotate(x);
		}
		rotate(x);
	}
	if (!goal) root = x;
}

I I I . III. III.插入节点操作
根据平衡树的性质不断往左往右找即可,如果这个点已经存在就让 c n t [ i ] + + cnt[i]++ cnt[i]++,否则新建一个节点,最后再将这个节点 s p l a y splay splay至根。

void insert(int x) {//cur是节点编号, 树上都是按编号操作, 不是按权值操作
	int cur = root, f = 0;
	while(cur && val[cur] != x) {
		f = cur;
		cur = ch[cur][x > val[cur]];
	}
	if (cur) {
		cnt[cur]++;
	} else {
		cur = ++tot;
		if (f) ch[f][x > val[f]] = cur;
		ch[cur][0] = ch[cur][1] = 0;
		siz[cur] = cnt[cur] = 1;
		pre[cur] = f, val[cur] = x;
	}
	splay(cur);
}

I V . IV. IV.寻找最大的比这个节点小或等于的节点,并将其 s p l a y splay splay至根节点

void change(int x) {
	if (!root) return ;
	int cur = root;
	while(ch[cur][x > val[cur]] && val[cur] != x) 
		cur = ch[cur][x > val[cur]];
	splay(cur);
}

V . V. V.寻找一个数的前驱
I V IV IV操作将最大的比这个节点小或等于的节点 s p l a y splay splay到根,然后在根节点的左节点的最右节点就是答案。(注意可能此时根节点就是答案)

int getpre(int x) {
	change(x);
	if (x > val[root]) return root;
	int cur = ch[root][0];
	while (ch[cur][1]) cur = ch[cur][1];
	return cur;
}

V I . VI. VI.寻找一个数的后继
操作 I V IV IV之后,根节点的右节点的最左节点即是答案。(注意可能此时根节点就是答案)

int getsuc(int x) {
	change(x);
	if (x < val[root]) return root;
	int cur = ch[root][1];
	while (ch[cur][0]) cur = ch[cur][0];
	return cur;
}

V I I . VII. VII.删除操作
一个数必定是夹在前驱与后继之间的,所以我们将前驱 s p l a y splay splay至根,然后将后继 s p l a y splay splay至前驱的右节点。此时,后继节点的左节点就是要删除的点。(本题有重复的数)

void del(int x) {
	int pre = getpre(x), suc = getsuc(x);
	splay(pre), splay(suc, pre);
	int cur = ch[suc][0];
	if (cnt[cur] > 1) {
		cnt[cur]--;
		splay(cur);
	} else {
		ch[suc][0] = 0;
		pushup(root), pushup(suc);
	}
}

V I I I . VIII. VIII.寻找第 k k k大数
从根节点往下左右找即可,如果这个节点的右 s i z siz siz大于等于 k k k说明答案在右子树中,否则在左子树。如果是进入左子树就先将 k k k减去右子树的 s i z siz siz和这个点的 c n t cnt cnt,如果 k k k小于 0 0 0了,说明答案就是这个点。

int kth(int x) {
	int cur = root;
	while (true) {
		if (siz[ch[cur][0]] >= x && ch[cur][0]) {
			cur = ch[cur][0];
		} else {
			x -= siz[ch[cur][0]] + cnt[cur];
			if (x <= 0) return cur;
			cur = ch[cur][1];
		}
	}
}

关于建树:一种是一个数一个数的 i n s e r t insert insert,效率较低。一种是像线段树一样建树,且这样建树得到的树是较平衡的。对于本题使用前一种方法。第二种方法:

int build(int l, int r, int f) {
	if (l > r) return 0;
	int mid = (l + r) >> 1, now = ++tot;
	val[now] = a[mid], pre[now] = f;
	siz[now] = 0;//根据不同题目这部分会有修改
	ch[now][0] = build(l, mid - 1, now);
	ch[now][1] = build(mid + 1, r, now);
	pushup(now);
	return now;
}

最后需要注意的就是操作之前要先插入两个哨兵节点极大值和极小值,以免越界问题导致 t l e tle tle。至此关于这题的 s p l a y splay splay操作就完成了… s p l a y splay splay树不仅可以简单的维护序列,还可以做到更多高级操作。

总的代码:

#include <bits/stdc++.h>
using namespace std;

const int maxn  = 1e5 + 5;
const int inf   = 0x3f3f3f3f;

int n, opt, x;
int root, tot;
int siz[maxn], cnt[maxn], pre[maxn], val[maxn], ch[maxn][2];

//由于这里我们用到了l - 1和r + 1号节点,所以对于节点1 ~ n的,如果查询1 ~ n这个区间,
//就会用到0, n + 1这两个节点,所以开始建树时要多加入两个节点。

void pushup(int x) {
	siz[x] = siz[ch[x][0]] + siz[ch[x][1]] + cnt[x];
}

int get(int x) {
	return ch[pre[x]][1] == x;
}

void rotate(int x) {
	int f = pre[x], gf = pre[f], k = get(x), w = ch[x][k ^ 1];
	ch[f][k] = w, pre[w] = f;
	ch[gf][get(f)] = x, pre[x] = gf;
	ch[x][k ^ 1] = f, pre[f] = x;
	pushup(f), pushup(x);
}

void splay(int x, int goal = 0) {//不传goal, 说明旋转成根节点
	int f, gf;
	while (pre[x] != goal) {
		f = pre[x], gf = pre[f];
		if (gf != goal) {
			if(get(x) == get(f)) rotate(f);
			else rotate(x);
		}
		rotate(x);
	}
	if (!goal) root = x;
}

void insert(int x) {//cur是节点编号, 树上都是按编号操作, 不是按权值操作
	int cur = root, f = 0;
	while(cur && val[cur] != x) {
		f = cur;
		cur = ch[cur][x > val[cur]];
	}
	if (cur) {
		cnt[cur]++;
	} else {
		cur = ++tot;
		if (f) ch[f][x > val[f]] = cur;
		ch[cur][0] = ch[cur][1] = 0;
		siz[cur] = cnt[cur] = 1;
		pre[cur] = f, val[cur] = x;
	}
	splay(cur);
}

void change(int x) {
	if (!root) return ;
	int cur = root;
	while(ch[cur][x > val[cur]] && val[cur] != x) 
		cur = ch[cur][x > val[cur]];
	splay(cur);
}

int getpre(int x) {
	change(x);
	if (x > val[root]) return root;
	int cur = ch[root][0];
	while (ch[cur][1]) cur = ch[cur][1];
	return cur;
}

int getsuc(int x) {
	change(x);
	if (x < val[root]) return root;
	int cur = ch[root][1];
	while (ch[cur][0]) cur = ch[cur][0];
	return cur;
}

void del(int x) {
	int pre = getpre(x), suc = getsuc(x);
	splay(pre), splay(suc, pre);
	int cur = ch[suc][0];
	if (cnt[cur] > 1) {
		cnt[cur]--;
		splay(cur);
	} else {
		ch[suc][0] = 0;
		pushup(root), pushup(suc);
	}
}

int kth(int x) {
	int cur = root;
	while (true) {
		if (siz[ch[cur][0]] >= x && ch[cur][0]) {
			cur = ch[cur][0];
		} else {
			x -= siz[ch[cur][0]] + cnt[cur];
			if (x <= 0) return cur;
			cur = ch[cur][1];
		}
	}
}

int main() {
	scanf ("%d", &n);
	insert(-inf);
	insert(inf);
	while (n--) {
		scanf ("%d %d", &opt, &x);
		if (opt == 1) {//插入x 
			insert(x);
		} else if (opt == 2) {//删除x 
			del(x);
		} else if (opt == 3) {//查询x的排名 
			change(x);
			printf ("%d\n", siz[ch[root][0]]);
		} else if (opt == 4) {//查询排名为x的数 
			printf ("%d\n", val[kth(x + 1)]);//x+1是因为加了一个极小的哨兵节点
		} else if (opt == 5) {//求x的前驱 
			printf ("%d\n", val[getpre(x)]);
		} else if (opt == 6) {//求x的后继 
			printf ("%d\n", val[getsuc(x)]);
		}
	}
	return 0;
}


推荐个好的博客,蒟蒻就是在这学的
splay简易教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值