【小结】平衡树

以下所有内容都是 FHQ-Treap。

P3369 【模板】普通平衡树

#include<bits/stdc++.h>
using namespace std;
mt19937 rd(114514);//种子可以随机rd(time(NULL)) 也可以 
const int N=1e5+10;
int n,op,x,i,c,root;
struct Node{
	int l,r,val,size,rk;//rk 随机数,val 权值,l,r 左右孩子,size 子树大小
}tr[N];
void update(int k)
{
	tr[k].size=tr[tr[k].l].size+tr[tr[k].r].size+1;//左边子树+右边子树+自身 
}
int add_node(int val)//加入一个新点,单独的点,先不插入树里,返回节点编号 
{
	c++;//c 表示节点个数 
	tr[c].val=val;//val 就是这个点权值 
	tr[c].size=1;//大小 1 
	tr[c].rk=rd();//随机赋值,因为要控制随机二叉搜索树 
	return c;
}
void split(int k,int &a,int &b,int val)//把 k 子树内的点拆分成以 a 为根的 <=val,以 b 为根的 >val 
{
	if (k==0)
	{
		a=b=0;//如果节点不存在,那么都赋值为 0
		return;
	}
	if (tr[k].val<=val)//如果 a,第一次是直接赋值为 a,其他的就直接一个一个儿子传下去
	{
		a=k;
		split(tr[k].r,tr[k].r,b,val);//以 r 为根,分为 r 和 b 两个,让后把 r 的这个更新 r
	}
	else
	{
		b=k;
		split(tr[k].l,a,tr[k].l,val);//以 l 为根,分为 l 和 a 两个,然后把 l 更新 l
	}
	update(k);//更新子树大小,合并 size 
}
void merge(int &k,int a,int b)//合并两个根为 a,b 的树 
{
	if (a==0||b==0)
	{
		k=a+b;//相当于 a=0 返回 b,b=0 返回 a,就是单个节点时直接返回,一定会有这样的边界,因为每次都会没掉一个点
		return;
	}
    //a 做 b 的左儿子或 b 做 a 的右儿子
    //为了 log(n),随机用 rk 的大小决定
    //这样就几乎是 O(n*log(n)) 的
	if (tr[a].rk<=tr[b].rk)//如果随机顺序 <=
	{
		k=a;//a 成为这个东西的根节点
		merge(tr[a].r,tr[a].r,b);//把 r 和 b 合并,合并到 r
	}
	else//>
	{
		k=b;//b 成为这个的根节点
		merge(tr[b].l,a,tr[b].l);//把 l 和 a 合并,合并到 r
	}
	update(k);//注意更新 
}
void insert(int &k,int val)//把 val 插入树内 
{
	int a=0,b=0,cur=add_node(val);//cur 表示新建的点 
	split(k,a,b,val);//先分开 
	merge(a,a,cur);//然后插入到 a 的右边去 
	merge(k,a,b);//最后 a 并 b 
}
void del(int &k,int val)//删一个 val 
{
	int a=0,b=0,z=0;
	split(k,a,b,val);//拆 
	split(a,a,z,val-1);//z 就是 val 
	merge(z,tr[z].l,tr[z].r);//合并,取代根,删根 
	merge(a,a,z);//<=val 合并 
	merge(k,a,b);//整个合并 
}
int find_num(int k,int x)//排名 x 的数 
{
	//分治 
	while (tr[tr[k].l].size+1!=x)//注意 tr[tr[k].l]]+1 
	{
		if (tr[tr[k].l].size>=x) k=tr[k].l;//>=x 不是 >x
		else
		{
			x-=tr[tr[k].l].size+1;//+1加自己别忘了
			k=tr[k].r;//调到右儿子
		}
	}
	return tr[k].val;//最后找到了,返回 val
}
int find_rank(int &k,int val)//val 排名 
{
	int a=0,b=0;
	split(k,a,b,val-1);//分 
	int tmp=tr[a].size+1;//排名 +1 
	merge(k,a,b);//还原,别忘了 
	return tmp;//要把 tmp 存下来
}
int pre(int &k,int val)//val 前驱 
{
	int a=0,b=0;
	split(k,a,b,val-1);//val-1
	int tmp=find_num(a,tr[a].size);//<val 最后一个 
	merge(k,a,b);//还原 
	return tmp;
}
int nxt(int &k,int val)//val 后继 
{
	//与 pre 同理 
	int a=0,b=0;
	split(k,a,b,val);
	int tmp=find_num(b,1);
	merge(k,a,b);
	return tmp;
}
int main()
{
	scanf("%d",&n);
	//调用函数 
    //root 一直等于 0,但在 merge() 的时候变换
    //新生成一个点有可能改变根
    //root 一开始等于 0,0 也有 l,r 儿子,在弄得过程中换根保持平衡从而 O(log(n))
	for (i=1;i<=n;i++)
	{
		scanf("%d%d",&op,&x);
		if (op==1) insert(root,x);
		else if (op==2) del(root,x);
		else if (op==3) printf("%d\n",find_rank(root,x));
		else if (op==4) printf("%d\n",find_num(root,x));
		else if (op==5) printf("%d\n",pre(root,x));
		else printf("%d\n",nxt(root,x));
	}
	return 0;
}

注:split() 有两种选择:分裂右儿子并分裂为 tr[k].r 和以上的 b,分裂左儿子同理,分裂为 tr[k].l 和以上的 a。注意 a 在前面。

merge() 也有两种选择:a 在 b 的上面,b 作为 a 的右儿子,和 tr[a].r 合并成为 rson。b 在 a 的上面,a 作为 b 的左儿子,和 tr[b].l 合并成 lson。

然后就是文艺平衡树。

文艺平衡树就是按下标分裂的平衡树,用来解决按下标操作的问题,这样直接按这个下标分裂,便于操作或找到一段区间。

split() 就是按照 \(size\) 来分。

因为原来的 \(val\) 保证了中序遍历,我可以直接用子树大小来分前 \(x\) 个。

区间翻转相当于一个子树的所有点的左右子树翻转。

所以每个点被翻转 \(m\) 次很浪费。

由于每一层都得翻转,所以打懒标记。

树的形态改变之前 pushdown()。

注意:我们分裂的并不是当前的权值,而是按大小分裂。这个东西具有单调性。而我们中间的合并纯粹是按下标合并,merge() 是随机的,树的形态会改变,但是永远保持下标的单调性。因为我是按照小,大传参的。val 表示当前的值,它改变了,但对我的 merge() 和 split() 并无影响。而下标永远保持单调性,所以最后的答案还是中序遍历。

文艺平衡树一开始顺序 merge(),就能保证下标的顺序。后面也是顺序传参,所以顺序不改变。

重要的是下标的单调性,而不是树的形态。

P3391 【模板】文艺平衡树

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,root,c,i,l,r;
mt19937 rd(time(NULL));
struct FHQ{
	int l,r,size,tag,val,rk;
}tr[N];
int add_node(int val)
{
	c++;
	tr[c].size=1;
	tr[c].rk=rd();
	tr[c].val=val;
	return c;
}
void update(int k)
{
	tr[k].size=tr[tr[k].l].size+tr[tr[k].r].size+1;
}
void pushdown(int k)
{
	if (tr[k].tag==0) return;
	swap(tr[k].l,tr[k].r);
	tr[tr[k].l].tag^=1;
	tr[tr[k].r].tag^=1;
	tr[k].tag=0;
}
void split(int k,int &a,int &b,int x)
{
	if (k==0)
	{
		a=b=0;
		return;
	}
	pushdown(k);
	if (tr[tr[k].l].size+1<=x)
	{
		a=k;
		split(tr[k].r,tr[k].r,b,x-tr[tr[k].l].size-1);
	}
	else
	{
		b=k;
		split(tr[k].l,a,tr[k].l,x);
	}
	update(k);
}
void merge(int &k,int a,int b)
{
	if (a==0||b==0)
	{
		k=a+b;
		return;
	}
	if (tr[a].rk<=tr[b].rk)
	{
		pushdown(a);
		k=a;
		merge(tr[a].r,tr[a].r,b);
	}
	else
	{
		pushdown(b);
		k=b;
		merge(tr[b].l,a,tr[b].l);
	}
	update(k);
}
void reverse(int &k,int l,int r)
{
	int a=0,b=0,z=0;
	split(k,a,b,r);
	split(a,a,z,l-1);
	tr[z].tag^=1;
	merge(a,a,z);
	merge(k,a,b);
}
void dfs(int k)
{
	if (k==0) return;
	pushdown(k);
	dfs(tr[k].l);
	printf("%d ",tr[k].val);
	dfs(tr[k].r);
}
int main()
{
	scanf("%d%d",&n,&m);
	for (i=1;i<=n;i++)
	{
		add_node(i);
		merge(root,root,i);
	}
	for (i=1;i<=m;i++)
	{
		scanf("%d%d",&l,&r);
		reverse(root,l,r);
	}
	dfs(root);
	return 0;
}

P3466 [POI 2008] KLO-Building blocks

枚举长度为 k 的区间 \([i-k+1,i]\),转化为求数轴上各点到一个点的距离。

容易发现,这个点选在中位数最好。

设中位数为 \(mid\),则贡献:

\(mid\times small-Small+Big-mid\times big\)

其中 \(big\) 表示大于 \(mid\) 的数的个数,\(small\) 表示小于等于 \(mid\) 的数的个数。\(Big\) 表示大于 \(mid\) 的数的和,\(Small\) 表示小于等于 \(mid\) 的和。

套路:求区间中位数,只需要动态删点,添点,平衡树维护。

AC 记录

#include<bits/stdc++.h>
using namespace std;
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
typedef long long ll;
const int N=5e5+10;
mt19937 rd(time(NULL));
int n,k,i,root,c;
ll a[N];
int pos;
ll mid,med,res;
struct FHQ{
	int l,r;
	ll val;
	int rk,size;
	ll sum;
}tr[N];
void update(int k)
{
	tr[k].size=tr[tr[k].l].size+tr[tr[k].r].size+1;
	tr[k].sum=tr[tr[k].l].sum+tr[tr[k].r].sum+tr[k].val;
}
void split(int k,int &a,int &b,ll val)
{
	if (k==0)
	{
		a=b=0;
		return;
	}
	if (tr[k].val<=val)
	{
		a=k;
		split(tr[k].r,tr[k].r,b,val);
	}
	else
	{
		b=k;
		split(tr[k].l,a,tr[k].l,val);
	}
	update(k);
}
void merge(int &k,int a,int b)
{
	if (a==0||b==0)
	{
		k=a+b;
		return;
	}
	if (tr[a].rk<=tr[b].rk)
	{
		k=a;
		merge(tr[a].r,tr[a].r,b);
	}
	else
	{
		k=b;
		merge(tr[b].l,a,tr[b].l);
	}
	update(k);
}
int add_cur(ll val)
{
	c++;
	tr[c].val=val;
	tr[c].rk=rd();
	tr[c].size=1;
	tr[c].sum=val;
	return c;
}
ll get_num(int k,int x)
{
	while (tr[tr[k].l].size+1!=x)
	{
		if (tr[tr[k].l].size>=x) k=tr[k].l;
		else
		{
			x-=tr[tr[k].l].size+1;
			k=tr[k].r;
		}
	}
	return tr[k].val;
}
void insert(int &k,ll val)
{
	int a=0,b=0,cur=add_cur(val);
	split(k,a,b,val);
	merge(a,a,cur);
	merge(k,a,b);
}
void del(int &k,ll val)
{
	int a=0,b=0,z=0;
	split(k,a,b,val);
	split(a,a,z,val-1);
	merge(z,tr[z].l,tr[z].r);
	merge(a,a,z);
	merge(k,a,b);
}
ll get_ans(ll mid)
{
	int a=0,b=0;
	split(root,a,b,mid);
	ll small=tr[a].size,big=tr[b].size;
	ll Small=tr[a].sum,Big=tr[b].sum;
	merge(root,a,b);
	return mid*small-Small+Big-mid*big;
}
int main()
{
	scan>>n>>k;
	for (i=1;i<=n;i++) scan>>a[i];
	for (i=1;i<=k;i++) insert(root,a[i]);
	mid=get_num(root,(k+1)/2);
	res=get_ans(mid);
	pos=k;
	for (i=k+1;i<=n;i++)
	{
		del(root,a[i-k]);
		insert(root,a[i]);
		med=get_num(root,(k+1)/2);
		if (get_ans(med)<res)
		{
			res=get_ans(med);
			mid=med;
			pos=i;
		}
	}
	printf("%lld\n",res);
	for (i=pos-k+1;i<=pos;i++) a[i]=mid;
	for (i=1;i<=n;i++) printf("%lld\n",a[i]);
	return 0;
}
posted @ 2026-08-21 16:51  GUO120822  阅读(2)  评论(0)    收藏  举报