神秘数据结构:笛卡尔树
神秘数据结构:笛卡尔树
这是一种大家都听过名字但都没做过多少例题的奇怪数据结构。
但是今天研究一下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性质。如下图:
即,对于原树右链上 \(u\) 到 \(v\) 的一条边,令 \(u\) 的右儿子为 \(i\),然后 \(i\) 的左儿子再接到 \(v\),这样就实现了把 \(i\) “插入” 到右链。
由于(小根)堆性质,任意一条向下的链都是递增的。我们要把当前的点 \(i\) 插入进来,就要找一个 “中间位置”,即,\(u\) 到 \(v\) 的边,满足:\(a_u<a_i\) 而 \(a_i<a_v\)。此时用上述的 “插入” 方法把 \(i\) 插进来,容易发现,即满足了BST性质,又满足了BST性质。
照理来说找这个位置应该用二分法,然而,我们可以用一个单调栈来暴力维护,由于每个数只会进出一次,所以总复杂度是线性的。
还有一个细节注意,就是 \(i\) 一来直接变成当前最小值,反 客 为 主,此时不但要清掉链,还要把根设置成 \(i\)。
性质 / 事实
(以小根为例)
-
以 \(u\) 为根的子树是一段连续的区间 (由BST性质),且 \(u\) 是这段区间的最小值,且不能再向两端延伸使得最小值不变(即,这一段区间是极长的)
-
在 \(u\) 左右子树里任选两个点,两点间的区间最小值必定是 \(y_u\)
注:两个点都可以取 \(u\) 本身,此时的区间最小值仍是 \(y_u\)
-
\(a,b\) 间的区间最小值为:\(y_{LCA(a,b)}\)
-
若 \(y\) 是个随机排列,\(x\) 是 \(1,2,3...n\),则树高期望为 \(\log\),具体多少后面说
-
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\)。
对于 \(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\) 部分的方案数为:
其中 \(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那个比较傻逼,而洛谷的那个还需要小小的去一下重。
代码:
动态维护笛卡尔树: 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))\) 种
于是总方案数就是
和 \(min\) 有关,一看就笛卡尔树。
建一个小根的笛卡尔树。根据上面的性质我们知道,枚举一个点 \(u\),在 \(u\) 的左子树中,选一个作为 \(l\);在 \(u\) 的右子树中,选一个作为 \(r\),则 \(l,r\) 区间最小值为 \(h_i(u)\)。同时,\(l,r\) 都可以取 \(u\) 本身。
所以把式子转化成:
\(ls,rs\) 分别表示左右子树,\(size\) 表示子树大小,\(+1\) 表示可以取 \(u\) 本身。
那对于一行的答案,我们就可以 \(O(C)\) 的算了。
那总不能每行暴力的建个笛卡尔树吧,又变成 \(O(RC)\) 了,肯定不行。
考虑两行之间的树,变化其实很小。首先每个位置都 \(+1\),然后是,这一行为黑的位置直接标 \(0\)
也就是说,我们需要资瓷:
- 整体 \(+1\)
- 单点修改
- 维护笛卡尔树
考虑如何维护笛卡尔树。我们要保持 BST 性质满足的情况下,调整堆性质。考虑到平衡树的旋转操作,可以保持 BST 性质不变。
于是我们用平衡树的旋转来调堆性质就行了。写出来一看,这好像就是个 treap。
那复杂度如何保证呢?注意到数据随机生成,就相当于 treap 里面的随机权值一样,能够保证期望复杂度是 \(\log\) 的。