笛卡尔树
引入
本文介绍一种不太常用,但是与大家熟知的平衡树与堆密切相关的数据结构——笛卡尔树。
笛卡尔树是一种二叉树,每一个结点由一个键值二元组 \((k,w)\) 构成。要求 \(k\) 满足二叉搜索树的性质,而 \(w\) 满足堆的性质。一个有趣的事实是,如果笛卡尔树的 \(k,w\) 键值确定,且 \(k\) 互不相同,\(w\) 互不相同,那么这个笛卡尔树的结构是唯一的。上图:
(图源自维基百科)
上面这棵笛卡尔树相当于把数组元素值当作键值 \(w\),而把数组下标当作键值 \(k\)。显然可以发现,这棵树的键值 \(k\) 满足二叉搜索树的性质,而键值 \(w\) 满足小根堆的性质。
其实图中的笛卡尔树是一种特殊的情况,因为二元组的键值 \(k\) 恰好对应数组下标,这种特殊的笛卡尔树有一个性质,就是一棵子树内的下标是连续的一个区间(这样才能满足二叉搜索树的性质)。更一般的情况则是任意二元组构建的笛卡尔树。
构建
栈构建
过程
我们考虑将元素按照键值 \(k\) 排序。然后一个一个插入到当前的笛卡尔树中。那么每次我们插入的元素必然在这个树的右链(右链:即从根结点一直往右子树走,经过的结点形成的链)的末端。于是我们执行这样一个过程,从下往上比较右链结点与当前结点 \(u\) 的 \(w\),如果找到了一个右链上的结点 \(x\) 满足 \(x_w<u_w\),就把 \(u\) 接到 \(x\) 的右儿子上,而 \(x\) 原本的右子树就变成 \(u\) 的左子树。
具体不解释,我们直接上图。图中红色框框部分就是我们始终维护的右链:
显然每个数最多进出右链一次(或者说每个点在右链中存在的是一段连续的时间)。这个过程我们可以用栈维护,栈中维护当前笛卡尔树的右链上的结点。一个点不在右链上了就把它弹掉。这样每个点最多进出一次,复杂度 \(O(n)\)。
实现
伪代码如下:
Text Only | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 |
|
笛卡尔树与 Treap
谈到笛卡尔树,很容易让人想到一种家喻户晓的结构——Treap。没错,Treap 是笛卡尔树的一种,只不过 \(w\) 的值完全随机。Treap 也有线性的构建算法,如果提前将元素排好序,显然可以使用上述单调栈算法完成构建过程,只不过很少会这么用。
例题
HDU 1506 最大子矩形
题目大意:\(n\) 个位置,每个位置上的高度是 \(h_i\),求最大子矩阵。举一个例子,如下图:
阴影部分就是图中的最大子矩阵。
这道题你可 DP,可单调栈,但你万万没想到的是它也可以笛卡尔树!具体地,我们把下标作为键值 \(k\),\(h_i\) 作为键值 \(w\) 满足小根堆性质,构建一棵 \((i,h_i)\) 的笛卡尔树。
这样我们枚举每个结点 \(u\),把 \(u_w\)(即结点 \(u\) 的高度键值 \(h\))作为最大子矩阵的高度。由于我们建立的笛卡尔树满足小根堆性质,因此 \(u\) 的子树内的结点的高度都大于等于 \(u\)。而我们又知道 \(u\) 子树内的下标是一段连续的区间。于是我们只需要知道子树的大小,然后就可以算这个区间的最大子矩阵的面积了。用每一个点计算出来的值更新答案即可。显然这个可以一次 DFS 完成,因此复杂度仍是 \(O(n)\) 的。
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
参考资料
本页面的全部内容在 小熊老师 - 莆田青少年编程俱乐部 0594codes.cn 协议之条款下提供,附加条款亦可能应用