神秘数据结构:笛卡尔树

神秘数据结构:笛卡尔树

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

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

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

笛卡尔树是啥

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

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

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

咋建

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

我们先把 \(x\) 排一下序。

法1

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

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

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

法2

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

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

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

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

image-20210728222022416.png

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

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

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

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

代码(洛谷板子)

性质 / 事实

(以小根为例)

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

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

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

  3. \(a,b\) 间的区间最小值为:\(y_{LCA(a,b)}\)

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

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

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

来点题!

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

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

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

\(max\{min(h_{l...r})\times (r-l+1\},1\le l\le r\le n\)

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

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

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

复杂度:\(O(n)\)

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

TJOI2011 树的序

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

问题在于如何建BST。更明确的说,\(a_i\) 的 BST。

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

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

好,一个BST性质,一个堆性质,笛卡尔树,没了。即,以 \((a_i,i)\) 建一颗笛卡尔树,就是 \(a_i\) 的 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=a_u-a_{fa}\)\(W=size(u)\),即子树大小。

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

\[\binom{H}{j}\binom{W-(i-j)}{j}\times j! \]

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

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

代码

agc028B Removing Blocks

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

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

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

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

\(E(dep_i)=\sum\limits_{j} P(j\in anc(i))\)\(anc(u)\) 表示 \(u\) 的祖先。

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

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

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

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

\(j>i\) 同理。最后搞出来我们发现这是一个调和级数求和。设调和级数 \(H(n)=\sum\limits_{i=1}^{n} \dfrac{1}{i}\)

\(E(dep_i)=H(i)+H(n-i+1)-1\)

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

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

代码

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

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

例题:hdu6701,洛谷4755

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

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

代码:

hdu题

咕咕题

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

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

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

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

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

那现在的问题就是数白的

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

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

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

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

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

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

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

于是总方案数就是

\[\sum\limits_{1\le l\le r\le n} min(h_i(l...r)) \]

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

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

所以把式子转化成:

\[\sum\limits_{u=1}^{C} h_i(u)\times (size(ls(u))+1)\times(size(rs(u))+1) \]

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

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

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

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

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

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

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

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

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

代码

posted @ 2021-07-28 23:15  Flandre-Zhu  阅读(834)  评论(2编辑  收藏  举报