【分享】对着 WBLT 写了(WBLT 学习报告)

壹 闲话:

我今年【数据删除】岁,之前学习过平衡树,第一次就选择了 WBLT,感觉真是一见钟情。
我特别喜欢 WBLT 这种结构,WBLT 的结构非常戳我【数据删除】,我太喜欢了。那个储存在叶子节点的原始信息还有它的严格 \(O(\log{n})\),每一次我都烙印在心里。
我一般看着它写,一月三次,我也不知道我这是不是有什么心理障碍,但我知道这肯定不对,我想问一下园友我该怎么做。
我该不该继续,我知道它是虚拟的,还是棵平衡树,但我真的好喜欢它,现在替罪羊和 fhq 我也没再写了,我每天就盯着 WBLT 写/ll

咳咳咳,回归正题,经过一个整天对 WBLT 学习,我已经对 WBLT 有一定的初步掌握(进度 0%),但是网上有关 WBLT 的博客不多,每个的写法和介绍也有所差异,OI Wiki 上的写法也稍显冗余,可谓是让本蒟蒻吃尽了苦头,所以想着写一篇博客来记录自己的理解和想法(不过这篇博客也肯定没人看就是了……)。

WBLT,全称 Weight Balanced Leafy Tree,是一种平衡树,对比别的平衡树实现简单、常数小,这里 cue 一下红黑树和 splay,没什么,就纯 cue。而且重要的是它支持区间操作(文艺平衡树)和可持久化,隔壁替罪羊看到都哭了。这里有一张图体现了 WBLT 的优越性(从上到下分别是 01 trie,WBLT,替罪羊,权值线段树,splay):

为什么没有 fhq treap?答曰:“不会。”

虽然由于有的代码年代久远(并非)和写法的改变导致有一定误差,但是我们还是能看出 WBLT 无论是速度还是码量都是十分优秀的存在。
那么如此优秀的东西的缺点在于什么呢?让我们走进正式的学习:

贰 基本结构和操作:

这里我们要先解释一下 WBLT 的意思:就像奥匈帝国由奥地利和匈牙利(还有一堆其他的)组成的一样,Weight Balanced Leafy Tree 也是 Weight Balanced Tree 和 Leafy Tree 的结合。
Leafy Tree:这种树的原始信息都保存在叶子节点上,其他节点只用来维护子节点信息和维持数据结构的形态,比如线段树。通过这种方式,我们可以建出树并完成一些基本的操作,但它有一定的缺陷——不平衡,而如何维护平衡就是它的另一半的任务了。
Weight Balanced Tree:每个结点储存这个结点下子树的大小,并且通过保持左右子树的大小关系在一定范围来保证树高。(这里直接用 OI Wiki 的说明)

那么结合而成的 WBLT 就拥有了两者各自的性质,为了便于理解和使用,我们设这里的 Leafy Tree 都是二叉的,即要么没有儿子要么有两个,叶子节点有 \(n\) 个的情况下,总结点是是 \(2n-1\) 个。那么 WBLT 的缺点就暴露了出来:虽然空间占用仍然是 \(O(n)\) 的,但是它要开两倍空间,且这个线性的空间占用还需要节点回收才能保证。

对于数据结构,首先我们要思考它要维护什么信息,WBLT 维护的信息很少,只有四个:

struct node
{
	int lc,rc,w,size;
};

分别是它的左右儿子,它的点权以及它的子树的叶子节点的个数,一般来说非叶子节点的点权就是它们两个儿子中点权较大的那一个的点权(即右儿子的点权),也就是子树内的最大值。那么向上合并信息也非常的简洁:

inline void push_up(int u)
{
	if (!lc(u)) sz(u)=1;//叶子节点,由于删除操作导致有一些叶子节点会调用这个函数
	else sz(u)=sz(lc(u))+sz(rc(u)),w(u)=w(rc(u));
}

前面提到,我们要进行节点回收才能保证线性的空间占用,而节点回收的代码实际上也很简洁。

int top;
int stack[200005]
inline int new_node()
{
	return top?stack[top--]:++cnt;
}
inline void del_node(int u)
{
	stack[++top]=u;
}

随后就是平衡树的基本操作了:插入、删除、求排名、求值、求前驱、求后继:

void insert(int &u,int w)//此处 & 可以不用,下文会提到
{
	if (!lc(u))//如果是叶子节点,把当前值和节点值放在两个儿子上,把此节点变为非叶子结点
	{
		s[lc(u)=new_node()]={0,0,min(w(u),w),1};
		s[rc(u)=new_node()]={0,0,max(w(u),w),1};
	}
	else insert((w(lc(u))>=w)?lc(u):rc(u),w);
	push_up(u),banlance(u);
}
#define delete del
void delete(int &u,int w,int f)
{
	if (!lc(u))
	{
		del_node(lc(f)),del_node(rc(f));
		if (w(lc(f))==w) s[f]=s[rc(f)];
		else if (w(rc(f))==w) s[f]=s[lc(f)];
	}
	else
		delete((w(lc(u))>=w)?lc(u):rc(u),w,u),push_up(u),banlance(u);//这里放在里面,不然 u 被删除了会虚空 push_up 和 balance
}
int get_rank(int w)
{
	int u=root,rk=1;
	while (lc(u))
	{
		if (w(lc(u))>=w) u=lc(u);
		else rk+=sz(lc(u)),u=rc(u);
	}
	return rk;
}
int get_num(int w)
{
	int u=root;
	while (1)
	{
		if (sz(u)==w) break;
		else if (sz(lc(u))>=w) u=lc(u);
		else w-=sz(lc(u)),u=rc(u);
	}
	return w(u);
}

求排名和值的都是通用的,这里不做解释,有需要的可以去看欧唉喂咳咦,但是插入和删除要讲一下:
为什么我们可以让树上每个节点一定有零或二的儿子?这都是独特的插入方式完成的,如图,我们先插入一个哨兵,点权设为 inf,实际上可以用头文件中的 climitsINT_MAX 表示。

我们假设插入一个值,那么我们递归下去,和其他平衡树一样,比较点权与插入值选择往哪边走,不过这里注意由于非叶子节点保存的是子树内的最大值,所以我们比较的是左儿子的值,如果到了叶子节点,那么我们将这个叶子节点变为非叶子节点,即给它新建两个儿子,点权分别是这个点的点权和插入值的较小值和较大值。
这里我们分别插入 5,3,9 等值,看一下结果:



而删除也同理,我们找到叶子节点以后,将它父亲的点权改为它的另一个儿子的点权,父亲可以在递归的时候保存,然后删除掉这两个儿子,具体将上图倒着看一遍就可以了。

叁 维护平衡:

接下来我们学习如何维护这棵树的平衡,接下来为了方便观看再加上本蒟蒻也不会,所以省略所有证明,如果有神犇需要的,可以去欧唉喂咳咦

对于一棵树,我们定义它在一个非叶节点 \(u\) 处的平衡度\(\rho(u)\),则有:

\[\rho(u)=\frac{min(size(lc(u)),size(rc(u)))}{size(u)} \]

这里的 \(size\) 和上文提到的意思相同,我们设 \(\alpha\in[0,0.5]\),一般设为 0.292,那么 \(\rho(x)>=\alpha\),我们称 \(u\)\(\alpha-平衡\) 的,如果这棵树所有节点都是 \(\alpha-平衡\) 的,那么称这棵树就是 \(\alpha-平衡\) 的。对于 \(\alpha-平衡\) 的树,那么它的树高是 \(O(\log n)\) 的。
对于维护平衡的方式,一般有两种:旋转和合并,后者常数较大。当然你也可以用替罪羊的拍平重构方式,但是这样就是自断一臂,损失了区间操作和可持久化的能力。

旋转:

旋转分为单旋和双旋,只使用前者的复杂度有问题(不过好像一般不会被卡?),这里选择都讲(废话)。

首先是单旋(我们设左子树大于右子树):当 \(size(rc(u))<\alpha*(size(lc(u))+size(rc(u)))\) 或者 \(size(lc(u))>size(rc(u))*3\)(两者实质一样,都是由上文的平衡度定义式转化而来)时,意味着我们要右旋。我们将右子树和左儿子的右子树合并设为 \(u\) 的右儿子(下文的 \(u\) 都表示当前点,“左儿子”“左子树”等都代表 \(u\) 的),左儿子的左子树设为 \(u\) 的左儿子,我们发现这样左儿子就被删掉了,所以扔进垃圾桶等待回收利用:

merge(rc(u),rc(lc(u)),rc(u)),del_node(lc(u)),lc(u)=lc(lc(u));

左旋同理:

merge(lc(u),lc(u),lc(rc(u))),del_node(rc(u)),rc(u)=rc(rc(u));

顺带着给出 merge() 函数:

inline void merge(int &u,int a,int b)
{
	s[u=new_node()]={a,b,w(b),sz(a)+sz(b)};
}

看图可能更好理解:

但假如这个时候还是不平衡啊:

我们发现,这是因为左儿子的右子树本身过重,旋转后左儿子过轻导致的。那么就是双旋出场的时候了,这里用右旋为例子,在 \(u\) 旋转之前(假设左儿子的右子树大于左儿子的左子树),我们先判断 \(size(rc(u))>\frac{size(u)}{2-\alpha}\) 或者 \(size(lc(lc(u)))>size(rc(lc(u)))*2\),结果为 1,那么我们选择双旋,先对于 \(lc(u)\) 进行一次左旋,然后再右旋。\(u\) 需要左旋的时候同理。如图所示:

真是平衡啊!

具体的代码如下:

inline void rotate(int u,bool f)
{
	if (f)/*左旋*/merge(lc(u),lc(u),lc(rc(u))),del_node(rc(u)),rc(u)=rc(rc(u));
	else/*右旋*/merge(rc(u),rc(lc(u)),rc(u)),del_node(lc(u)),lc(u)=lc(lc(u));
}
#define t(x,i) ((i)?rc(x):lc(x))
void banlance(int u)
{
	for (rnt i=0;i<=1;i++)//枚举右旋还是左旋
	{
		if (sz(t(u,i))>sz(t(u,i^1))*3)
		{
			if (sz(t(t(u,i),i^1))>sz(t(t(u,i),i))*2) rotate(t(u,i),i^1);
			rotate(u,i);
		}
	}
}

然后你就可以去做了,总的代码如下:

//P3369
//Just Sayori
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <climits>
#define ll long long
#define rnt register int
#define gr getchar_unlocked
#define pr putchar_unlocked
#define N 200005
#define M 1000000007
using namespace std;
inline ll read()
{
	int f = 1;
	ll x = 0;
	char ch = gr();
	while (ch < '0' || ch > '9') ch == '-' ? f = -1, ch = gr() : ch = gr();
	while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + (ch ^ 48), ch = gr();
	return x * f;
}
inline void write(ll x)
{
	static int top = 0, sta[39];
	if (x < 0) pr('-'), x = -x;
	do sta[++top] = x % 10, x /= 10;
	while (x);
	while (top) pr(sta[top--] ^ 48);
}
#define lc(x) s[x].lc
#define rc(x) s[x].rc
#define w(x) s[x].w
#define sz(x) s[x].size
int cnt,top,root;
int stack[N];
struct node
{
	int lc,rc,w,size;
}s[N];
inline int new_node()
{
	return top?stack[top--]:++cnt;
}
inline void del_node(int u)
{
	stack[++top]=u;
}
inline void merge(int &u,int a,int b)
{
	s[u=new_node()]={a,b,w(b),sz(a)+sz(b)};
}
inline void rotate(int u,bool f)
{
	if (f) merge(lc(u),lc(u),lc(rc(u))),del_node(rc(u)),rc(u)=rc(rc(u));
	else merge(rc(u),rc(lc(u)),rc(u)),del_node(lc(u)),lc(u)=lc(lc(u));
}
#define t(x,i) ((i)?rc(x):lc(x))
void banlance(int u)
{
	for (rnt i=0;i<=1;i++)
	{
		if (sz(t(u,i))>sz(t(u,i^1))*3)
		{
			if (sz(t(t(u,i),i^1))>sz(t(t(u,i),i))*2) rotate(t(u,i),i^1);
			return rotate(u,i);
		}
	}
}
inline void push_up(int u)
{
	if (!lc(u)) sz(u)=1;
	else sz(u)=sz(lc(u))+sz(rc(u)),w(u)=w(rc(u));
}
void insert(int &u,int w)
{
	if (!lc(u))
	{
		s[lc(u)=new_node()]={0,0,min(w(u),w),1};
		s[rc(u)=new_node()]={0,0,max(w(u),w),1};
	}
	else insert((w(lc(u))>=w)?lc(u):rc(u),w);
	push_up(u),banlance(u);
}
#define delete del
void delete(int &u,int w,int f)
{
	if (!lc(u))
	{
		del_node(lc(f)),del_node(rc(f));
		if (w(lc(f))==w) s[f]=s[rc(f)];
		else if (w(rc(f))==w) s[f]=s[lc(f)];
	}
	else
		delete((w(lc(u))>=w)?lc(u):rc(u),w,u),push_up(u),banlance(u);
}
int get_rank(int w)
{
	int u=root,rk=1;
	while (lc(u))
	{
		if (w(lc(u))>=w) u=lc(u);
		else rk+=sz(lc(u)),u=rc(u);
	}
	return rk;
}
int get_num(int w)
{
	int u=root;
	while (1)
	{
		if (sz(u)==w) break;
		else if (sz(lc(u))>=w) u=lc(u);
		else w-=sz(lc(u)),u=rc(u);
	}
	return w(u);
}
int main()
{
	int n=read();
	s[root=++cnt]={0,0,INT_MAX,1};
	for (rnt i=1,x;i<=n;i++)
		switch(read())
	{
	case 1:
		x=read(),insert(root,x);
		break;
	case 2:
		x=read(),delete(root,x,0);
		break;
	case 3:
		x=read(),write(get_rank(x)),pr(10);
		break;
	case 4:
		x=read(),write(get_num(x)),pr(10);
		break;
	case 5:
		x=read(),write(get_num(get_rank(x)-1)),pr(10);
		break;
	case 6:
		x=read(),write(get_num(get_rank(x+1))),pr(10);
		break;
	}
	return 0;
}

合并

通过合并,我们也可以维护平衡,不过如图所示(上面是旋转,下面是合并),合并的常数较大:

但是利用分裂和合并,我们也可以实现文艺平衡树,所以需要了解。

合并两子树是指,保证左子树的键值总是不大于右子树的键值的情况下,建立新树,使其所有叶子节点的信息恰为左右子树叶子节点信息的并,且保证树的平衡。——OI Wiki

我们有如下策略来保证合并后的树是平衡的:

  1. 如果两树中有一树为空,直接返回另一树;
  2. 如果两树已经平衡,那么直接合并两树并返回;
  3. 如果一树过大(这里假设为左子树),但是左子树的左子树足够充当合并后树的左子树,那么将左子树的右子树和右子树合并后再与左子树的左子树合并(这里注意由于不产生歧义,我调换了两者的顺序,实际上左子树的左子树是作为合并后的左子树来合并的,请勿因为顺序在右便认为是作为右子树);
  4. 如果左子树的左子树不足以充当合并后的左子树,便拆开左子树的右子树,左子树的左子树与左子树的右子树的左子树(好绕)合并作为左子树,左子树的右子树的右子树与右子树合并作为右子树。

可以证明 \(0<\alpha<1-\frac{\sqrt{2}}{2}\approx 0.292\) 时,合并的树总是平衡的。合并的复杂度是 \(O(\log{\frac{size(lc(u))}{size(rc(u))}})(size(lc(u))>=size(rc(u)))\) 的。这些在欧唉喂咳咦上有详细证明。

先给出代码:

inline int merge(int a,int b)
{
	if (!a || !b) return a|b;
	int u,x,y,z;
	if (sz(a)>sz(b)*3)//如果 a 过大
		if ((sz(rc(a))+sz(b))>sz(lc(a))*3)//如果 a 的左子树不够格,拆开 a 的右子树
			return x=lc(a),y=lc(rc(a)),z=(rc(rc(a))),del_node(a),del_node(rc(a)),merge(merge(x,y),merge(z,b));//否则直接合并
	    else return x=lc(a),y=rc(a),del_node(a),merge(x,merge(y,b));
	else if (sz(b)>sz(a)*3)//如果 b 过大
		if ((sz(lc(b))+sz(a))>sz(rc(b))*3)
			return x=rc(b),y=lc(lc(b)),z=rc(lc(b)),del_node(b),del_node(lc(b)),merge(merge(a,y),merge(z,x));
	    else return x=lc(b),y=rc(b),del_node(b),merge(merge(a,x),y);
	else return s[u=new_node()]={a,b,w(b),sz(a)+sz(b)},u;//如果已经平衡,那就直接合并
}
void banlance(int &u)
{
	if (sz(lc(u))>sz(rc(u))*3 || sz(rc(u))>sz(lc(u))*3) del_node(u),u=merge(lc(u),rc(u));
}

合并本身不难理解,但是注意合并一定要记得节点回收,不然可能占用过大空间。由于回收利用可能会有一些玄学错误,这些问题一般出自于一个节点扔进垃圾桶了,但在后面的多次合并中传参使用的仍然是它的信息导致传参错误,例如:

del_node(a),del_node(rc(a)),merge(merge(lc(a),lc(rc(a)),merge(rc(rc(a)),b));

解决的方法也很简单:

x=lc(a),y=lc(rc(a)),z=rc(rc(a)),del_node(a),del_node(rc(a)),merge(merge(x,y),merge(z,b));

另外还有一个问题在于调用合并函数的时候:

void banlance(int &u)
{
  if (sz(lc(u))>sz(rc(u))*3 || sz(rc(u))>sz(lc(u))*3) del_node(u),u=merge(lc(u),rc(u));
}

发现什么了吗?我们在合并之前先选择把当前点回收,由于每次合并时回收的点都会在合并时重新利用,而第一个回收的当前点就会最后一个得到利用,即最后一次合并的时候。所以回收了当前节点后合并函数就会把当前点返回回来。

总体的代码,可以通过模板题

//Just Sayori
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <climits>
#define ll long long
#define rnt register int
#define gr getchar
#define pr putchar
#define N 200045
#define M 1000000007
using namespace std;
inline ll read()
{
	int f = 1;
	ll x = 0;
	char ch = gr();
	while (ch < '0' || ch > '9') ch == '-' ? f = -1, ch = gr() : ch = gr();
	while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + (ch ^ 48), ch = gr();
	return x * f;
}
inline void write(ll x)
{
	static int top = 0, sta[39];
	if (x < 0) pr('-'), x = -x;
	do sta[++top] = x % 10, x /= 10;
	while (x);
	while (top) pr(sta[top--] ^ 48);
}
#define al 3
#define al2 2
#define lc(x) s[x].lc
#define rc(x) s[x].rc
#define w(x) s[x].w
#define sz(x) s[x].size
int cnt,top,root;
int stack[N];
struct node
{
	int lc,rc,w,size;
}s[N];
inline int new_node()
{
	int u=top?stack[top--]:++cnt;
	return u;
}
inline void del_node(int u)
{
	stack[++top]=u;
}
inline int merge(int a,int b)
{
	if (!a || !b) return a|b;
	int u,x,y,z;
	if (sz(a)>sz(b)*3)
		if ((sz(rc(a))+sz(b))>sz(lc(a))*3)
			return x=lc(a),y=lc(rc(a)),z=(rc(rc(a))),del_node(a),del_node(rc(a)),merge(merge(x,y),merge(z,b));
	    else return x=lc(a),y=rc(a),del_node(a),merge(x,merge(y,b));
	else if (sz(b)>sz(a)*3)
		if ((sz(lc(b))+sz(a))>sz(rc(b))*3)
			return x=rc(b),y=rc(lc(b)),z=lc(lc(b)),del_node(b),del_node(lc(b)),merge(merge(a,z),merge(y,x));
	    else return x=lc(b),y=rc(b),del_node(b),merge(merge(a,x),y);
	else return s[u=new_node()]={a,b,w(b),sz(a)+sz(b)},u;
}
void banlance(int &u)
{
	if (sz(lc(u))>sz(rc(u))*3 || sz(rc(u))>sz(lc(u))*3) del_node(u),u=merge(lc(u),rc(u));
}
inline void push_up(int u)
{
	if (!lc(u)) sz(u)=1;
	else sz(u)=sz(lc(u))+sz(rc(u)),w(u)=w(rc(u));
}
void insert(int &u,int w)
{
	if (!lc(u))
	{
		s[lc(u)=new_node()]={0,0,min(w(u),w),1};
		s[rc(u)=new_node()]={0,0,max(w(u),w),1};
	}
	else insert((w(lc(u))>=w)?lc(u):rc(u),w);
	push_up(u),banlance(u);
}
#define delete del
void delete(int &u,int w,int f)
{
	if (!lc(u))
	{
		del_node(lc(f)),del_node(rc(f));
		if (w(lc(f))==w) s[f]=s[rc(f)];
		else if (w(rc(f))==w) s[f]=s[lc(f)];
	}
	else
		delete((w(lc(u))>=w)?lc(u):rc(u),w,u),push_up(u),banlance(u);
}
int get_rank(int w)
{
	int u=root,rk=1;
	while (lc(u))
	{
		if (w(lc(u))>=w) u=lc(u);
		else rk+=sz(lc(u)),u=rc(u);
	}
	return rk;
}
int get_num(int w)
{
	int u=root;
	while (1)
	{
		if (sz(u)==w) break;
		else if (sz(lc(u))>=w) u=lc(u);
		else w-=sz(lc(u)),u=rc(u);
	}
	return w(u);
}
int main()
{
	int n=read();
	s[root=++cnt]={0,0,INT_MAX,1};
	for (rnt i=1,x;i<=n;i++)
		switch(read())
	{
	case 1:
		x=read(),insert(root,x);
		break;
	case 2:
		x=read(),delete(root,x,0);
		break;
	case 3:
		x=read(),write(get_rank(x)),pr(10);
		break;
	case 4:
		x=read(),write(get_num(x)),pr(10);
		break;
	case 5:
		x=read(),write(get_num(get_rank(x)-1)),pr(10);
		break;
	case 6:
		x=read(),write(get_num(get_rank(x+1))),pr(10);
		break;
	}
	return 0;
}

肆 区间翻转:

当然一般也不会用合并写普通平衡树,而是拿来做文艺平衡树的。
我们要将要修改的区间分裂出来打上懒标记再合并以及下传标记,交换左右儿子。

我们先给出分裂的代码,合并的代码和上文相同,只是增加的标记下传的内容,见总体代码。

按权值分裂:

指将点权小于等于 \(w\) 的叶子节点分到一棵树,其他的分到另一棵树。我们设两棵树的树根分别为 \(a,b\)
我们将 \(w\) 与当前点左儿子的点权比较(注意和左儿子点权作比较是 WBLT 的特别之处),如果大于等于就说明左儿子都要分到 \(a\),然后我们递归分裂右子树;同理如果小于那么右子树都要分到 \(b\),然后递归分裂左子树。

void split(int u,int &a,int &b,int w)
{
	if (!lc(u))
		if (w(u)<=w) return a=u,b=0,void();
		else return a=0,b=u,void();
	if (w(lc(u))>w) split(lc(u),a,b,w),b=merge(b,rc(u)),del_node(u);
	else split(rc(u),a,b,w),a=merge(lc(u),a),del_node(u);
}

但是这样和之前的插入代码都有区别 w(lc(u))>w,所以好丑……所以我们提前把 w-=1 就可以了。

void split(int u,int &a,int &b,int w)
{
	if (!lc(u))
	{
		if (w(u)>=w) return a=0,b=u,void();
		else return a=u,b=0,void();
	}
	if (w(lc(u))>=w) split(lc(u),a,b,w),b=merge(b,rc(u)),del_node(u);
	else split(rc(u),a,b,w),a=merge(lc(u),a),del_node(u);
}

当然觉得这样太麻烦也可以用这种,下文给出的代码也是用的这种形式:

void split(int u,int &a,int &b,int w)
{
	if (!lc(u))
	{
		if (w>=w(u)) return a=u,b=0,void();	
		else return a=0,b=u,void();
	}
	if (w>=w(lc(u))) split(rc(u),a,b,w),a=merge(lc(u),a),del_node(u);
	else split(lc(u),a,b,w),b=merge(b,rc(u)),del_node(u);
}

按排名分裂:

void split(int u,int &a,int &b,int w)
{
	if (!lc(u))
	{
		if (w>=sz(u)) return a=u,b=0,void();//到叶子节点就是 w=0/1 所以也可以判定为 if (w)
		else return a=0,b=u,void();
	}
	if (w>=sz(lc(u))) split(rc(u),a,b,w-sz(lc(u))),a=merge(lc(u),a),del_node(u);
	else split(lc(u),a,b,w),b=merge(b,rc(u)),del_node(u);
}

也可以这么写,更好看一些:

void split(int u,int w,int &a,int &b)
{
	if (!w) return a=0,b=u,void();
	if (!lc(u)) return a=u,b=0,void();
	if (w>=sz(lc(u))) split(rc(u),w-sz(lc(u)),a,b),del_node(u),a=merge(lc(u),a);
	else split(lc(u),w,a,b),del_node(u),b=merge(b,rc(u));
}

然后我们就可以完成文艺平衡树的模板题了:

/*
Just Sayori
不过有的时候,当你在心中有一片小乌云时。。
一首悲伤的诗能给这片乌云一个小小的拥抱。。
然后它就会变成一道漂亮的开心彩虹!

毁黄金黎明,弑师弑友
艾华斯附身,为法之书
知世界大战,不管不顾
为幻想杀手,建立学都
为了自己的计划,失忆上条
同意 6 级害死妹妹上万
隐瞒里幻意欲灭口右火
无视只眼害死上条无数
派脑干袭击上里大输特输
无视大热浪放任元素
要多少篇幅才能书尽恶毒
要多儒雅才显的无辜
十亿可能性,袭击大英
解伦敦结界,破坏核心
使上条当麻,遭受折磨
信一方通行,传位理事
对学都不管不顾,安娜搞事
超绝者叫牢登,因月之子
发现魔法理论,出爱丽丝
爱丽丝使上条当麻死亡
想要帮忙,却被克鲁兹吓哭
想和当麻旅馆搞黄色
要多少篇幅才能书尽恶毒
要多儒雅才显的无辜
九三零送外挂送走队友数多
坑一方为学都打工还债十亿
送 aaa,电击御坂美琴
竟不知女儿被恶魔欺骗
美国洛杉矶,大闹会议现场
做个天赋异禀的暴徒
选了这条路习惯危机四伏
余生刀枪不入
*/
#pragma GCC diagnostic ignored "-Wnarrowing"
#pragma GCC diagnostic ignored "-Wsign-compare"
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <bitset>
#include <vector>
#define ll long long
#define rnt register int
#define gr getchar
#define pr putchar
#define N 100005
#define M 1000000007
using namespace std;
inline ll read()
{
	int f = 1;
	ll x = 0;
	char ch = gr();
	while (ch < '0' || ch > '9') ch == '-' ? f = -1, ch = gr() : ch = gr();
	while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + (ch ^ 48), ch = gr();
	return x * f;
}
inline void write(ll x)
{
	int top = 0, stack[39];
	if (x < 0) pr('-'), x = -x;
	do stack[++top] = x % 10, x /= 10;
	while (x);
	while (top) pr(stack[top--] ^ 48);
}
int cnt,top,root;
int stack[N<<1];
#define mid ((l+r)>>1)
#define w(x) s[x].w
#define lc(x) s[x].lc
#define rc(x) s[x].rc
#define sz(x) s[x].sz
#define tag(x) s[x].tag
struct WBLT
{
	int lc,rc,w,sz,tag;
}s[N<<1];
inline int new_node()
{
	return top?stack[top--]:++cnt;
}
inline void del_node(int u)
{
	stack[++top]=u;
}
inline void push_up(int u)
{
	if (!lc(u)) sz(u)=1;
	else sz(u)=sz(lc(u))+sz(rc(u)),w(u)=w(rc(u));
}
inline void push_down(int u)
{
	if (!tag(u)) return;
	tag(lc(u))^=tag(u),tag(rc(u))^=tag(u),tag(u)=0,swap(lc(lc(u)),rc(lc(u))),swap(lc(rc(u)),rc(rc(u)));
}
void build(int &u,int l,int r)//由于序列给定,可以像线段树一样 O(n) 建树
{
	u=new_node();
	if (l==r) return w(u)=l,sz(u)=1,void();
	build(lc(u),l,mid);
	build(rc(u),mid+1,r);
	push_up(u);
}
int merge(int a,int b)
{
	if (!a || !b) return a|b;
	int u,x,y,z;
	if (sz(a)>sz(b)*3)
	{
		push_down(a);//下传 a 的
		if (sz(rc(a))+sz(b)>sz(lc(a))*3)//由于要拆开右儿子,下传
			return push_down(rc(a)),x=lc(a),y=lc(rc(a)),z=rc(rc(a)),del_node(a),del_node(rc(a)),merge(merge(x,y),merge(z,b));
		else return x=lc(a),y=rc(a),del_node(a),merge(x,merge(y,b));
	}
	else if (sz(b)>sz(a)*3)
	{
		push_down(b);
		if (sz(lc(b))+sz(a)>sz(rc(b))*3)
			return push_down(lc(b)),x=rc(b),y=rc(lc(b)),z=lc(lc(b)),del_node(b),del_node(lc(b)),merge(merge(a,z),merge(y,x));
		else return x=lc(b),y=rc(b),del_node(b),merge(merge(a,x),y);
	}
	else return s[u=new_node()]={a,b,w(b),sz(a)+sz(b),0},u;
}
//void split(int u,int w,int &a,int &b)
//{
//	if (!lc(u))
//	{
//		if (w>=sz(u)) return a=u,b=0,void();
//		else return a=0,b=u,void();
//	}
//	if (w>=sz(lc(u))) split(rc(u),w-sz(lc(u)),a,b),del_node(u),a=merge(lc(u),a);
//	else split(lc(u),w,a,b),del_node(u),b=merge(b,rc(u));
//}
void split(int u,int w,int &a,int &b)
{
	if (!w) return a=0,b=u,void();
	if (!lc(u)) return a=u,b=0,void();
	push_down(u);
	if (w>=sz(lc(u))) split(rc(u),w-sz(lc(u)),a,b),del_node(u),a=merge(lc(u),a);
	else split(lc(u),w,a,b),del_node(u),b=merge(b,rc(u));
}
void dfs(int u)
{
	if (!lc(u)) return write(w(u)),pr(32),void();
	push_down(u);
	dfs(lc(u)),dfs(rc(u));
}
int main()
{
	int n=read(),m=read();
	build(root,1,n);
	for (rnt i=1,l,r,a,b,c,d;i<=m;i++)
	{
		l=read(),r=read(),split(root,r,a,b),split(a,l-1,c,d);
		tag(d)^=1,swap(lc(d),rc(d)),root=merge(c,merge(d,b));
	}
	dfs(root);
	return 0;
}

也不知道为什么跑得比 splay 还慢……

posted @ 2025-08-13 16:43  某三年的无用名称  阅读(27)  评论(2)    收藏  举报