平衡树

例题:【模板】普通平衡树

你需要动态地维护一个可重集合 \(M\),并且提供以下操作:

  1. \(M\) 中插入一个数 \(x\)
  2. \(M\) 中删除一个数 \(x\)(若有多个相同的数,应只删除一个)。
  3. 查询 \(M\) 中有多少个数比 \(x\) 小,并且将得到的答案加一。
  4. 查询如果将 \(M\) 从小到大排列后,排名位于第 \(x\) 位的数。
  5. 查询 \(M\)\(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)。
  6. 查询 \(M\)\(x\) 的后继(后继定义为大于 \(x\),且最小的数)。

对于操作 3,5,6,不保证当前可重集中存在数 \(x\)

本来是可以用二叉搜索树来做的,但最坏情况下操作复杂度会达到 \(O(n)\)(那真的根本没必要用二叉搜索树了)。

所以引出平衡树,它是通过一定操作维持树的高度(平衡性)来降低操作复杂度的二叉搜索树。\(Splay\)\(Treap\) 是维护平衡的两种树。

什么是平衡性?

不同的平衡树中的平衡有不同的定义,二叉搜索树中的平衡指以 T 为根节点的树,每一个结点的左子树和右子树高度差最多为 1。

Splay

Splay 树,或 伸展树,是一种平衡二叉查找树,它通过 伸展(splay)操作 不断将某个节点旋转到根节点,使得整棵树仍然满足二叉查找树的性质,能够在均摊 \(O(\log N)\) 时间内完成插入、查找和删除操作,并且保持平衡而不至于退化为链。

struct splay{
	int fa, son[2], val, cnt, siz;
//     父亲  0左1右   值  个数  子树大小
}t[maxn];

rotate

旋转操作。本质就是将某个节点上移一个位置。

旋转操作需要保证

  1. 整棵Splay的中序遍历不变(不破坏二叉查找树的性质。
  2. 受影响的节点维护的信息依然正确有效。
  3. root必须只想旋转后的节点。

点击查看代码
void rotate(int x)
{
	int y=t[x].fa, z=t[y].fa;
	int k=t[y].son[1]==x;
	t[z].son[t[z].son[1]==y]=x;
	t[x].fa=z;
	t[y].son[k]=t[x].son[k^1];
	t[t[x].son[k^1]].fa=y;
	t[x].son[k^1]=y;
	t[y].fa=x;
	pushup(y), pushup(x);
}

splay

\(splay(x,king)\) 是将x转到king的子节点,如果king为0,就把x转到根节点。

三种情况:

点击查看代码
void splay(int x,int king)
{
	while(t[x].fa!=king)
	{
		int y=t[x].fa, z=t[y].fa;
		if(z!=king)
		{
			if((t[z].son[0]==y)^(t[y].son[0]==x)) rotate(x);
			else rotate(y);
		}
		rotate(x);
	}
	if(king==0) root=x;
}

大致就是这两种操作,他可以使得复杂度维持到 \(O(nlogn)\) ,至于为啥,可以肤浅的理解为这样操作不会使得它退化为一个链状结构,降低了复杂度。详细证明请百度。

例题代码实现
#include<bits/stdc++.h>
#define int long long
#define endl "\n" 
using namespace std;
const int maxn=1e5+5;
int n;
struct splay{
	int fa, son[2], val, cnt, siz;
}t[maxn];
int root, tot;
void pushup(int x)
{
	t[x].siz=t[t[x].son[0]].siz+t[t[x].son[1]].siz+t[x].cnt;
}
void rotate(int x)
{
	int y=t[x].fa, z=t[y].fa;
	int k=t[y].son[1]==x;
	t[z].son[t[z].son[1]==y]=x;
	t[x].fa=z;
	t[y].son[k]=t[x].son[k^1];
	t[t[x].son[k^1]].fa=y;
	t[x].son[k^1]=y;
	t[y].fa=x;
	pushup(y), pushup(x);
}
void splay(int x,int king)
{
	while(t[x].fa!=king)
	{
		int y=t[x].fa, z=t[y].fa;
		if(z!=king)
		{
			if((t[z].son[0]==y)^(t[y].son[0]==x)) rotate(x);
			else rotate(y);
		}
		rotate(x);
	}
	if(king==0) root=x;
}
void find(int x)
{
	int u=root;
	if(!u) return ;
	while(t[u].son[x>t[u].val]&&x!=t[u].val)
	{
		u=t[u].son[x>t[u].val];
	}
	splay(u, 0);
}
void insert(int x)
{
	int u=root, fa=0;
	while(u&&t[u].val!=x)
	{
		fa=u;
		u=t[u].son[x>t[u].val];
	}
	if(u) t[u].cnt++;
	else
	{
		u=++tot;
		if(fa) t[fa].son[x>t[fa].val]=u;
		t[u].son[0]=t[u].son[1]=0;
		t[u].fa=fa, t[u].val=x, t[u].cnt=1, t[u].siz=1;
	}
	splay(u, 0);
}
int Next(int x,int opt)
{
	find(x);
	int u=root;
	if(t[u].val>x&&opt) return u;
	if(t[u].val<x&&!opt) return u;
	u=t[u].son[opt];
	while(t[u].son[opt^1]) u=t[u].son[opt^1];
	return u;
}
void delet(int x)
{
	int pre=Next(x, 0), suf=Next(x, 1);
	splay(pre, 0), splay(suf, pre);
	int del=t[suf].son[0];
	if(t[del].cnt>1)
	{
		t[del].cnt--;
		splay(del, 0);
	}
	else t[suf].son[0]=0;
}
int kth(int x)
{
	int u=root;
	if(t[u].siz<x) return 0;
	while(1)
	{
		int y=t[u].son[0];
		if(x>t[y].siz+t[u].cnt)
		{
			x-=t[y].siz+t[u].cnt;
			u=t[u].son[1];
		}
		else
		{
			if(t[y].siz>=x) u=y;
			else return t[u].val;
		}
	}
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n;
	insert(1e9), insert(-1e9);
	for(int i=1;i<=n;i++)
	{
		int opt, x;
		cin>>opt>>x;
		if(opt==1) insert(x);
		else if(opt==2) delet(x);
		else if(opt==3) 
		{
			find(x);
			cout<<t[t[root].son[0]].siz+(t[root].val<x?t[root].cnt:0)<<endl;
		}
		else if(opt==4) cout<<kth(x+1)<<endl;
		else if(opt==5) cout<<t[Next(x, 0)].val<<endl;
		else if(opt==6) cout<<t[Next(x, 1)].val<<endl;
	}
	return 0;
}

注释请看课件。

注意,这种题一般都会先insert最大值和最小值,否则会死循环!!!加上之后排名也要+1!!!!

区间splay

相当于把序列的标号排,每个点的权值为那个标号的数值。例如对 \([1,5,4,2,3]\) 构造splay数为:

基本的操作和普通splay树的操作没有区别。

split

核心操作就是提取一段区间 \([l,r]\)\(kth(l-1)\) splay到根,再把 \(kth(r+1)\) splay到根的右儿子,那么 \(kth(r+1)\) 的坐姿书就是这段区间。画一下图即可理解。

然后可以向线段树一样记录懒标记来进行区间修改。

例题1:文艺平衡树

操作为翻转区间,考虑先把这个区间提取出来,然后给这个区间子树的根打上标记,向下pushdown。pushdown具体可以维护一个标记0/1,每次下传就^1,并交换左右子树即可。注意在查找kth时下传标记。

练习1:[NOI2005] 维护数列

感觉线段树和splay要傻傻分不清了。首先是插入子序列操作,如果直接insert复杂度为 \(O(nlogn)\) ,考虑现将这些数像建线段树一样建一个平衡树,这样他肯定是平衡的。在插入的时候,同样将insert操作里的数先建成平衡树,然后接到要插入的点下即可:

void build(int l,int r,int fa)
{
	int mid=(l+r)>>1, now=id[mid]; 
	if(l==r)
	{
		t[now].sum=t[now].ans=a[l];
		t[now].tag=1e9;
		t[now].rev=0;
		t[now].pre=t[now].suf=max(0ll, a[l]);
		t[now].siz=1;
	}
	if(l<mid) build(l, mid-1, mid);
	if(mid<r) build(mid+1, r, mid);
	t[now].val=a[mid];
	t[now].fa=id[fa];
	t[now].tag=1e9;
	pushup(now);
	t[id[fa]].son[mid>=fa]=now;
}

然后修改操作和文艺平衡树一样,打标机,在kth时pushdown就行了。还有一点是求区间最大字段和就像线段树一样pushup。注意要用空间换时间,搞一个回收机制。总体来说和线段树的操作还是非常非常相似的,区别就在于他是一棵二叉搜索树。

点击查看代码
//区间splay套山海经
#include<bits/stdc++.h>
#define int long long
#define endl "\n" 
using namespace std;
const int maxn=1e6+5;
int n, m, a[maxn], id[maxn], idx, top, rub[maxn];
struct splay{
	int fa, son[2], val, sum, siz, tag;
	int pre, suf, ans, rev;
	void clear()
	{
		son[0]=son[1]=fa=rev=val=sum=siz=pre=suf=ans=0;
		tag=1e9;	
	}
}t[maxn];
int root, tot;
int node()
{
	if(top==0) return ++idx;
	return rub[top--];
}
inline void pushup(int x)
{
	splay l=t[t[x].son[0]], r=t[t[x].son[1]];
	t[x].siz=l.siz+r.siz+1;
	t[x].sum=l.sum+r.sum+t[x].val;
	t[x].pre=max({l.pre, l.sum+r.pre+t[x].val});
	t[x].suf=max({r.suf, l.suf+r.sum+t[x].val});
	t[x].ans=max({l.ans, r.ans, l.suf+r.pre+t[x].val});
}
void build(int l,int r,int fa)
{
	int mid=(l+r)>>1, now=id[mid]; 
	if(l==r)
	{
		t[now].sum=t[now].ans=a[l];
		t[now].tag=1e9;
		t[now].rev=0;
		t[now].pre=t[now].suf=max(0ll, a[l]);
		t[now].siz=1;
	}
	if(l<mid) build(l, mid-1, mid);
	if(mid<r) build(mid+1, r, mid);
	t[now].val=a[mid];
	t[now].fa=id[fa];
	t[now].tag=1e9;
	pushup(now);
	t[id[fa]].son[mid>=fa]=now;
}
inline void change1(int x,int val)
{
	if(!x) return ;
	t[x].val=val;
	t[x].sum=val*t[x].siz;
	t[x].pre=t[x].suf=max(t[x].sum, 0ll);
	t[x].ans=max(val,t[x].sum);
	t[x].tag=val;
}
inline void change2(int x)
{
	swap(t[x].son[0], t[x].son[1]);
	swap(t[x].pre, t[x].suf);
	t[x].rev^=1;
}
inline void pushdown(int x)
{
	if(t[x].rev)
	{
		change2(t[x].son[0]);
		change2(t[x].son[1]);
		t[x].rev=0;
	}
	if(t[x].tag!=1e9)
	{
		change1(t[x].son[0], t[x].tag);
		change1(t[x].son[1], t[x].tag);
		t[x].tag=1e9; 
	}
}
inline void rotate(int x)
{
	int y=t[x].fa, z=t[y].fa;
	int k=t[y].son[1]==x;
	t[z].son[t[z].son[1]==y]=x;
	t[x].fa=z;
	t[y].son[k]=t[x].son[k^1];
	t[t[x].son[k^1]].fa=y;
	t[x].son[k^1]=y;
	t[y].fa=x;
	pushup(y), pushup(x);
}
inline void splay(int x,int king)
{
	while(t[x].fa!=king)
	{	
		int y=t[x].fa, z=t[y].fa;
		if(z!=king)
		{
			if((t[z].son[0]==y)^(t[y].son[0]==x)) rotate(x);
			else rotate(y);
		}
		rotate(x);
	}
	if(king==0) root=x;
}
inline int kth(int x)
{
	int u=root;
	if(t[u].siz<x) return 0;
	while(1)
	{
		pushdown(u);
		
		int y=t[u].son[0];
		if(x>t[y].siz+1)
		{
			x-=t[y].siz+1;
			u=t[u].son[1];
		}
		else
		{
			if(t[y].siz>=x) u=y;
			else return u;
		}
	}
}
inline void insert(int k,int len)
{
	for(int i=1;i<=len;i++) cin>>a[i];
	for(int i=1;i<=len;i++) id[i]=node();
	build(1, len, 0);
	int rt=id[(1+len)>>1];//直接连新平衡树的根
	int x=kth(k+1), y=kth(k+2);
	//cout<<"awa:"<<x<<" "<<y<<endl;
	splay(x, 0), splay(y, x);
	t[rt].fa=y;
	t[y].son[0]=rt;
	pushup(y), pushup(x); 
}
inline void erase(int x)
{
	if(t[x].son[0]) erase(t[x].son[0]);
	if(t[x].son[1]) erase(t[x].son[1]);
	rub[++top]=x;
	t[x].clear();
}
inline void delet(int k,int len)
{
	int x=kth(k), y=kth(k+len+1);
	splay(x, 0), splay(y, x);
	int node=t[y].son[0];
	erase(node);
	t[y].son[0]=0;
	pushup(y), pushup(x);
}
inline void mksm(int k,int len,int val)
{
	int x=kth(k), y=kth(k+len+1);
	splay(x, 0), splay(y, x);
	int node=t[y].son[0];
	change1(node, val);
	pushup(y), pushup(x);
}

inline void revers(int k,int len)
{
	int x=kth(k), y=kth(k+len+1);
	splay(x, 0), splay(y, x);
	int node=t[y].son[0];
	if(t[node].tag!=1e9) return ;
	change2(node);
	pushup(y), pushup(x);
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n>>m;
	t[0].ans=a[1]=a[n+2]=-1e9;
	for(int i=1;i<=n;i++) 
	{
		cin>>a[i+1];
	}
	for(int i=1;i<=n+2;i++) id[i]=i;
	build(1, n+2, 0);
	root=(1+n+2)>>1;
	idx=n+2;
	while(m--)
	{
		int pos, tot, val;
		string s;
		cin>>s;	
//		cout<<t[10].fa<<endl;
		if(s[0]=='I')
		{
			cin>>pos>>tot;
			insert(pos, tot);
		}
		else if(s[0]=='D')
		{
			cin>>pos>>tot;
			delet(pos, tot);
		}
		else if(s[0]=='M'&&s[2]=='K')
		{
			cin>>pos>>tot>>val;
		
			mksm(pos, tot, val);
		}
		else if(s[0]=='R')
		{
			cin>>pos>>tot;
			revers(pos, tot);
		}
		else if(s[0]=='G')
		{
			cin>>pos>>tot;
			int x=kth(pos), y=kth(pos+tot+1);
		//	cout<<x<<" "<<y<<endl;
			splay(x, 0), splay(y, x);
			cout<<t[t[y].son[0]].sum<<endl;
		}
		else
		{
			cout<<t[root].ans<<endl;
		}
	}
	return 0;
}
/*
9 11
2 -6 3 5 1 -5 -3 6 3 
GET-SUM 5 4
MAX-SUM
INSERT 8 3 -5 7 2
GET-SUM 1 6
DELETE 12 1
GET-SUM 1 6
MAKE-SAME 3 3 2
GET-SUM 1 6
REVERSE 3 6
GET-SUM 5 4
MAX-SUM
9 11
2 -6 3 5 1 -5 -3 6 3 
GET-SUM 5 4
MAX-SUM
INSERT 8 3 -5 7 2
MAX-SUM
DELETE 12 1
MAX-SUM
MAKE-SAME 3 3 2
MAX-SUM
REVERSE 3 6
GET-SUM 5 4
MAX-SUM
*/

注意:下传标记顺序:先在区间右端点全部修改好,打上tag,pushdown正规写法。

无旋Treap

Treap=tree+heap,即树堆,Treap中每个节点都有一个权值和一个优先级,权值满足二叉搜索树性质,优先级满足堆的性质。这个优先级是随机给的,所以打乱了节点的插入顺序,从而维持了它的平衡。

无旋treap,就是没有旋转操作的treap。它以两个最重要的操作(split和merge)解决了所有操作。

split

比较套路地判断p的权值(val)是否大于 v,如果 val<=v,那么已经确定 p 及其左子树是很分裂后的左treap,将 x 指向 p,因为右子树也可能部分 <=v ,所以继续分裂右子树。反之,亦然。
放一张很好懂的图:

注意:你可能会问x和y的意义是什么,观察上图,每次x和y传下的值都是分割后的点,它其实起到了连接红色边的作用。

点击查看代码
void split(int p,int v,int &x,int &y)
{
	if(!p)
	{
		x=y=0;
		return ;
	}
	if(t[p].val<=v)
	{
		x=p;
		split(t[x].r, v, t[x].r, y);
		pushup(x);
	}
	else
	{
		y=p;
		split(t[y].l, v, x, t[y].l);
		pushup(y);
	}
}

merge

把两个分开的树合并到一起去,并返回合并后的根节点,和线段树合并很像,甚至比它更简单。
因为原来的树已经满足二叉搜索树的性质,根据优先级考虑把哪个树放到上面。

**merge的顺序即为两个子树合并后在treap上的顺序,即 \(merge(a,b)≠merge(b,a)\)

点击查看代码
int merge(int x,int y)
{
	if(!x||!y) return x+y;
	if(t[x].rnd<t[y].rnd)
	{
		t[x].r=merge(t[x].r, y);
		pushup(x);
		return x;
	}
	else
	{
		t[y].l=merge(x,t[y].l);
		pushup(y);
		return y;
	}
}

例题代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int n, idx, root;
struct treap{
	int l, r, val, rnd, siz;
}t[maxn];
void newnode(int &x,int v)
{
	x=++idx;
	t[x].val=v;
	t[x].rnd=rand();
	t[x].siz=1;
}
void pushup(int x)
{
	t[x].siz=t[t[x].l].siz+t[t[x].r].siz+1;
}
void split(int p,int v,int &x,int &y)
{
	if(!p)
	{
		x=y=0;
		return ;
	}
	if(t[p].val<=v)
	{
		x=p;
		split(t[x].r, v, t[x].r, y);
		pushup(x);
	}
	else
	{
		y=p;
		split(t[y].l, v, x, t[y].l);
		pushup(y);
	}
	
}
int merge(int x,int y)
{
	if(!x||!y) return x+y;
	if(t[x].rnd<t[y].rnd)
	{
		t[x].r=merge(t[x].r, y);
		pushup(x);
		return x;
	}
	else
	{
		t[y].l=merge(x,t[y].l);
		pushup(y);
		return y;
	}
}
void insert(int v)
{
	int x, y, z;
	split(root, v, x, y);
	newnode(z, v);
	root=merge(merge(x, z), y);
}
void del(int v)
{
	int x, y, z;
	split(root, v, x, z);
	split(x, v-1, x, y);
	y=merge(t[y].l, t[y].r);
	root=merge(merge(x, y), z);
}
int kth(int v)
{
	int x, y;
	split(root, v-1, x, y);
	int ans=t[x].siz+1;
	root=merge(x, y);
	return ans;
}
int getval(int root,int v)
{
	if(v==t[t[root].l].siz+1)
		return t[root].val;
	else if(v<=t[t[root].l].siz)
		return getval(t[root].l, v);
	else return getval(t[root].r, v-t[t[root].l].siz-1);
}
int pre(int v)
{
	int x, y, s, ans;
	split(root, v-1, x, y);
	s=t[x].siz;
	ans=getval(x, s);
	root=merge(x, y);
	return ans;
}
int nxt(int v)
{
	int x, y, s, ans;
	split(root, v, x, y);
	ans=getval(y, 1);
	root=merge(x, y);
	return ans;
}
signed main()
{
	cin>>n;
	while(n--)
	{
		int opt, x;
		cin>>opt>>x;
		if(opt==1) insert(x);
		else if(opt==2) del(x);
		else if(opt==3) cout<<kth(x)<<"\n";
		else if(opt==4) cout<<getval(root, x)<<"\n";
		else if(opt==5) cout<<pre(x)<<"\n";
		else cout<<nxt(x)<<"\n";
	}
	return 0;
} 

序列无旋-treap

和区间splay有点像,但无旋treap的好处就是直接有split操作了,不需要另写一个。

insert

for(int i=1;i<=n;i++)
{
	cin>>a[i];
	root=merge(root, newnode(a[i]));
}

一种比较常见好写的插入方式,复杂度为 O(nlogn)。

split

这时的split就不在是按照值分裂了,而是按照下标(二叉搜索树上的顺序)

点击查看代码
void split(int p,int v,int &x,int &y)
{
	if(!p)
	{
		x=y=0;
		return ;
	}
	if(t[t[p].l].siz<v)
	{
		x=p;
		v-=t[t[p].l].siz+1;
		split(t[x].r, v, t[x].r, y);
	}
	else
	{
		y=p;
		split(t[y].l, v, x, t[y].l);
	}
	pushup(p);
}

find

一般序列里的每个元素的值和下标都是不一样的,插入是按照初始下标排的,但由于一些操作,它的顺序会发生变化,有时候我们需要根据一些元素的值来找它目前的下标。

点击查看代码
int find(int x)//这里的x是当前节点编号,即下文的id[val]
{
	int res=t[t[x].l].siz+1;
	while(t[x].fa)
	{
		if(x==t[t[x].fa].r) res+=t[t[t[x].fa].l].siz+1;
		x=t[x].fa;
	}
	return res;
}

同样的,关于fa和id的处理:

点击查看代码
int newnode(int &x,int v)
{
	x=++idx;
	t[x].val=v;
	t[x].rnd=rand();
	t[x].siz=1;
	id[v]=idx;
	return x;
}
void pushup(int x)
{
	t[x].siz=t[t[x].l].siz+t[t[x].r].siz+1;
	if(t[x].l) t[t[x].l].fa=x;
	if(t[x].r) t[t[x].r].fa=x;
}

练习

1.[ZJOI2006] 书架

板子题,由于当时没有很搞懂merge,卡了好久。学到了find和比较常用的提取一个点的方法:

int x, y, z;
split(root, k, x, y);
split(x, k-1, x, z);
root=merge(x, merge(y, z));//z就是提取的点

2.[TJOI2013] 最长上升子序列

讨论区都说建议降蓝,但我还是想不出来>﹏<。比较巧妙地就是将dp值扔到treap上,多维护一个值max表示最大dp值(treap按照dp值排序),每输入一个数,就把小于等于这个数的子树(x)分裂出来,那么以当前点结尾的答案就为 \(t[x].max+1\) ,记得pushup时把max带上。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int n, m, idx, root, a[maxn], id[maxn], dp[maxn];
struct treap{
	int l, r, val, rnd, siz, fa, pos;
}t[maxn];
int newnode(int &x,int v)
{
	x=++idx;
	t[x].val=v;
	t[x].rnd=rand();
	t[x].siz=1;
	return x;
}
void pushup(int x)
{
	t[x].siz=t[t[x].l].siz+t[t[x].r].siz+1;
	t[x].val=max(max(t[t[x].l].val, t[t[x].r].val),dp[t[x].pos]);
}
void split(int p,int v,int &x,int &y)
{
	if(!p)
	{
		x=y=0;
		return ;
	}
	if(t[t[p].l].siz<v)
	{
		x=p;
		v-=t[t[p].l].siz+1;
		split(t[x].r, v, t[x].r, y);
	}
	else
	{
		y=p;
		split(t[y].l, v, x, t[y].l);
	}
	pushup(p);
}
int merge(int x,int y)
{
	if(!x||!y) return x+y;
	if(t[x].rnd<t[y].rnd)
	{
		t[x].r=merge(t[x].r, y);
		pushup(x);
		return x;
	}
	else
	{
		t[y].l=merge(x, t[y].l);
		pushup(y);
		return y;
	}
}
void insert(int val,int i)
{
	int x, y, z;
	split(root, val, x, y);
	int v=t[x].val;
	newnode(z, v+1);
	dp[i]=v+1;
	t[idx].pos=i;
	root=merge(merge(x, z), y);
}
signed main()
{
	srand(time(0));
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		insert(a[i], i);
		cout<<t[root].val<<endl;
	}
	return 0;
} 

3.【模板】树套树

人生的第一道树套树居然没时间写总结了。

4.星系探索

忘了欧拉序,想得完全不和正解沾边。首先把这张图按照依赖关系看成一棵树,那么要做的操作就是根链查询、把一棵子树移到另一个节点下面、一个子树全部+val。显然如果求出这个树的欧拉序:


例如这棵树的欧拉序为1,2,4,7,7,8,8,9,9,4,5,5,2,3,6,6,3,1

那么就把这个问题转换到序列上了。对于欧拉序上相同的两个数,前面数(st[i])的权值为该节点对应的权值,后面(ed[i])的则为相反数。那么查询操作就是从st[1]到st[u]的和,例如查询8,答案即为{1,2,4,7,7,8}这一段的权值之和。把一棵子树移到另一个节点下面就是将一个区间移动到另一个点的后面。由这些操作可以联想到平衡树,可以用序列无旋treap来维护。区间加的操作可以像线段树一样维护一个懒标记后pushdown。
需要注意换父亲操作导致欧拉序中节点顺序改变,无旋Treapsplit的时候不能直接split(st[x]),要在树上先求 st[x] 现在排在序列的位置(即到根路径上左子树大小+1的和),然后再按这个排名split。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+5;
int n, m, idx, a[maxn], id[maxn], tot, w[maxn], st[maxn], ed[maxn];
int head[maxn], edgenum;
struct edge{
    int next;
    int to;
}edge[maxn<<1];
void add_(int from,int to)
{
    edge[++edgenum].next=head[from];
    edge[edgenum].to=to;
    head[from]=edgenum;
}
struct Treap{
	int l, r, rnd, siz, val, fa;
	int opt, sum, sumt, tag;
}t[maxn];
struct fhqTreap{
	int root;
	int newnode(int v,int tt)
	{
		int x=++idx;
		t[x].rnd=rand();
		t[x].siz=1;
		t[x].val=t[x].sum=v*tt;
		t[x].opt=t[x].sumt=tt;
		return x;
	}
	void pushup(int x)
	{
		t[x].siz=t[t[x].l].siz+t[t[x].r].siz+1;
		t[x].sum=t[t[x].l].sum+t[t[x].r].sum+t[x].val;
		t[x].sumt=t[t[x].l].sumt+t[t[x].r].sumt+t[x].opt;
		if(t[x].l) t[t[x].l].fa=x;
		if(t[x].r) t[t[x].r].fa=x;
	}
	void add(int x,int val)
	{
		t[x].val+=val*t[x].opt;
		t[x].sum+=val*t[x].sumt;
		t[x].tag+=val; 
	}
	void pushdown(int x)
	{
		if(t[x].tag)
		{
			add(t[x].l, t[x].tag);
			add(t[x].r, t[x].tag);
			t[x].tag=0;
		}
	}
	void split(int p,int v,int &x,int &y)
	{
		if(!p)
		{
			x=y=0;
			return ;
		}
		pushdown(p);
		if(t[t[p].l].siz<v)
		{
			x=p;
			v-=t[t[p].l].siz+1;
			split(t[x].r, v, t[x].r, y);
		}
		else
		{
			y=p;
			split(t[y].l, v, x, t[y].l);
		}
		pushup(p);
	}
	int merge(int x,int y)
	{
		pushdown(x), pushdown(y);
		if(!x||!y) return x+y;
		if(t[x].rnd<t[y].rnd)
		{
			t[x].r=merge(t[x].r, y);
			pushup(x);
			return x;
		}
		else
		{
			t[y].l=merge(x,t[y].l);
			pushup(y);
			return y;
		}
	}
	int getkth(int x)
	{
		int res=t[t[x].l].siz+1;
		while(t[x].fa)
		{
			if(x==t[t[x].fa].r) res+=t[t[t[x].fa].l].siz+1;
			x=t[x].fa;
		}
		return res;
	}
	void dfs(int u,int fa)
	{
		st[u]=newnode(w[u], 1);
		root=merge(root, st[u]);
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			if(v==fa) continue;
			dfs(v, u);
		}
		ed[u]=newnode(w[u], -1);
		root=merge(root, ed[u]);
	}
	int query(int v)
	{
		int x, y;
		split(root, getkth(st[v]), x, y);
		t[x].fa=t[y].fa=0;
		int ans=t[x].sum;
		root=merge(x, y);
		t[root].fa=0;
		return ans;
	}
	void change(int u,int v)
	{
		int x, y, z;
		split(root, getkth(st[u])-1, x, y);
		t[x].fa=t[y].fa=0;
		split(y, getkth(ed[u]), y, z);
		t[z].fa=t[y].fa=0;
		root=merge(x, z);
		t[root].fa=0;
		split(root, getkth(st[v]), x, z);
		t[x].fa=t[z].fa=0;
		root=merge(merge(x, y), z);
		t[root].fa=0;
	}
	void modify(int u,int v)
	{
		int x, y, z;
		split(root, getkth(st[u])-1, x, y);
		t[x].fa=t[y].fa=0;
		split(y, getkth(ed[u]), y, z);
		t[z].fa=t[y].fa=0;
		add(y, v);
		root=merge(merge(x, y), z);
		t[root].fa=0;
	}
}treap;
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	srand(time(0));
	cin>>n;
	for(int i=2;i<=n;i++)
	{
		int ff;
		cin>>ff;
		add_(ff, i);
		add_(i, ff);
	}
	for(int i=1;i<=n;i++)
	{
		cin>>w[i];
	}
	treap.dfs(1, 0);
	//for(int i=1;i<=n;i++) cout<<st[i]<<" "<<ed[i]<<endl;
	cin>>m;
	while(m--)
	{
		char opt;
		int x, y;
		cin>>opt;
		if(opt=='Q')
		{
			cin>>x;
			cout<<treap.query(x)<<endl;
		}
		else if(opt=='C')
		{
			cin>>x>>y;
			treap.change(x, y);
		}
		else
		{
			cin>>x>>y;
			treap.modify(x, y);
		}
	}
	return 0;
} 
/*
3
1 1
4 5 7
5
Q 2
F 1 3
Q 2
C 2 3
Q 2
*/

5.[HNOI2004] 宠物收养场

简单了很多。自己写了一遍后WA20,调了好久才发现领养者也可以在宠物收养场等,本来想着建两棵treap,但发现建1棵就行,只记录数量多的那一方。写了写但没过,题解写法也很简单,改后就过了。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5, mod=1000000;
int n, idx, root, ans, cnt;
struct treap{
	int l, r, val, siz, rnd;
}t[maxn]; 
void newnode(int &x,int v)
{
	x=++idx;
	t[x].siz=1;
	t[x].rnd=rand();
	t[x].val=v;
}
void pushup(int x)
{
	t[x].siz=t[t[x].l].siz+t[t[x].r].siz+1;
}
void split(int p,int v,int &x,int &y)
{
	if(!p)
	{
		x=y=0;
		return;
	}
	if(t[p].val<=v)
	{
		x=p;
		split(t[x].r, v, t[x].r, y);
	}
	else
	{
		y=p;
		split(t[y].l, v, x, t[y].l);
	}
	pushup(p);
}
int merge(int x,int y)
{
	if(!x||!y)	return x+y;
	if(t[x].rnd<t[y].rnd)
	{
		t[x].r=merge(t[x].r, y);
		pushup(x);
		return x;
	}
	else
	{
		t[y].l=merge(x, t[y].l);
		pushup(y);
		return y;
	}
}
void insert(int v)
{
	int x, y, z;
	split(root, v, x, y);
	newnode(z, v);
	root=merge(merge(x, z), y);
}
void del(int v)
{
	int x, y, z;
	split(root, v, x, z);
	split(x, v-1, x, y);
	y=merge(t[y].l, t[y].r);
	root=merge(merge(x, y), z);
}
int getval(int root,int v)
{
	if(v==t[t[root].l].siz+1)
		return t[root].val;
	else if(v<=t[t[root].l].siz)
		return getval(t[root].l, v);
	else return getval(t[root].r, v-t[t[root].l].siz-1);
}
int pre(int v)
{
	int x, y, s, ans;
	split(root, v, x, y);
	s=t[x].siz;
	ans=getval(x, s);
	root=merge(x, y);
	return ans;
}
int nxt(int v)
{
	int x, y, s, ans;
	split(root, v-1, x, y);
	ans=getval(y, 1);
	root=merge(x, y);
	return ans;
}
signed main()
{
	cin>>n;
	insert(-1e18);
	insert(1e18);
	int flag=0;
	for(int i=1;i<=n;i++)
	{
		int a, b;
		cin>>a>>b;
		if(flag<=0)
		{
			if(!a) insert(b);
			else
			{
				int x=pre(b), y=nxt(b);
				if(x==-1e18&&y==1e18) continue;
				if(b-x<=y-b)
				{
					del(x);
					ans+=(b-x)%mod;
				}
				else
				{
					del(y);
					ans+=(y-b)%mod;
				}
				ans%=mod;	
			}
		}
		else if(flag>0)
		{
			if(a) insert(b);
			else
			{
				int x=pre(b), y=nxt(b);
				if(x==-1e18&&y==1e18) continue;
				if(b-x<=y-b)
				{
					del(x);
					ans+=(b-x)%mod;
				}
				else
				{
					del(y);
					ans+=(y-b)%mod;
				}
				ans%=mod;	
			}
		}
		flag+=(a?1:-1);
	}
	cout<<ans%mod;
	return 0;
}

“如果你驯服了我,我们就彼此需要了。”

posted @ 2025-03-20 20:42  zhouyiran2011  阅读(74)  评论(0)    收藏  举报