( 转载请注明原帖地址http://www.cnblogs.com/yyf0309/p/LeftistTree.html ,转载不注明地址必究 )
左偏树是可并堆的一种实现。对比一下普通的堆和左偏树
| 插入 | 取出顶部元素 | 弹出 | 合并 |
普通的堆 | $O(\log n)$ | $O(1)$ | $O(\log n)$ | $O((n_1 + n_2)\log (n_1 + n_2))$ |
左偏树 | $O(\log n)$ | $O(1)$ | $O(\log n)$ | $O(\log n_1 + \log n_2)$ |
那么左偏树又是如何高效地实现合并两棵左偏树的呢?下面将展开探讨。(由于我比较懒,没怎么画图,所以讲理论的时候请读者多画图,多理解一下,如果不希望学习理论知识可以直接看操作)
左偏树的定义
定义1 左偏树中的一个节点,如果它的左子树或右子树为空,则称它是一个外结点。
定义2 对于左偏树中的一个节点x,到它的子节点中,离它最近的一个外结点经过的边数称为它的距离,记为$dist(x)$。特别地,外结点的距离为0,空节点(null)的距离为-1。
现在约定一下,本文中出现的左偏树的距离是指,左偏树根节点的距离。
性质1(堆性质) 对于左偏树中的一个非叶节点应满足堆的性质。如果是大根堆,应满足任意非叶节点的左子树和右子树(如果有的话)的根节点的权值大于等于这个节点,即$val(x) \geqslant val(left(x)), val(x) \geqslant val(right(x))$。如果是小根堆则满足应满足任意非叶节点的左子树和右子树(如果有的话)的根节点的权值小于等于这个节点
下面这个性质是左偏树一个很重要的性质,也是为什么它被叫做左偏树,以及为什么能够在log级的时间内合并两棵左偏树。
性质2(左偏性质) 对于左偏树中的任意节点满足它的左子树的距离大于等于右子树的距离。即$dist(left(x)) \geqslant dist(right(x))$。
同时也不难得出下面这条性质。这意味着左偏树是递归定义的。
性质3 左偏树中的任意节点的左子树和右子树(如果有的话)都是左偏树。
由这几条性质可以得出左偏树的定义:左偏树是具有左偏性质的堆有序二叉树。
为了弄清楚为什么左偏树的合并是log级别,下面将展开左偏树的距离和节点数之间的关系的探讨。
左偏树的更多性质
引理1 左偏树中的节点的距离总是满足$dist(x) = dist(right(x)) + 1$。
证明 根据定义2有$dist(x) = \min (dist(left(x)), dist(right(x))) + 1$。根据左偏性质可以证得。
现在就来探讨一下当左偏树的距离为$k$时,左偏树的节点数至少为多少。我们希望节点数尽量少,那应该满足$dist(left(x)) = dist(right(x))$,根据引理1可以得到它们都等于$dist(x) – 1$。此时的左偏树就是一棵深度为$(k + 1)$,所以此时的左偏树的节点个数为$2^{k + 1} - 1$。整理得到下面的结论。
定理2 所有距离为k的左偏树中,节点最少的是满二叉树,且节点数为$2^{k + 1} - 1$。
当我知道一棵左偏树有n个节点,我能否计算出它的最大深度?答案是肯定的。
推论3 一棵节点数为$n$的左偏树,它的距离至多为$\left \lfloor \log_2 (n + 1) \right \rfloor - 1$。
证明 根据定理2有$2^{k + 1} - 1\leqslant n$。因此$k\leqslant \log_2(n + 1) - 1$。因为k为整数,所以$k\leqslant \left \lfloor \log_2 (n + 1) \right \rfloor - 1$。因此定理得证。
现在再做一个临时的约定,称从左偏树根节点一直访问右子树,直到不能访问位置所有经过的边和点形成的链为最右链。(不然后文会耗很多费笔墨)
引理4 左偏树的最右链恰好有1个外结点。
证明 显然存在一个外结点,不然这一定不是一棵树。假设存在两个,对于深度浅一点的外结点它存在右子树(不然没有后文了),但是不存在左子树(不然它不是外结点),与左偏性质矛盾,所以定理得证。
得到这个有什么用?现在可以得到最右链的节点数的上界,至于这个有什么用?等会儿就知道了。
定理5 一棵有n个节点的左偏树的最右链,至多有$\left \lfloor \log_2 (n + 1) \right \rfloor$个节点。
证明 根据推论3有最右链的至多有$\left \lfloor \log_2 (n + 1) \right \rfloor$个非外节点,又根据引理4,可以得知最右链至多有$\left \lfloor \log_2 (n + 1) \right \rfloor$个节点。因此定理得证。
小练习
判断下列命题的正误,如果正确说明理由,否则请举出反例。(答案在页面尾部)
- 左偏树的任意节点的左子树的大小比右子树大。(一棵树的大小是指它拥有的节点数)
- 左偏树的外节点的所有子节点都是外节点。
- 一条链也可能是左偏树。
左偏树的常用操作
有了上面五条结论,可以开始讨论左偏树的操作了。
1.合并操作(merge)
为什么先讨论合并操作?因为这是左偏树最基本的操作,插入删除都依赖于它。
至于怎么合并,根据上面几条关于最右链的结论和最右链脱不了关系。
现在假设都是满足大根堆性质。直观的感受是这样的,将左偏树按照最右链的边进行划分,例如下图
然后将根节点更小的那棵左偏树每一块从根节点大的依次插入另一颗左偏树相邻的两块中,使得满足堆的性质(即插入的地方满足它的下一块的根小于它)。(语言很难懂?毕竟我语文渣得要死,看图吧↓)
继续这样的操作,直到被插入树为空
虽然显然插入算法使得新堆满足堆的性质,但是会破坏左偏性质(例如权位4的节点),所以在回溯的时候,要进行重新计算dist和维护左偏性质(如果变成”右偏”,就swap左右子树)。
显然新树的最右链上的dist值会发生改变,其他什么都不会发生改变(思考一下定义,一个节点的上方怎么乱,跟它的dist 半毛钱关系),所以维护dist只需要考虑最右链上的点。如果要使用引理1,则注意一下要先维护左偏性质。
下面是一张维护完成后的左偏树
至于代码实现,如果用这种笨方法的话,可能代码会比较长吧。所以说有一个简短、不易写错的写法
显然这个算法是正确的,所以直接开始讨论时间复杂度。
简单地可以得到时间复杂度为O(两棵树的最右链覆盖的节点数之和),再根据定理5,就可以简单地得到,它的时间复杂度约为 。
2.插入操作(push)
有了log级的合并操作,那插入操作就很简单了,将插入的元素看成一棵只有一个节点的左偏树,然后merge一下,完事。
3.删除操作(pop)
这个也比较简单,将根节点的左右子树合并,然后再将根节点删掉。同样很简单。
一个应用的例子
[APIO2012] Dispatching
题目大意 有一棵有n个节点的有根树,每个点有两个权值,一个领导值,一个费用。选择一个点,然后在它所在的子树内(包括它自己),选出一些点,使得它们的费用和不超过m,但选出点的个数乘先选择的点的领导值的值尽量大。($n\leqslant 10^5, m\leqslant 10^9$)
为了使选出点的尽量多(在初始点一定时),所以选出点的费用因尽量小。这就可以想到堆。因为初始点需要去枚举,这个可以用dfs,所以会发生合并的过程,这个时候左偏树就登场了。显然在一个节点合并完成左偏树内的节点费用和很可能会超过$m$,这时想一下如何处理。左偏树就弄大根堆,根据之前的贪心策略,凡是费用超过$m$就弹出,直到合法,然后更新答案。
1 /** 2 * bzoj 3 * Problem#2809 4 * Accepted 5 * Time:1244ms 6 * Memory:15024k 7 */ 8 #include <iostream> 9 #include <cstdio> 10 #include <ctime> 11 #include <cmath> 12 #include <cctype> 13 #include <cstring> 14 #include <cstdlib> 15 #include <fstream> 16 #include <sstream> 17 #include <algorithm> 18 #include <map> 19 #include <set> 20 #include <stack> 21 #include <queue> 22 #include <vector> 23 #include <stack> 24 #ifndef WIN32 25 #define Auto "%lld" 26 #else 27 #define Auto "%I64d" 28 #endif 29 using namespace std; 30 typedef bool boolean; 31 const signed int inf = (signed)((1u << 31) - 1); 32 const double eps = 1e-6; 33 const int binary_limit = 128; 34 #define smin(a, b) a = min(a, b) 35 #define smax(a, b) a = max(a, b) 36 #define max3(a, b, c) max(a, max(b, c)) 37 #define min3(a, b, c) min(a, min(b, c)) 38 template<typename T> 39 inline boolean readInteger(T& u){ 40 char x; 41 int aFlag = 1; 42 while(!isdigit((x = getchar())) && x != '-' && x != -1); 43 if(x == -1) { 44 ungetc(x, stdin); 45 return false; 46 } 47 if(x == '-'){ 48 x = getchar(); 49 aFlag = -1; 50 } 51 for(u = x - '0'; isdigit((x = getchar())); u = (u << 1) + (u << 3) + x - '0'); 52 ungetc(x, stdin); 53 u *= aFlag; 54 return true; 55 } 56 57 #define LL long long 58 59 typedef class LeftistTreeNode { 60 public: 61 int val; 62 int dis; 63 LeftistTreeNode *l, *r; 64 65 LeftistTreeNode():val(0), dis(0), l(NULL), r(NULL) { } 66 LeftistTreeNode(int val, int dis, LeftistTreeNode* l, LeftistTreeNode* r):val(val), dis(dis), l(l), r(r) { } 67 68 inline boolean bad() { // Check the node if it satisfies left skewed property. 69 if(!r) return false; 70 return !l || (l->dis < r->dis); 71 } 72 73 inline void update() { 74 if(!l || !r) dis = 0; 75 else dis = r->dis + 1; 76 } 77 }LeftistTreeNode; 78 79 LeftistTreeNode pool[500000]; 80 LeftistTreeNode* top = &pool[0]; 81 LeftistTreeNode* newnode() { 82 top->l = top->r = NULL; 83 top->val = -inf, top->dis = 0; 84 return top++; 85 } 86 87 LeftistTreeNode* newnode(int val) { 88 top->l = top->r = NULL; 89 top->val = val, top->dis = 0; 90 return top++; 91 } 92 93 typedef class LeftistTree { 94 public: 95 LeftistTreeNode* root; 96 LL sum; 97 int size; 98 99 LeftistTree():root(NULL), sum(0), size(0) { 100 101 } 102 103 LeftistTreeNode* merge(LeftistTreeNode* a, LeftistTreeNode* b) { 104 if(a == NULL) return b; // When node a or b equals to null, merging should be stopped. 105 if(b == NULL) return a; 106 if(a->val < b->val) // Keep a->val bigger than b->val. 107 swap(a, b); 108 a->r = merge(a->r, b); // Merge. 109 if(a->bad()) // Maintain node a. 110 swap(a->l, a->r); 111 a->update(); 112 return a; 113 } 114 115 inline void merge(LeftistTreeNode* b) { 116 if(!root || root->val < b->val) swap(root, b); 117 merge(root, b); 118 } 119 120 inline void merge(LeftistTree& b) { 121 sum += b.sum, size += b.size; 122 merge(b.root); 123 } 124 125 inline void push(int x) { 126 merge(newnode(x)); 127 sum += x, size++; 128 } 129 130 inline int top() { 131 if(!root) return 0; 132 return root->val; 133 } 134 135 inline void pop() { 136 sum -= top(), size--; 137 if(root->r && root->l->val < root->r->val) swap(root->l, root->r); 138 merge(root->l, root->r); 139 root = root->l; 140 } 141 }LeftistTree; 142 143 int n, m; 144 int *costs, *leads; 145 vector<int> *g; 146 147 inline void init() { 148 readInteger(n); 149 readInteger(m); 150 g = new vector<int>[(n + 1)]; 151 costs = new int[(n + 1)]; 152 leads = new int[(n + 1)]; 153 for(int i = 1, fa; i <= n; i++) { 154 readInteger(fa); 155 readInteger(costs[i]); 156 readInteger(leads[i]); 157 if(fa != 0) 158 g[fa].push_back(i); 159 } 160 } 161 162 LeftistTree* lts; 163 long long res = 0; 164 void tree_dp(int node) { 165 lts[node].push(costs[node]); 166 for(int i = 0; i < (signed)g[node].size(); i++) { 167 tree_dp(g[node][i]); 168 lts[node].merge(lts[g[node][i]]); 169 } 170 while(lts[node].sum > m) 171 lts[node].pop(); 172 smax(res, (long long)lts[node].size * leads[node]); 173 } 174 175 inline void solve() { 176 lts = new LeftistTree[(n + 1)]; 177 tree_dp(1); 178 printf(Auto, res); 179 } 180 181 int main() { 182 init(); 183 solve(); 184 return 0; 185 }
最后的部分
小练习的答案
1.错误。一个很好的反例,根节点就不满足(下图)
2.错误。在合并的讲解中有个很好的例子。上图的9增加一个右子树也成了反例。
3.正确。如果数据满足堆的性质,找一个端点作为根,其他的挨个作为前一个的左子树。
参考资料
《左偏树的特点及其应用》 王源河
特别感谢
Doggu
Idy002
欢迎指出以上存在的问题。