poj3580 SuperMemo splay --- 从入门到入土

(太可怕了,我只是一个刚学懂splay的蒟蒻)

poj地址: http://poj.org/problem?id=3580

AcWing地址:https://www.acwing.com/problem/content/268/

 

题意:

给你一个数列 A1~An

有几个操作:

输入格式

第一行包含一个整数 n。

接下来 n 行给出了序列中的数。

接下来一行包含一个整数 M,描述操作和询问的数量。

接下来 M 行给出了所有的操作和询问。

输出格式

对于每一个”MIN”询问,输出正确答案。

每个答案占一行。

数据范围:

0<n,m<=1e5

(这道题我完全是看这别人的博客打出来的orz...)

下面就说说我对这道题的感悟。。。

add操作

splay的规则使得平衡树在旋转过程中可以保证其正确性(节点左儿子小于节点,节点右儿子大于节点),因为splay也是二叉树,我们也可以在上面同时用线段树的做法。

但是有一点不同的是splay其实是在不断旋转的,同时符合平衡树性质也有很多不同的节点组合方法,在旋转过程中一个节点的左右子节点会发生变化

比如:

(2号节点的左右儿子在不断发生变化,但是之前我们使用线段树的时候是静态地维护树上的信息)

在线段树区间更新的时候,我们需要使用延迟标记,而对于不断变化的平衡树,则需要:

1.在旋转的时候需要及时的将需要旋转的节点的延迟标记更新给其子节点

2.需要将节点旋转到合适的地方使得我们能正确的使用延迟标记标记一段区间

因此假如要为l~r区间内所有项都加上一个数的话,首先我们需要将l~r区间放到一起,因此将l-1旋转到根,r+1旋转到根的右儿子,此时l~r就是根的右儿子的左儿子了。

然后打上lazy标记即可

 

翻转操作

假如要将[l,r]区间翻转的话,和add一样首先要找到l,r区间,因此将l-1区间splay到根节点,r+1区间splay到根节点的右儿子,此时l,r区间就得到了

那么翻转操作实际上如何在平衡树中体现呢,事实上平衡树的中序遍历的结果得到的就是原区间。

因此问题可以转化为对一棵二叉树我需要中序遍历得到其序列的翻转,那么二叉树要怎么调整呢?

看看中序遍历的代码

二叉树的中序遍历(递归)
void output(int x){
    output(左儿子)
    输出x
    output(右儿子)
}

假如需要逆序得到二叉树,那么可以这么改

二叉树的中序遍历(递归)
得到逆序
void output(int x){
    output(右儿子)
    输出x
    output(左儿子)
}

对比上面两个代码得知,我只需要将区间中每一个节点的左右儿子互换,那么我就可以将一个区间翻转了

所以对于上面得到的代表l~r区间的子树,将这棵子树里面每一个节点的左右儿子都互换即可得到逆序区间

同时,对于一个区间假如连续翻转两次的话,相当于没有翻转,这点暗示我们翻转也可以采用延迟标记的方法。

 

轮换操作:

比如说要轮换k次,那么只是将区间最右边k个数剪切到区间最左边即可

因此首先找到需要剪切的区间(通过splay将区间找到),然后剪下来(区间的父节点和区间之间的连线断开,然后保存剪下来的区间的根节点序号)

接着拼接到区间最左边,比如区间最左边的编号是l,那么我们需要找到 l-1 和 l的编号,然后分别splay(l-1,0) , splay(l,l-1),此时

因为l和l-1是相连的,因此ch[l][0] = 0 ,将剪下来的区间粘贴到 ch[l][0] 处即可(建立双方连线)

(这个具体可以看一下代码)

 

插入操作 :

同样和上面轮换操作的拼接类似,要插入一个点首先要找到那个点前后的两个节点,比如要在 x和y 之间插入一个点,那么就将x节点旋转到根,将y节点旋转到其父节点为x节点,即splay(x,0) , splay(y,x) , 此时因为本来x,y是相连的,因此ch[y][0] = 0 ,而我们只需要在ch[y][0]这个地方插入一个点即可(建立双方连线)

 

删除操作:

比如需要删除x节点,那么则需要将x-1旋转到根,将x+1旋转到其父亲节点为x-1即可,然后将ch[x+1][0]就是x节点,此时断开连接即可。

 

MIN操作;

同理,要查看[l,r]区间的min操作,首先要找到l,r区间,做法就是splay(l-1,0) , splay(r+1,l-1) ,然后因为对于节点我们有维护节点及其子节点的min值,此时直接输出ch[r+1][0]的min属性即可

 

最后一点感悟:

使用splay的时候为其初始化两个节点往往是非常好用的。

比如插入操作,需要在x后面插入一个值为val的点的时候,正常来说应该先检查一下x后面是否还有点,然后再决定是否splay,但是假如开始的时候就为序列两端初始化上-INF和INF,这个时候就不需要检查x后面是否还有点了,因为这是必然的。

 

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#define ll long long 
#define ull unsigned long long 
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 2e5 + 7;
int n, root, tot;
//struct node {
//	int f, siz, v, ch[2], minn;
//}tre[maxn];
//我发现封成结构体太特么难打了,就放弃了

//f:父亲节点 , siz:节点及其子节点的大小(节点数)  ch:记录左右儿子  minn:维护节点及其子节点的最小值
int f[maxn], siz[maxn], val[maxn], ch[maxn][2], minn[maxn];
// add的延迟标记  , 翻转的延迟标记
int lazy[maxn], rev[maxn];
int num[maxn];
void add_node(int ind,int v,int fat) {
	f[ind] = fat;
	ch[ind][0] = ch[ind][1] = lazy[ind] = rev[ind] = 0;
	val[ind] = minn[ind] = v;
	siz[ind] = 1;
}
//往上更新
void push_up(int ind) {
	if (!ind)
		return; ///?
	siz[ind] = 1, minn[ind] = val[ind];
	if (ch[ind][0]) {
		siz[ind] += siz[ch[ind][0]];
		minn[ind] = min(minn[ind], minn[ch[ind][0]]);
	}
	if (ch[ind][1]) {
		siz[ind] += siz[ch[ind][1]];
		minn[ind] = min(minn[ind], minn[ch[ind][1]]);
	}
}
void update_rev(int x) {
	swap(ch[x][0], ch[x][1]);
	rev[x] ^= 1;
}
void push_down(int ind) {
	if (!ind)
		return;
	if (lazy[ind]) {
		val[ch[ind][0]] += lazy[ind];
		val[ch[ind][1]] += lazy[ind];
		lazy[ch[ind][0]] += lazy[ind];
		lazy[ch[ind][1]] += lazy[ind];
		minn[ch[ind][0]] += lazy[ind];
		minn[ch[ind][1]] += lazy[ind];
		lazy[ind] = 0;
	}
	if (rev[ind]) {
		update_rev(ch[ind][0]);
		update_rev(ch[ind][1]);
		rev[ind] = 0;
	}
}
//初始化出一棵完美的平衡树
void build_tree(int &las,int l, int r,int fat) {	
	if (l > r)
		return;
	las = (l + r) >> 1;
	add_node(las, num[las], fat);
	build_tree(ch[las][0], l, las - 1, las);
	build_tree(ch[las][1], las + 1, r, las);
	push_up(las);
}
int GetIdByRank(int x) {
	int now = root;
	while (1) {
		push_down(now);
		if (x <= siz[ch[now][0]])
			now = ch[now][0];
		else if (x > siz[ch[now][0]] + 1) {
			x -= siz[ch[now][0]] + 1;
			now = ch[now][1];
		}
		else break;
	}
	return now;
}
void rotate(int x,int opt) { //opt: 0 左旋  1:右旋
	int y = f[x], z = f[y];
	push_down(y);
	//push_down(z);
	push_down(x);
	ch[y][!opt] = ch[x][opt];
	if (ch[x][opt])
		f[ch[x][opt]] = y;
	f[x] = z;
	if (z)
		ch[z][ch[z][1] == y] = x;
	f[y] = x;
	ch[x][opt] = y;
	push_up(y), push_up(x);
}
void splay(int x, int goal) {
	push_down(x);
	while (f[x] != goal) {
		int y = f[x], z = f[y];
		push_down(z);
		push_down(y);
		push_down(x);
		if (f[y] == goal)
			rotate(x, ch[y][0] == x);
		else {
			int p = ch[f[y]][0] == y;
			if (ch[y][p] == x) {
				rotate(x, !p);
				rotate(x, p);
			}
			else {
				rotate(y, p);
				rotate(x, p);
			}
		}
	}
	push_up(x);
	if (goal == 0)
		root = x;
}
//l,r区间里都加上一个数
void add(int l, int r, int x) {
	l = GetIdByRank(l - 1), r = GetIdByRank(r + 1);
	splay(l, 0), splay(r, l);
	if (ch[r][0]) {
		lazy[ch[r][0]] += x, minn[ch[r][0]] += x;
		val[ch[r][0]] += x;
	}
}
//翻转l,r区间
void Reverse(int l, int r) {
	l = GetIdByRank(l - 1),r = GetIdByRank(r + 1);
	splay(l, 0), splay(r, l);
	update_rev(ch[r][0]);
}
// l1: 最左边 [l2,r2]:需要剪下来拼接的区间
void Revolve(int l1, int l2, int r2) {
	l2 = GetIdByRank(l2 - 1), r2 = GetIdByRank(r2 + 1);
	splay(l2, 0), splay(r2, l2);
	int cpy = ch[r2][0];
	ch[r2][0] = 0;
	int sto= GetIdByRank(l1);
	l1 = GetIdByRank(l1 - 1);
	splay(l1, 0), splay(sto, l1);
	ch[sto][0] = cpy;
	f[cpy] = sto;
}
//x后面插入权值为v的节点
void Insert(int x, int v) {
	int id = GetIdByRank(x), iid = GetIdByRank(x + 1);
	splay(id, 0), splay(iid, id);
	add_node(++tot, v, iid);
	ch[iid][0] = tot;
	push_down(iid), push_up(iid);
	push_down(id), push_up(id);
	//for (int i = iid; i; i = f[i])
	//	push_down(i), push_up(i);
	splay(iid, 0); 
}
//删除x节点
void Delete(int x) {
	int y = GetIdByRank(x + 1);
	x = GetIdByRank(x - 1);
	splay(x, 0), splay(y, x);
	int aim = ch[y][0];
	f[aim] = siz[aim] = val[aim] = ch[aim][0] = ch[aim][1] = minn[aim] = 0;
	lazy[aim] = rev[aim] = 0;
	ch[y][0] = 0;
	push_up(y);
	push_up(x);
}
// 得到区间最小值
int getMin(int l,int r) {
	l = GetIdByRank(l - 1), r = GetIdByRank(r + 1);
	splay(l, 0), splay(r, l);
	return minn[ch[r][0]];
}
int main() {
	cin >> n;
	num[1] = -INF;
	for (int i = 2; i <= n + 1; i++) 
		scanf("%d", num + i);
	tot = n + 2;
	num[tot] = INF;
	//构建完美的平衡树
	build_tree(root, 1, tot, 0);
	push_up(root);
	char s[10];
	int inp1, inp2, inp3;
	int m; cin >> m;
	while (m--) {
		scanf("%s", s);
		//因为完美的平衡树从节点2开始,所以所有的操作下标都要+1
		if (s[0] == 'A') {
			scanf("%d %d %d", &inp1, &inp2, &inp3);
			add(inp1 + 1, inp2 + 1, inp3);
		}
		else if (s[0] == 'R') {
			scanf("%d %d", &inp1, &inp2);
			if (s[3] == 'E')
				Reverse(inp1 + 1, inp2 + 1);
			else {
				scanf("%d", &inp3);
				inp3 %= (inp2 - inp1 + 1);
				if (inp3)
					Revolve(inp1 + 1, inp2 - inp3 + 2, inp2 + 1);
			}
		}
		else if (s[0] == 'I') {
			scanf("%d %d", &inp1, &inp2);
			Insert(inp1 + 1, inp2);
		}
		else if (s[0] == 'D') {
			scanf("%d", &inp1);
			Delete(inp1 + 1);
		}
		else {
			scanf("%d %d", &inp1, &inp2);
			printf("%d\n", getMin(inp1 + 1, inp2 + 1));
		}
	}
	return 0;
}

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值