平衡树 - FHQ 学习笔记

平衡树 - FHQ 学习笔记

主要参考万万没想到 的 FHQ-Treap学习笔记。

本片文章的姊妹篇:平衡树 - Splay 学习笔记。

感觉完全不会平衡树,又重新学习了一遍 FHQ,一口气把常见套路都学完了。

一、大致内容及分类

FHQ(???),全称非旋转 Treap,是一种可以用于维护按权值、排名分裂的数据结构。它相比与 Splay 虽然常数较大,但是实现起来代码难度相对容易,而且由于它非旋的特点,也可以用来实现可持久化。

既然叫做非旋 Treap,它兼有 Treap 的特点又有非旋转独特的优势。

  • 从 Treap 角度看,他们同样都是依赖修正值 rnd 是随机的,用将他们按照 rnd 形成一个小根堆。与 Treap 相同,它也满足笛卡尔树的性质,它的中序遍历和它的插入顺序相同,即 \(1\)\(n\) 的序列。
  • 从非旋角度看,FHQ 直接通过 splitmerge 操作实现添加、删除元素,不用再树上旋转了。

根据不同题目要求,将平衡树分为序列平衡树权值平衡树

  • 序列平衡树的中序遍历为每个元素在序列中的下标,权值为每个元素具体的值,常见题型为区间翻转等。
  • 权值平衡树的中序遍历是所有元素从小到大排序,即按照中序遍历提取所有元素后元素权值递增,常见操作为第 \(k\) 大等。

如果毒瘤出题人同时综合了以上两种操作,即区间翻转 \(+\) 区间第 \(k\) 大,应该怎么做呢?好吧,如果真是这样,这篇文章可能不能够帮到你,直接上一个根号做法!

二、基本操作

FHQ 的核心操作就是 split 出操作区间,操作完后 merge 回去。

下边讲解中默认的平衡树类型为权值平衡树,序列平衡树其实是将某些 \(val\) 改为了 \(siz\)

分裂 split

无论是按照排名还是权值分裂,他们都是将原树分为左右两半,可以利用中序遍历的性质进行分裂。

具体操作时,我们新建两个临时变量 \(x,y\) 分别表示分裂出来的左边、右边的那颗平衡树。

如果我们遇到一个应该属于 \(x\) 树的节点,就将这个点以及这个点的左子树加入 \(x\) 树中,并递归分裂右子树;如果遇到属于 \(y\) 的,就将这个点与它的右子树加入 \(y\) 树中,并递归分裂左子树。

需要注意到是,如果使用序列平衡树,下面和 \(k\) 的大小判断应该变为 \(ls.siz+1\le k\)

\(\bigstar\texttt{Attention}\):如果需要 split 一个区间,记得先 split 右端点再 split 左端点!

可以写出伪代码如下:

void split(int p,int k,int &x,int &y) // 分裂出 (-infty,k],(k,+infty) 
{if(!p) { x=y=0; return; }pushdown(p);if(tree[p].val<=k) x=p,split(rs,k,rs,y);else y=p,split(ls,k,x,ls);pushup(p);
}

合并 merge

由于这是 FHQ 的 merge,需要在合并时既保证小根堆性质又不破坏中序遍历的特点,对合并的两棵树有特殊的要求:左右区间不能够相交或者顺序颠倒!

所以我们在合并时必须按照顺序从左到右合并。

具体操作时,可以直接将 rnd 小的作为新树的根节点,如果这个根节点来自左子树就递归右子树,相反来自右子树就递归左子树(由于满足上面区间不相交也不颠倒的特点)。

写出伪代码:

int merge(int x,int y)
{if(!x || !y) return x+y;if(tree[x].rnd<tree[y].rnd){ pushdown(x),tree[x].pr=merge(tree[x].pr,y),pushup(x); return x; }else{ pushdown(y),tree[y].pl=merge(x,tree[y].pl),pushup(y); return y; }
}

新建节点 new

没什么可说的,就是给新节点附一个随机的 rnd

inline int New(ll Val)
{int p=++All;tree[p].rnd=rand(),tree[p].val=Val;tree[p].siz=tree[p].cnt=1;tree[p].pl=tree[p].pr=0;return p;
}

插入 insert

直接分裂出两端区间,把新建的加点放到两棵树中间在合并即可。

inline void Insert(ll Val)
{split(root,Val,x,y);root=merge(merge(x,New(Val)),y);
}

删除 delete

FHQ 可以实现删除一个数或删除这个值的所有数,唯一区别就在于分裂时的不同。

inline void Delete_one(ll Val)
{split(root,Val,x,z),split(x,Val-1,x,y);y=merge(tree[y].pl,tree[y].pr);root=merge(merge(x,y),z);
}
inline void Delete_All(ll Val)
{split(root,Val,x,z),split(x,Val-1,x,y);root=merge(x,z);
}

查询排名对应权值 Rank_to_Value

根据每颗子树的 \(siz\) 暴力跳即可。

inline int kth(int p,int Rank) // 这里返回的是找到节点的下标 
{while(p){if(tree[ls].siz>=Rank) p=ls;else if(tree[ls].siz+tree[p].cnt>=Rank) return p;else Rank-=tree[ls].siz+tree[p].cnt,p=rs;}return p;
}

查询权值对应排名 Value_to_Rank

按照权值分裂出来后左区间树的 \(siz\) 就是排名。

inline int Value_to_Rank(ll Value)
{split(root,Value-1,x,y);int ret=tree[x].siz+1;root=merge(x,y);return ret;
}

查询前驱 Findpre

分裂出来后在左子树中排名最靠后的是前驱。

inline ll Findpre(ll Value)
{split(root,Value-1,x,y);ll ret=tree[kth(x,tree[x].siz)].val;root=merge(x,y);return ret;
}

查询后继 Findnex

分裂出来后在右子树中排名最靠前的是后继。

inline ll Findnex(ll Value)
{split(root,Value,x,y);ll ret=tree[kth(y,1)].val;root=merge(x,y);return ret;
}

三、可持久化平衡树

平衡树上的可持久化和线段树的可持久化其实差别不大,每次修改的时候需要建立新的节点,对于每个版本也需要保存根节点。

新建一个点的原则:如果我们把版本最新的点叫做新点,那么我们只能够在可持久化平衡树中对新点修改,不然就会出错,所以在 splitmerge 中,我们一旦对一个值进行了修改,就需要新建一个节点。

伪代码如下:

inline void pushdown(int p)
{if(!tree[p].tag) return;if(ls) ls=clone(ls),tree[ls].tag^=1,swap(tree[ls].pl,tree[ls].pr);if(rs) rs=clone(rs),tree[rs].tag^=1,swap(tree[rs].pl,tree[rs].pr);tree[p].tag=false;
}
inline int clone(int p) { tree[++All]=tree[p]; return All; }
void split(int p,ll k,int &x,int &y)
{if(!p) { x=y=0; return; }pushdown(p);if(tree[p].val<=k) x=clone(p),split(rs,k,tree[x].pr,y),pushup(x);else y=clone(p),split(ls,k,x,tree[y].pl),pushup(y);
}
int merge(int x,int y)
{if(!x || !y) return x+y;if(tree[x].rnd<tree[y].rnd){pushdown(x),x=clone(x),tree[x].pr=merge(tree[x].pr,y),pushup(x);return x;}else{pushdown(y),y=clone(y),tree[y].pl=merge(x,tree[y].pl),pushup(y);return y;}
}

四、常见优化技巧

垃圾回收

在每次新建节点的时候先从垃圾桶中捡,如果垃圾捡光了再新开节点。

笛卡尔树方式建树

由于我们的 FHQ 是一种笛卡尔树,所以如果给定了一堆点,完全可以直接用笛卡尔树的方式 \(\mathcal{O(n)}\) 建树。

inline int build()
{int tp=0,p=0,Last;for(int i=1;i<=n;i++){p=New(v[i]),Last=0;while(tp && tree[sta[tp]].rnd>tree[p].rnd) pushup(Last=sta[tp--]);if(tp) tree[sta[tp]].pr=p;tree[p].pl=Last,sta[++tp]=p;}while(tp) pushup(sta[tp--]);return sta[1];
}

定期重构

如果题目中对空间有限制而且不要求查询历史版本,可以定期重构。当使用的空间超过一定值的时候,我们中序遍历整棵树并放入数组中,在线性建树。

启发式合并

如果需要合并两个有交集的 Treap 时该怎么做?我们可以每次将较小的数合并到较大的树中去,这样每个点最多只会合并 \(\log n\) 次,每次合并复杂度 \(n\log n\),总时间复杂度 \(\mathcal{O(n\log ^2 n)}\)

区间缩点

详见万万没想到的博客,咕咕咕。

五、模板

struct FHQ_number
{#define Maxn 点数#define ls tree[p].pl#define rs tree[p].prprivate:int All=0,root=0;struct NODE { int pl,pr,siz,cnt,rnd; ll val; };NODE tree[Maxn];inline int Dot() { return ++All; }inline int New(ll Val){int p=Dot();tree[p].rnd=rand(),tree[p].val=Val;tree[p].siz=tree[p].cnt=1;tree[p].pl=tree[p].pr=0;return p;}inline void pushdown(int p) { p--; }inline void pushup(int p){ tree[p].siz=tree[ls].siz+tree[rs].siz+tree[p].cnt; }void split(int p,int k,int &x,int &y){if(!p) { x=y=0; return; }pushdown(p);if(tree[p].val<=k) x=p,split(rs,k,rs,y);else y=p,split(ls,k,x,ls);pushup(p);}int merge(int x,int y){if(!x || !y) return x+y;if(tree[x].rnd<tree[y].rnd){pushdown(x),tree[x].pr=merge(tree[x].pr,y),pushup(x);return x;}else{pushdown(y),tree[y].pl=merge(x,tree[y].pl),pushup(y);return y;}}inline int kth(int p,int Rank){while(p){if(tree[ls].siz>=Rank) p=ls;else if(tree[ls].siz+tree[p].cnt>=Rank) return p;else Rank-=tree[ls].siz+tree[p].cnt,p=rs;}return p;}int x,y,z;public:inline void Insert(ll Val){ split(root,Val,x,y),root=merge(merge(x,New(Val)),y); }inline void Delete_one(int Val){split(root,Val,x,z),split(x,Val-1,x,y);y=merge(tree[y].pl,tree[y].pr);root=merge(merge(x,y),z);}inline ll Rank_to_Value(int Rank){ return tree[kth(root,Rank)].val; }inline int Value_to_Rank(ll Value){split(root,Value-1,x,y);int ret=tree[x].siz+1;root=merge(x,y);return ret;}inline ll Findpre(ll Value){split(root,Value-1,x,y);ll ret=tree[kth(x,tree[x].siz)].val;root=merge(x,y);return ret;}inline ll Findnex(ll Value){split(root,Value,x,y);ll ret=tree[kth(y,1)].val;root=merge(x,y);return ret;}
}T;
struct FHQ_sequence
{#define Maxn 点数#define ls tree[p].pl#define rs tree[p].print All=0,root=0;struct NODE { int pl,pr,siz,cnt,rnd,val; bool tag; };NODE tree[Maxn];inline int Dot() { return ++All; }inline int New(int Val){int p=Dot();tree[p].rnd=rand(),tree[p].val=Val;tree[p].cnt=tree[p].siz=1;tree[p].pl=tree[p].pr=0;return p;}inline void pushdown(int p){if(!tree[p].tag) return;swap(tree[ls].pl,tree[ls].pr);swap(tree[rs].pl,tree[rs].pr);tree[ls].tag^=1,tree[rs].tag^=1;tree[p].tag=false;}inline void pushup(int p){ tree[p].siz=tree[ls].siz+tree[p].cnt+tree[rs].siz; }void split(int p,int k,int &x,int &y){if(!p) { x=y=0; return; }pushdown(p);if(tree[ls].siz<k) x=p,split(rs,k-tree[ls].siz-1,rs,y);else y=p,split(ls,k,x,ls);pushup(p);}int merge(int x,int y){if(!x || !y) return x+y;if(tree[x].rnd<tree[y].rnd){pushdown(x),tree[x].pr=merge(tree[x].pr,y),pushup(x);return x;}else{pushdown(y),tree[y].pl=merge(x,tree[y].pl),pushup(y);return y;}}int kth(int p,int Rank){while(p){if(tree[ls].siz>=Rank) p=ls;else if(tree[ls].siz+tree[p].cnt>=Rank) return p;else Rank-=tree[ls].siz+tree[p].cnt,p=ls;}return p;}void Insert(int Val) // 插到末尾 { root=merge(root,New(Val)); }int x,y,z;inline void Reverse(int l,int r){split(root,r,x,z),split(x,l-1,x,y);swap(tree[y].pl,tree[y].pr),tree[y].tag^=1;root=merge(merge(x,y),z);}void print(int p){pushdown(p);if(ls) print(ls);printf("%d ",tree[p].val);if(rs) print(rs);}
}T;

六、例题

【模板】普通平衡树

【模板】普通平衡树(数据加强版)

【模板】文艺平衡树

【模板】可持久化平衡树

【模板】可持久化文艺平衡树

平衡树题单

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/318000.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【周末狂欢赛6】[AT1219]历史研究(回滚莫队),大魔法师(矩阵+线段树),单峰排列

文章目录T1&#xff1a;单峰排列题目题解codeT2&#xff1a;历史研究题目题解codeT3&#xff1a;大魔法师题目题解code我可能这辈子都更不出来狂欢赛5了&#xff0c;先咕咕 T1&#xff1a;单峰排列 题目 一个n的全排列A[i]是单峰的&#xff0c;当且仅当存在某个x使得A[1]<…

YBTOJ:圈套问题(分治法、鸽笼原理)

文章目录题目描述数据范围解析代码图片转载自&#xff1a; https://blog.csdn.net/weixin_43346722/article/details/118435430题目描述 平面上有 n个点&#xff0c;用n个大小相同的圆分别将一个点作为圆心&#xff0c;同时满足圆圈不相交&#xff0c;求圆的最大半径。 数据范…

CF1598E-Staircases【计数】

正题 题目链接:https://www.luogu.com.cn/problem/CF1598E 题目大意 给出一个nmn\times mnm的网格图&#xff0c;开始所有都是黑色的&#xff0c;qqq次取反一个格子的颜色&#xff0c;然后求楼梯的数量。 楼梯定义为全黑色的下/右交替的格子集。 1≤n,m≤1000,1≤q≤1041\le…

ASP.NET Core 实战:使用 NLog 将日志信息记录到 MongoDB

一、前言在项目开发中&#xff0c;日志系统是系统的一个重要组成模块&#xff0c;通过在程序中记录运行日志、错误日志&#xff0c;可以让我们对于系统的运行情况做到很好的掌控。同时&#xff0c;收集日志不仅仅可以用于诊断排查错误&#xff0c;由于日志同样也是大量的数据&a…

532. 货币系统

532. 货币系统 题意&#xff1a; 有 n 种不同面额的货币&#xff0c;第 i 种货币的面额为 a[i]&#xff0c;每一种货币都有无穷多张&#xff0c;货币之间可以彼此代替&#xff0c;比如6等于两张3&#xff0c;问有多少种货币是不可替代的 题解&#xff1a; 我们换一个问…

概率期望题(期望 DP)做题记录

概率期望题(期望 DP)做题记录 P3830 [SHOI2012]随机树 难点在于第二问&#xff1a;生成树的期望深度。 不 wei zhuo 捏&#xff0c;设 \(dp_{i,j}\) 表示已经有了 \(i\) 个叶子结点&#xff0c;深度大于 \(j\) 的概率。 考虑枚举一棵子树的大小&#xff0c;转移方程如下&#x…

[学习笔记] 伸展树splay详解+全套模板+例题[Luogu P3369 【模板】普通平衡树]

文章目录引入概念全套模板变量声明updaterotate旋转splay操作insert插入delete删除查找x的位置查找第k大前驱/后继极小值-inf和极大值inf的作用例题&#xff1a;P3369 【模板】普通平衡树题目code声明一下&#xff0c;许多代码的注解都在模板代码里面写了的&#xff0c;所以正文…

2021.9.23模拟

前言 174pts 40502460 四个暴力分 qwq T1想切结果矩乘T飞了。。。 不要迷信矩乘&#xff0c;这玩意也是会T的… 考场 先看题 感觉T1和T最可做 T3期望想到zld的全排列大法了&#xff0c;但是似乎只能线性… T4是初始化加强版&#xff0c;思路倒是有&#xff0c;但是不想写&am…

AT1981-[AGC001C]Shorten Diameter

正题 题目链接:https://www.luogu.com.cn/problem/AT1981 题目大意 给出nnn个点的一棵树&#xff0c;每次你可以删除一个叶子&#xff0c;求最少的操作数使得树的直径长度不超过kkk。 1≤n,k≤20001\leq n,k\leq 20001≤n,k≤2000 解题思路 开始以为是dpdpdp啥的&#xff0c…

手写AspNetCore 认证授权代码

在普通的MVC项目中 我们普遍的使用Cookie来作为认证授权方式&#xff0c;使用简单。登录成功后将用户信息写入Cookie&#xff1b;但当我们做WebApi的时候显然Cookie这种方式就有点不适用了。在dotnet core 中 WebApi中目前比较流行的认证授权方式是Jwt (Json Web Token) 技术。…

FWT 学习笔记

FWT 学习笔记 学的时候比较匆忙&#xff0c;于是就学一个 \(\texttt{or,and,xor}\) 卷积跑路。 P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT) 前置知识&#xff1a;高维前缀和&#xff0c;下面前缀和的操作大多都是用高维前缀和来实现的。 设有两个长度为 \(2^n\) 的序列 …

大盗阿福

大盗阿福 题意&#xff1a; 长度为n的数组a&#xff0c;不能取连续的数&#xff0c;问所能取的最大值是多少 题解&#xff1a; 设dp[i][0]表示第i个数不选&#xff0c;dp[i][1]表示第i个数选 如果第i个数不选&#xff0c;那么第i-1个数可以选也可以不选&#xff0c;我们取最…

YBTOJ:采矿战略(线段树维护dp、树链剖分)

文章目录题目描述解析代码题目描述 所谓线段树维护dp&#xff0c;就是在线段树上维护dp &#xff08;逃&#xff09; 解析 把树剖一下后就变成了区间问题 考虑建一棵线段树&#xff0c;每一个结点都是一个背包 这样就能区间查询&#xff0c;也能带修了 这种做法复杂度其实并不…

【用皇宫三十六计生存法则带你走进LCT(动态树)】LCT概念+模板+例题【洛谷P3690 Link Cut Tree(动态树)】

文章目录LCT概念模板rotatoisrootsplayaccessmakerootsplitfindrootlinkcut封装版例题题目code普通版code封装版这篇博客主要是帮助大家理解各个模板及LCTLCTLCT的意思&#xff0c;方便理解&#xff0c;模板写法的理解在代码里有注释详解&#xff0c;如果要看原理的话&#xff…

AGC002(D~F)【Kruskal重构树,博弈论,dp】

正题 AT1998 [AGC002D] Stamp Rally【Kruskal重构树,倍增】 https://www.luogu.com.cn/problem/AT1998 题目大意 给出nnn个点mmm条边的一张无向图&#xff0c;qqq次询问两个人分别从x,yx,yx,y&#xff0c;要求总共经过zzz个点的情况下经过边的最大编号的最小值。 1≤n,m,q≤…

迈向现代化的 .Net 配置指北

1. 欢呼 .NET Standard 时代我现在已不大提 .Net Core&#xff0c;对于我来说&#xff0c;未来的开发将是基于 .NET Standard&#xff0c;不仅仅是 面向未来 &#xff0c;也是 面向过去&#xff1b;不只是 .Net Core 可以享受便利&#xff0c; .NET Framework 不升级一样能享受…

DP 套 DP

DP 套 DP 学习笔记 大致内容 DP 套 DP 就是将一个简单 DP 的状态压缩起来放到新的 DP 中当做状态进行 DP 的过程。 常用于计算简单 DP 的答案为 \(k\) 的转移方案的数量。 一般都需要 decode 和 recode 操作&#xff0c;这里和 插头DP/轮廓线DP 有异曲同工之妙&#xff01; 例题…

acwing提高组 第一章 动态规划

文章目录数字三角形模型最长上升子序列模型背包模型状态机模型状态压缩DP区间DP树形DP数位DP单调队列优化DP斜率优化DPoj链接数字三角形模型 AcWing 1015. 摘花生1357人打卡 AcWing 1018. 最低通行费1279人打卡 AcWing 1027. 方格取数1158人打卡 AcWing 275. 传纸条933人打卡 …

YBTOJ洛谷P2042:维护数列(平衡树)

文章目录题目描述解析删除区间插入数列修改&翻转区间和&最大子段和代码传送门题目描述 解析 阴间题… 这不是裸的板子吗&#xff1f; 国赛真的有人能把这题写出来吗… 应该算一道练习作用很强的题了 写完这题&#xff0c;各种平衡树维护区间操作的方法可以说是毕业了吧…

CAP 2.4版本发布,支持版本隔离特性

前言自从上次 CAP 2.3 版本发布 以来&#xff0c;已经过去了几个月的时间&#xff0c;这几个月比较忙&#xff0c;所以也没有怎么写博客&#xff0c;趁着2019年到来之际&#xff08;现在应该是2019年开始的时候&#xff09;&#xff0c;CAP也发布了2018年的最后一个大版本 2.4&…