FHQ非旋平衡树
目录
平衡树的灵魂:
平衡树的核心操作:
split:
merge:
对核心操作运用(split与merge)的例题:
思路:
代码:
无旋 treap 的区间操作:
建树:
区间翻转:
其他区间操作:
CF702F T-Shirts
题面:
思路:
code:
平衡树的灵魂:
在本人看来,就是rand()的运用了,其保证了其平衡性
按二叉搜索树的性质,一棵树的形状可能如图1所示:
图1
查询等操作需n的时间,n即点的数量
加上rank(rank由rand()随机赋值),令rank小的为rank大的祖先,可得图2
图2
这样,log n就可以解决问题了
平衡树的核心操作:
split:
平衡树的分裂,可以按各种关键值,这里为了方便,拿按值分裂举例
选定一个值v,小于等于v的分出一颗子树,大于v的为一棵子树
看代码之前,我们先画图感受一下分裂过程,如图3
图3
关键在于明白为什么右子树也有一部分小于v
//sz为树的大小,val为树根的值,ls rs为树的左右儿子
void pushup(int rt){
sz[rt]=sz[ls[rt]]+sz[rs[rt]]+1;
}
void split(int rt,int v,int &x,int &y){//x,y为分出的子树的树根
if(!rt){ x=y=0 ; return ;}//无树可分
if(val[rt]<=v){
x=rt;
split(x,v,rs[x],y);
}else{
y=rt;
split(y,v,x,ls[y]);
}
pushup(rt);
}
merge:
平衡树合并时要保证rank的升序,val的大小关系一般已经知道(注:由于合并操作一般在分裂后马上进行,故分裂出的以x为树根的子树的val小于以y为树根的val)
//rk代表rank
void merge(int x,int y){
if( !x || !y ){ return x|y ;} // 其中一个树为空,则直接返回不为空的树
if( rk[x]<rk[y] ){
x=merge(rs[x],y);//y的val大于x的,为x的右子树
pushup(x);
return x;
}else{
y=merge(x,ls[rt]);//x的val小于y的,为y的左子树
pushup(y);
return y;
}
}
有了split与merge后,许多操作就只是对它们的运用而已,
对核心操作运用(split与merge)的例题:
洛谷 P2584 [ZJOI2006] GameZ游戏排名系统
这里我讲一讲GameZ游戏排名系统
思路:
split 时不再是单一的比较,首先比较玩家的游戏得分,然后是时间;
//x代表当前比较的树根的编号,v为分裂的依据
struct st{
int val,tim;
};
bool judge(int x,st v){
if(val[x].val==v.val) return val[x].tim<=v.tim;
else return val[x].val>v.val;
}
void split(int rt,st v,int &x,int &y){
if(rt==0) { x=y=0; return ;}
if(judge(rt,v)){
x=rt;
split(rs[rt],v,rs[rt],y);
}
else{
y=rt;
split(ls[rt],v,x,ls[rt]);
}
pushup(rt);
}
弄清这个后其余就是基操了,嘿嘿
代码:
#include <bits/stdc++.h>
#define re register int
#define INF 0x3f3f3f3f
#define N 260000
using namespace std;
int read(){
int x = 0,f = 1;char ch = getchar();
while (ch < '0' || ch>'9'){
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9'){
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
int n;
struct st{
int val,tim;
};
map<string,st>pl;
map<pair<int,int>,string>pls;
struct Tree{
int root,tot,rk[N],ls[N],rs[N],sz[N];
st val[N];
void pushup(int rt){sz[rt]=sz[ls[rt]]+sz[rs[rt]]+1; }
int getnew(st v){
val[++tot]=v;
sz[tot]=1,rk[tot]=rand();
return tot;
}
bool judge(int x,st v){
if(val[x].val==v.val) return val[x].tim<=v.tim;
else return val[x].val>v.val;
}
void split(int rt,st v,int &x,int &y){
if(rt==0) { x=y=0; return ;}
if(judge(rt,v)){
x=rt;
split(rs[rt],v,rs[rt],y);
}
else{
y=rt;
split(ls[rt],v,x,ls[rt]);
}
pushup(rt);
}
int merge(int x,int y){
if(!x||!y) return x|y;
if(rk[x]<rk[y]){
rs[x]=merge(rs[x],y);
pushup(x);
return x;
}
else{
ls[y]=merge(x,ls[y]);
pushup(y);
return y;
}
}
void insert(st v){
int x,y;
split(root,v,x,y);
root=merge(merge(x,getnew(v)),y);
}
void remove(st v){
int x,y,z;
split(root,v,x,y);
v.tim--;
split(x,v,x,z);
root=merge(x,y);
}
int getrank(st v){
int x,y;
v.tim--;
split(root,v,x,y);
int ans=sz[x]+1;
root=merge(x,y);
return ans;
}
int getsz(){return sz[root];}
string find(int rank){
int rt=root;
while(rt){
if(sz[ls[rt]]+1==rank) break;
else if(sz[ls[rt]]>=rank) rt=ls[rt];
else rank-=(sz[ls[rt]]+1),rt=rs[rt];
}
return pls[make_pair(val[rt].val,val[rt].tim)];
}
}tree;
int main(){
n=read();
for(int i=1;i<=n;i++){
string s;
cin>>s;
if(s[0]=='+'){
s.erase(s.begin());
st w;
w.val=read(),w.tim=i;
if(pl.count(s))
tree.remove(pl[s]);
pl[s]=w;
pls[make_pair(w.val,w.tim)]=s;
tree.insert(w);
}
else{
s.erase(s.begin());
if(s[0]<='Z'&&s[0]>='A') printf("%d\n" ,tree.getrank(pl[s]));
else{
int num=0;
for(int j=0;j<s.size();j++) num=num*10+s[j]-'0';
for(int j=0;j<10;j++){
if(tree.getsz()<num+j) break;
cout<<tree.find(num+j)<<" ";
}
printf("\n");
}
}
}
}
无旋 treap 的区间操作:
建树:
在建树或者插入点的时候,我们可以用暴力,一个个加,时间复杂度O(n log n)
但在更具难度的题目中,要插入的点可能很多,所以我们需要更优秀的算法
容易想到将要插入的点变成一条长链插入,但是若只是普通的链,难免在merge中被分 裂开(merge在rank的影响下,读者自己想想原因),这样就跟一个个插入没有区别了, 所以我们需要构造一条在merge不会多余操作的神仙链,
int build(int a[],int siz){//a[]: 插入的点集 siz:点集的大小
stack<int>s;//使用单调栈,保证rank递增
int rt,last; //last: 链头
for(int i=1;i<=siz;i++){
rt=getnew(a[i]);//新建点
last=0;
while(!s.empty() && rk[s.top()]>rk[rt]){//在加入rt前,将rk大于rk[rt]的弹出,保证加入rt后栈保持单调递减
last=s.top();
pushup(last);
s.pop();
}
if(!s.empty()) rs[s.top()]=rt;//为了保证中序遍历不变(即 仍是原数组的顺序),我们让rt为是s.top()的右儿子
ls[rt]=last; //rt的左儿子为链
s.push(rt);
}
while(!s.empty()) {//处理掉栈内剩余的点即可
last=s.top();
pushup(last),s.pop();
}
return last;//返回链头
}
//构造链 O(n),而后 merge 为 O(log n)
//总复杂度为 O(n+log n) ,即 O(n)
区间翻转:
无旋 treap 相比旋转 treap 的一大好处就是可以实现各种区间操作,下面我们以文艺平衡树的 模板题 为例,介绍 treap 的区间操作。
在这道题目中,我们需要实现的是区间翻转,那么我们首先需要考虑如何建树,建出来的树需要是初始的区间。
我们只需要把区间的下标依次插入 treap 中,这样在中序遍历(先遍历左子树,然后当前节点,最后右子树)时,就可以得到这个区间。如图4,序列为(3,2,4,7,1,6,5)
图4
若翻转区间[2,6],序列变为(3,6,1,7,4,2,5),恰好为交换4节点的左右儿子的中序遍历,如图5
图5
在线段树中,我们一般在更新和查询时下传懒标记。这是因为,在更新和查询时,我们想要更新/查询的范围不一定和懒标记代表的范围重合,所以要先下传标记,确保查到和更新后的值是正确的。这时我们就需要pushdown来下传标记了
//turn为翻转的标记
void down_turn(int rt){
swap(ls[rt],rs[rt]);
turn[rt]^=1;
}
void pushdown(int rt){
if(turn[rt]){
if(ls[rt]) down_turn(ls[rt]);
if(rs[rt]) down_turn(rs[rt]);
turn[rt]^=1;
}
}
至于split与merge中对pushdown的运用在代码中我会详细介绍
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+1e2;
const int inf=0x3f3f3f3f;
int n,m;
struct Tree{
int ls[N],rs[N],val[N],rk[N],sz[N],turn[N],root,tot;
void pushup(int rt){sz[rt]=sz[ls[rt]]+sz[rs[rt]]+1;}
void down_turn(int rt){
swap(ls[rt],rs[rt]);
turn[rt]^=1;
}
void pushdown(int rt){
if(turn[rt]){
if(ls[rt]) down_turn(ls[rt]);
if(rs[rt]) down_turn(rs[rt]);
turn[rt]^=1;
}
}
int getnew(int v){
val[++tot]=v,sz[tot]=1,rk[tot]=rand();
return tot;
}
void split(int rt,int v,int &x,int &y){
if(!rt) {x=y=0;return ;}
pushdown(rt);//在分裂自己前,务必要解决自己身上的标记
if(sz[ls[rt]]<v){//sz[rt]<=v,size要慎用,因为没有还未pushup
x=rt;
split(rs[rt],v-sz[ls[rt]]-1/*v-sz[rt]*/,rs[rt],y);
}
else{
y=rt;
split(ls[rt],v,x,ls[rt]);
}
pushup(rt);
}
int merge(int x,int y){
if(x) pushdown(x);// 在合并自己前,务必要解决自己身上的标记
if(y) pushdown(y);// 在合并自己前,务必要解决自己身上的标记
if(!x||!y){return x|y;}
if(rk[x]<rk[y]){
rs[x]=merge(rs[x],y);
pushup(x);
return x;
}
else{
ls[y]=merge(x,ls[y]);
pushup(y);
return y;
}
}
void insert(int v){
int x,y;
split(root,v,x,y);
root=merge(merge(x,getnew(v)),y);
}
void flip(int l,int r){
int x,y,z;
split(root,r,y,z);
split(y,l-1,x,y);
swap(ls[y],rs[y]);
turn[y]^=1;
root=merge(merge(x,y),z);
}
void __count(int rt){
if(!rt) return ;
pushdown(rt);
__count(ls[rt]);
printf("%d ",val[rt]);
__count(rs[rt]);
}
void _count(){
__count(root);
}
}tree;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) tree.insert(i);
for(int i=1,l,r;i<=m;i++){
scanf("%d%d",&l,&r);
tree.flip(l,r);
}
tree._count();
}
其他区间操作:
除了区间翻转,fhq平衡树还可以处理区间赋值,区间最值;
有些题比较毒瘤,我不多说,这里用一道例题结束这篇blog
CF702F T-Shirts
题面:
有 (
) 种 T 恤,每种有价格
和品质
。
有 (
) 个客户要买 T 恤,第
个人有
元,每人每次都会买一件能买得起的
最大的 T 恤。一个人只能买一种 T 恤一件,所有人之间都是独立的。
问最后每个人买了多少件 T 恤?如果有多个 最大的 T 恤,会从价格低的开始买。
Examples
Input
3 7 5 3 5 4 3 2 13 14Output
2 3
Input
2 100 500 50 499 4 50 200 150 100Output
1 2 2 1
思路:
考虑将衣服 按质量排序,优先选择质量高的,其次为价格低的,对每一件衣服计算贡 献。 建立平衡树 维护客户剩余的钱 和 客户买的衣服数量 。
但是,人手中的钱数减去 后可能会导致重复,即不能保证平衡树二叉搜索树的性 质,也无法完成 FHQ 的合并操作(合并两个 FHQ Treap 需要保证一个子树中的最大值小 于另一个子树中的最小值),怎么办呢?
方法是,对于有重复的部分,一个一个地暴力插入;不重复的部分,打标记即可。
直觉地认为这肯定会超时,但是如果一个人手中的钱数(不妨设为 )需要被暴力插入,其 需要满足:
(即:
)
观察到每次暴力插入的话人手中的钱数是会减去 的,因为
大于
的一半,所以 每次暴力插入时都使
至少减少了一半,所以对于一个有
块钱的人来说其最多会被暴力 插入 O(
) 次,所以可以保证总的时间复杂度为 O((
)
),在四秒的时 限内可以通过此题。
code:
#include<bits/stdc++.h>
using namespace std;
const int N=4e5+1e2;
const int inf=0x3f3f3f3f;
int n,m;
struct st{
int c,q;
}co[N];
bool cmp(st x,st y){
if(x.q==y.q) return x.c<y.c;
return x.q>y.q;
}
struct Tree{
int ls[N],rs[N],val[N],rk[N],sum[N],ly1[N],ly2[N],root,tot,top,s[N];
void down1(int rt,int cc){
if(rt) val[rt]-=cc;ly1[rt]+=cc;
}
void down2(int rt,int cc){
if(rt) sum[rt]+=cc;ly2[rt]+=cc;
}
void pushdown(int rt){
if(ly2[rt]){
if(ls[rt]) down2(ls[rt],ly2[rt]);
if(rs[rt]) down2(rs[rt],ly2[rt]);
ly2[rt]=0;
}
if(ly1[rt]){
if(ls[rt]) down1(ls[rt],ly1[rt]);
if(rs[rt]) down1(rs[rt],ly1[rt]);
ly1[rt]=0;
}
}
void split(int rt,int v,int &x,int &y){
if(!rt) { x=y=0; return ;}
pushdown(rt);
if(val[rt]<=v){
x=rt;
split(rs[x],v,rs[x],y);
}else{
y=rt;
split(ls[y],v,x,ls[y]);
}
}
int merge(int x,int y){
if(x) pushdown(x);
if(y) pushdown(y);
if(!x||!y) return x|y;
if(rk[x]<rk[y]){
rs[x]=merge(rs[x],y);
return x;
}else{
ls[y]=merge(x,ls[y]);
return y;
}
}
void insert(int v,int i){
int x,y;
val[i]=v,rk[i]=rand();
split(root,v,x,y);
root=merge(merge(x,i),y);
}
int get(int k){
return sum[k];
}
void dfs(int rt){
if(!rt) return ;
pushdown(rt);
dfs(ls[rt]),dfs(rs[rt]);
s[++top]=rt,ls[rt]=rs[rt]=0;
}
void work(int c){
int x,y,z;
split(root,c-1,x,y);
down1(y,c),down2(y,1);
split(y,c-1,y,z);
top=0;dfs(y);
for(int i=1;i<=top;i++)
split(x,val[s[i]],x,y),x=merge(merge(x,s[i]),y);
root=merge(x,z);
}
}tree;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d%d",&co[i].c,&co[i].q);
sort(co+1,co+1+n,cmp);
scanf("%d",&m);
for(int i=1,v;i<=m;i++){
scanf("%d",&v);
tree.insert(v,i);
}
for(int i=1;i<=n;i++) {
tree.work(co[i].c);
}
tree.dfs(tree.root);
for(int i=1;i<=m;i++)
printf("%d " ,tree.get(i));
}
纵有风骨
本文来自博客园,作者:MegaSam,转载请注明原文链接:https://www.cnblogs.com/MegaSamTXL/p/17607129.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探