神秘数据结构:笛卡尔树

 


神秘数据结构:笛卡尔树

这是一种大家都听过名字但都没做过多少例题的奇怪数据结构。

但是今天研究一下NOI大纲,它好像要靠,算了,整吧

和其它维护信息的数据结构不太相同的是,它更多的是去支持数数,或者是决策,之类的。

笛卡尔树是啥

这是一种数据结构,可以把一堆形如 (xi,yi) 的点对放到二叉树上,使得:

  • 只看 x,树的中序遍历有序,即满足二叉搜索树(Binary Search Tree, BST)性质(左小右大)
  • 只看 y,这是一个二叉堆,大根/小根

它的性质非常优秀,可以支持一些跟最大值有关的问题,或者是插入删除之类的问题(记录插入删除时间)

咋建

以小根为例,大根就反一下。

我们先把 x 排一下序。

法1

考虑树的根。由堆的性质,这是原序列的最小值。由BST性质,我们可以递归解决两边。

然后就每次把最小值提起来变成根,左右分别处理接在下面就行了。这样就是 O(n2) 的,用线段树优化一下能做到 O(nlogn)

尽管这个方法没有下一个方法快,但它能帮助我们理解笛卡尔树的性质。

法2

线性!(如果保证 x 有序)

考虑增量法,每次枚举一个 i 看看怎么把它加进来

为了满足BST性质,我们的第一反应是不断的走右儿子(即,右链),然后把它接在这条链底部的右儿子。

实际上我们再想一下,我们其实可以在右链上找一个地方把它“插入”进来,同样满足BST性质。如下图:

image-20210728222022416.png

即,对于原树右链上 uv 的一条边,令 u 的右儿子为 i,然后 i 的左儿子再接到 v,这样就实现了把 i “插入” 到右链。

由于(小根)堆性质,任意一条向下的链都是递增的。我们要把当前的点 i 插入进来,就要找一个 “中间位置”,即,uv 的边,满足:au<aiai<av。此时用上述的 “插入” 方法把 i 插进来,容易发现,即满足了BST性质,又满足了BST性质。

照理来说找这个位置应该用二分法,然而,我们可以用一个单调栈来暴力维护,由于每个数只会进出一次,所以总复杂度是线性的。

还有一个细节注意,就是 i 一来直接变成当前最小值,反 客 为 主,此时不但要清掉链,还要把根设置成 i

代码(洛谷板子)

性质 / 事实

(以小根为例)

  1. u 为根的子树是一段连续的区间 (由BST性质),且 u 是这段区间的最小值,且不能再向两端延伸使得最小值不变(即,这一段区间是极长的)

  2. u 左右子树里任选两个点,两点间的区间最小值必定是 yu

    注:两个点都可以取 u 本身,此时的区间最小值仍是 yu

  3. a,b 间的区间最小值为:yLCA(a,b)

  4. y 是个随机排列,x1,2,3...n,则树高期望为 log,具体多少后面说

  5. Treap是一颗笛卡尔树,它依靠性质4确保复杂度

    ——因此我们可以动态维护笛卡尔树!

来点题!

上面提到,笛卡尔树可以有效的解决一些和最大/最小值有关的问题,或者是和插入删除有关的问题

经典题:柱状图最大子矩阵

直接形式化描述吧,给一个数组 h,求

max{min(hl...r)×(rl+1},1lrn

我们想到可以钦点最小值,然后看最小值等于它的最长区间。

区间限制在下标上,而最大值的限制在 h 上。容易想到,下标第一维,h 值第二维,来一颗笛卡尔树。

由性质1,我们可以枚举笛卡尔树上的节点,然后最长区间就是子树大小,更新一下最大值即可。

复杂度:O(n)

当然,这题也可以直接单调栈做,本质是差不多的

TJOI2011 树的序

首先有一个明显的贪心:我们先把BST搞出来,然后在BST上贪心跑:先走小儿子,后走大儿子。很显然这样的贪心是对的,因为先走大儿子,在这一位上就输了,肯定不优。

问题在于如何建BST。更明确的说,ai 的 BST。

那我们建出来的树肯定对于 a 满足 BST 性质了。然后再想到,我们每次插一个新数,都会插在下面。所以这颗树上,下面编号大于上面。

所以对于编号来说,这颗树满足堆性质。

好,一个BST性质,一个堆性质,笛卡尔树,没了。即,以 (ai,i) 建一颗笛卡尔树,就是 ai 的 BST 了。

由于 a 无序,需要先把它排一下序,复杂度就是 log 了。

当然,这个 log 的常数小的可以,因为 std::sort 非常快!

总结:若发现了一个BST性质,一个堆性质,啪的一下想到笛卡尔树

SPOJ PERIODNI

bzoj2616

我们注意到,对于一段棋盘,真正有限制条件的只有下面那一段。对于上面,因为没法跨过来,所以可以随便放,无影响。

我们的问题又和最小值有关了,所以考虑笛卡尔树,小根的。

建出来树之后直接在树上考虑问题。设当前点是 u。对于它左右两颗子树对应区间里的棋盘,我们划分成两块:最小值上面的那一块,称作 A;大家共有的最小值那一块,称作 B。如下图,绿色线上面就是 A,下面就是 B

image-20210728224805612.png

对于 B,问题相当于我们要选若干个位置,使得行列均不同。用组合数选出行与列都是啥,再配对,就行了。

而对于 A,问题就麻烦在如何把下面这一块去掉。经过一番思考,得到一个神秘方法:直接记 f(u,x) 表示,u 子树里,把 u 父亲的那一段长度切掉,然后放 x 个车车的方案数。

那对于当前的 u,我们直接把左右儿子的 f 做一个卷积(这里可以暴力卷,因为瓶颈不在这),就可以得到在 A 部分里选若干个的方案数了。

对于 B,我们需要选若干个行,列出来。假设当前有 H 行,W 列供选择。容易发现,H=auafaW=size(u),即子树大小。

假设我们要求 f(u,i),即一共选 i 个;在 B 部分选择 j 个。此时 A 部分选择 ij 个,尽管不占据 B 部分的行,但占据掉了列。于是 B 部分的方案数为:

(Hj)(W(ij)j)×j!

其中 j! 是选出来的行与列配对的方案数。

然后我们再把 A,B 部分的方案数卷起来,就可以得到 f(u,0...k) 了。

代码

agc028B Removing Blocks

首先我们可以把它看成是,均匀随机一个删除顺序,求代价的期望,乘一个 n!

我们发现,每次删一个位置,然后两边分开搞,和笛卡尔树建树过程非常的类似。把每个点的删除时间搞出来,删一个点就提它做根,然后左右两边接过来,我们发现,它是一个笛卡尔树。其中,下标满足BST性质,删除时间满足小根堆性质。

一个位置的贡献就是它在笛卡尔树上的点深度 (即,到根路径上多少个点)。我们相当于求它的期望深度,乘以它的权值,加起来,就是代价的期望(由期望线性性)

现在问题变成如何求随机排列构成笛卡尔树的期望深度。

E(depi)=jP(janc(i))anc(u) 表示 u 的祖先。

问题又变成,ji 祖先的概率。不妨令 j<ij>i 同理。

想象一下笛卡尔树的结构,如果 j 要是 i 的祖先,那 [j,i] 这一段里面,j 得是最小值——要不然切在中间就把 i,j 分在两颗子树里了。

现在问题又变成,随机一个排列,区间 [l,r]l 位置是最小值的概率。

首先我们先选 rl+1 个位置放在区间里,然后令 n(rl+1) 个位置随便排, l 位置放最小值,(rl+1)1 个位置再随便排,最后除以 n! 就行了。推一波式子发现一堆东西都抵消了,最后概率为 1rl+1。应该有其它妙妙理解,但是我只会瞎几把推。

j>i 同理。最后搞出来我们发现这是一个调和级数求和。设调和级数 H(n)=i=1n1i

E(depi)=H(i)+H(ni+1)1

然后再按上述做一波就好了。

同时,由于 H(n)logn 是同阶的,我们也证明了随机排列的笛卡尔树的深度,期望是 log 的。

代码

笛卡尔树上启发式合并/分裂

启发式合并大家熟悉,启发式分裂的意思就是,我枚举一个点 u,算跨过 u 的情况,然后两边分别处理。对于计算跨过 u 的情况,我看左右哪个子树小,我就按这个子树作为枚举的标准,另一颗子树里块速的做。和启发式合并类似,它的复杂度也是多一个 log

例题:hdu6701,洛谷4755

都是启发式分裂,统计一下跨过中间的答案,两边递归做就行。

hdu那个比较傻逼,而洛谷的那个还需要小小的去一下重。

代码:

hdu题

咕咕题

动态维护笛卡尔树: ZJOI2012 小蓝的好友

说是动态的笛卡尔树,其实就他妈的是个treap

当然,treap就他妈的是个笛卡尔树,所以似乎怎么说都没问题

设资源点为“黑点”,其余为“白点”。那我们要数至少一个黑点的子矩阵数

注意到这个题的"至少一个"四个字,仿佛是把"容斥"二字写在了脸上,怎么想都得容斥,变成 “没有”

那现在的问题就是数白的

我们当然可以预处理,就是这个 R,C 有点大,不太能忍一下

那还有啥好办法呢,注意到 R,C,n 都挺小的,那要么枚举 R,C,要么枚举 n

后者相当于要考虑加入一个黑的,会减少几个白的。但又要考虑到其它的黑色的影响,似乎还要来一个 2n 的容斥:算了吧

那考虑前者。一种常见的套路就是,我们枚举它的一条边界,算另外几条边界

这个套路在处理二维的最大子矩阵中也用到了,枚举一维,处理出“高度”,转化成上面说的柱状图最大子矩阵

形式化点说,枚举 i 表示限制子矩阵的下边界是第 i 行。同样考虑处理出“高度”,即,hi(j) 表示 (i,j) 这个位置往上多少格才会碰到黑的。如果自己就是黑的,那么 hi(j)=0

考虑枚举子矩阵的左右边界 l,r。此时的方案数就取决于高度,而这个高度不能超过 l,r 间每个位置的高度,于是只能是 [1,min(hi(l...r))] 之间的整数,方案数是 min(hi(l...r))

于是总方案数就是

1lrnmin(hi(l...r))

min 有关,一看就笛卡尔树。

建一个小根的笛卡尔树。根据上面的性质我们知道,枚举一个点 u,在 u 的左子树中,选一个作为 l;在 u 的右子树中,选一个作为 r,则 l,r 区间最小值为 hi(u)。同时,l,r 都可以取 u 本身。

所以把式子转化成:

u=1Chi(u)×(size(ls(u))+1)×(size(rs(u))+1)

ls,rs 分别表示左右子树,size 表示子树大小,+1 表示可以取 u 本身。

那对于一行的答案,我们就可以 O(C) 的算了。

那总不能每行暴力的建个笛卡尔树吧,又变成 O(RC) 了,肯定不行。

考虑两行之间的树,变化其实很小。首先每个位置都 +1,然后是,这一行为黑的位置直接标 0

也就是说,我们需要资瓷:

  • 整体 +1
  • 单点修改
  • 维护笛卡尔树

考虑如何维护笛卡尔树。我们要保持 BST 性质满足的情况下,调整堆性质。考虑到平衡树的旋转操作,可以保持 BST 性质不变。

于是我们用平衡树的旋转来调堆性质就行了。写出来一看,这好像就是个 treap。

那复杂度如何保证呢?注意到数据随机生成,就相当于 treap 里面的随机权值一样,能够保证期望复杂度是 log 的。

代码

posted @   Flandre-Zhu  阅读(1988)  评论(4编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示