P2596 [ZJOI2006]书架 题解

发布时间 2023-06-22 17:43:31作者: Aisaka_Taiga

题目传送门:link

FHQ-Treap

解题的关键在于如何来求出一本书上面有多少本书,但考虑到我们里面没有像权值一样的东西来让我们用按值分裂来完成这个操作,所以考虑用按排名分裂来实现。

我们按照先后顺序把所有的书都给插入到平衡树里面,根据二叉搜索树的性质,当前结点的所有左子树的结点都是在当前结点代表的书的上面的书,但是其实并不只是当前结点的左子树,比如下面的图:

image

假设我们要查询 \(5\) 号点代表的书上面有几本书,我们就会发现,其实不止是 \(10\) 号点,还有 \(2\) 号结点的左子树所有点,实际上在平衡树中,这些点在原序列中都是在 \(5\) 号点代表的书的上方的。

我们能发现一个规律就是我们从当前的点开始算上当前结点的左子树大小,然后如果当前点是父节点的右儿子,那么我们跳到父节点,然后这时当前点的左子树的所有点都是在要查询的点代表的书的上方,所以我们累加当前点左子树的左子树大小加一,因为还有当前点也是在我们要查询的点代表的书的上方的。我们一直到了根节点才停止,这样就求出了当前点代表的书上方有几本书了。

用代码实现起来就是这样的:

inline int fid(int x)
{
	int xx = x, res = siz[ch[x][0]] + 1;
	while(xx != rt && x)
	{
		if(ch[fa[x]][1] == x) res += siz[ch[fa[x]][0]] + 1;
		x = fa[x];
	}
	return res;
}

题目中的各项操作都是基于给定书的上方有几本书来解决的,所以我们现在的主要问题就是父节点。

我们在分裂和合并的时候,要顺便维护一下当前点的父节点是谁,比如在分裂的过程中,我们多传两个参数表示当前点的候选父节点,也就是一个是左部分的当前分裂到的上一个结点和右部分分裂到的上一个结点,如果要是当前点的左子树的结点数小于 \(k\),我们就把当前点的父节点标记为上层传下来的左半部分的父节点,因为当前点是要被划分到左部分的,反之同理;对于合并操作,我们在递归往前推的时候记录一下更新完的点的父节点即可。

我们在一开始把所有书插入平衡树的时候是直接插到后面的,需要开一个数组来记录对应的书的编号在平衡树中结点的编号,在掉用上面的 fid 的时候直接用题目给的书的编号来查找当前编号的书的上面有多少本书即可。

对于 Insert 操作是 \(0\) 的就可以直接跳过,因为插入后还是在原来的位置。

code:

#include <bits/stdc++.h>

#define int long long
#define N 100100
#define endl '\n'

using namespace std;

int n, m, id[N], rt, cnt, ch[N][2], fa[N], siz[N], key[N], val[N];

inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}

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

inline int node(int x)
{
	cnt ++;
	val[cnt] = x;
	key[cnt] = rand();
	siz[cnt] = 1;
	id[x] = cnt;//记录当前值在平衡树中的结点中的编号
	return cnt ;
}

inline void split(int x, int k, int &xx, int &yy, int fx = 0, int fy = 0)
{
	if(x == 0) return xx = yy = 0, void();//下面要维护父节点的信息 
	if(siz[ch[x][0]] + 1 <= k) fa[x] = fx, xx = x, split(ch[x][1], k - siz[ch[x][0]] - 1, ch[x][1], yy, x, fy);
	else fa[x] = fy, yy = x, split(ch[x][0], k, xx, ch[x][0], fx, x);
	push_up(x);
}
 
inline int merge(int x, int y)
{
	if(! x || ! y) return x + y;
	if(key[x] < key[y])
	{
		ch[x][1] = merge(ch[x][1], y);
		fa[ch[x][1]] = x;//维护父节点的信息 
		push_up(x);
		return x;
	}
	else
	{
		ch[y][0] = merge(x, ch[y][0]);
		fa[ch[y][0]] = y; 
		push_up(y);
		return y;
	}
}

inline int fid(int x)//查找当前编号的书上面有多少本书 
{
	int xx = x, res = siz[ch[x][0]] + 1;
	while(xx != rt && x)
	{
		if(ch[fa[x]][1] == x) res += siz[ch[fa[x]][0]] + 1;
		x = fa[x];
	}
	return res;
}

signed main()
{
	srand((unsigned)time(NULL));
	string op;
	n = read(), m = read();
	for(int i = 1; i <= n; i ++)
	{
		int a = read();
		rt = merge(rt, node(a));
	}
	for(int i = 1; i <= m; i ++)
	{
		int x, y, z, z1, k, o;
		cin >> op;
		o = read();
//		if(op[0] != 'Q')
//			cout << "find:" << fid(id[o]) << endl;
		if(op[0] == 'T')
		{
			int xx = fid(id[o]);//找到上面有几本书之后按排名分裂 
			split(rt, xx, x, y);
			split(x, xx - 1, x, z);
			rt = merge(z, merge(x, y));//把当前编号的书放到顶端 
		}
		if(op[0] == 'B')
		{
			int xx = fid(id[o]);//同理把当前编号的书放到最下面 
			split(rt, xx, x, y, 0);
			split(x, xx - 1, x, z, 0);
			rt = merge(merge(x, y), z);
		}
		if(op[0] == 'I')
		{
			k = read();
			int xx = fid(id[o]);
			if(k == 0) continue;//放回原位就直接跳过 
			if(k == 1)//上面的书多了一本 
			{
				split(rt, xx + 1, x, y);//相当于和在平衡树中的后继互换位置 
				split(x, xx, x, z);
				split(x, xx - 1, x, z1);
				rt = merge(merge(merge(x, z), z1), y);
			}
			if(k == -1)//上面的书少了一本 
			{
				split(rt, xx, x, y);//相当于和在平衡树中的前驱互换位置 
				split(x, xx - 1, x, z);
				split(x, xx - 2, x, z1);
				rt = merge(merge(merge(x, z), z1), y);
			}
		}
		if(op[0] == 'A')
		{
			int xx = fid(id[o]);//查找上面有多少本书 
			cout << xx - 1 << endl;//因为里面的是包含自己的,所以要减去 
		}
		if(op[0] == 'Q')
		{
			split(rt, o, x, y);//按照o的排名分裂 
			int xx = x;
			while(ch[xx][1]) xx = ch[xx][1];//一直跳到最大值 
			cout << val[xx] << endl;
			rt = merge(x, y);
		}
	}
	return 0;
}