初涉左偏树
昨天在九老师的课件里看见了左偏树(可并堆)。看上去是一种有趣的东西
左偏树介绍
左偏树的历史
在网上搜了一下左偏树的历史,似乎是在2005年左右出现的数据结构。参见:
IOI2005国家集训队论文「左偏树的特点及其应用」by黄源河
左偏树的定义
以下是九老师的定义
定义一个树的斜深度为从根节点开始一直向右走走到叶子节点的步数。
左偏树是一种特殊的堆,满足左儿子的斜深度大于等于右儿子。
事实上左偏堆有两种写法。一种是使「斜深度」平衡,另一种是使「子树大小」平衡。
不过似乎区别不大?
左偏树长什么样子
偷张图先
大概左偏树就是长成这个样子。那么名副其实的是,左偏树的图形化结构就是一颗向左倾斜的树。显而易见,这样使得左偏树的根节点编号与子节点编号之间毫无规律,因此它不是一个二叉堆(但也许是二项堆?)。
左偏树的特点
它往左偏又有什么用呢?
左偏树优势在于是一种可并堆,就是合并起来更加快速的堆。普通的二叉堆合并需要$O(n)$,而左偏树合并只需要$O(log n)$。
显然它左偏之后右儿子相对来说较少,那么每一次合并时只需把新的树与右儿子合并,就能够节省下时间。
例如我们用 子树大小 来平衡左右子树,那么有右子树大小$right.size$小于等于左子树大小$left.size$。因而每一次向右合并的规模是会减半的,故合并复杂度为$O(logn)$。
左偏树板子
P3377 【模板】左偏树(可并堆)
题目描述
如题,一开始有N个小根堆,每个堆包含且仅包含一个数。接下来需要支持两种操作:
操作1: 1 x y 将第x个数和第y个数所在的小根堆合并(若第x或第y个数已经被删除或第x和第y个数在用一个堆内,则无视此操作)
操作2: 2 x 输出第x个数所在的堆最小数,并将其删除(若第x个数已经被删除,则输出-1并无视删除操作)
输入输出格式
输入格式:
第一行包含两个正整数N、M,分别表示一开始小根堆的个数和接下来操作的个数。
第二行包含N个正整数,其中第i个正整数表示第i个小根堆初始时包含且仅包含的数。
接下来M行每行2个或3个正整数,表示一条操作,格式如下:
操作1 : 1 x y
操作2 : 2 x
输出格式:
输出包含若干行整数,分别依次对应每一个操作2所得的结果。
数据规模
对于30%的数据:N<=10,M<=10
对于70%的数据:N<=1000,M<=1000
对于100%的数据:N<=100000,M<=100000
这个是luogu上左偏树的模板题。唯一有些区别的就是再加一个并查集,判断$x,y$是否在同一个堆里即可。
自己提交的时候RE了好多,发现是unions没有return
话说没return照样本地一点事没有的事情真是mmp。早上还有一道题读优没打完居然本地过pretest和手造数据
上代码:
1 #include<bits/stdc++.h> 2 using namespace std; 3 int n,m; 4 struct node 5 { 6 int lch,rch,dist,num,fa; 7 }f[100035]; 8 int read() 9 { 10 char ch = getchar();int num = 0;bool fl = 0; 11 for (; !isdigit(ch); ch = getchar()) 12 if (ch=='-')fl = 1; 13 for (; isdigit(ch); ch = getchar()) 14 num = (num<<3)+(num<<1)+ch-48; 15 if (fl)num = -num; 16 return num; 17 } 18 int find(int x){return f[x].fa?find(f[x].fa):x;} 19 void swap(int &a, int &b){a^=b;b^=a;a^=b;} 20 int unions(int x, int y) 21 { 22 if (x==0 || y==0)return x+y; 23 if (f[x].num > f[y].num || (f[x].num==f[y].num && x > y)) 24 swap(x, y); 25 int &ll = f[x].lch,&rr = f[x].rch; 26 rr = unions(rr, y); 27 f[rr].fa = x; 28 if (f[ll].dist < f[rr].dist)swap(ll,rr); 29 f[x].dist = f[rr].dist+1; 30 return x; 31 } 32 void erase(int x) 33 { 34 int lc = f[x].lch,rc = f[x].rch; 35 f[x].num = -1;f[lc].fa = 0;f[rc].fa = 0; 36 unions(lc, rc); 37 } 38 int main() 39 { 40 n = read();m = read(); 41 f[0].dist = -1; 42 register int i; 43 for (i=1; i<=n; i++) 44 f[i].num = read(); 45 for (i=1; i<=m; i++) 46 { 47 int opt = read(), x = read(); 48 if (opt-1){ 49 if (f[x].num!=-1){ 50 int fx = find(x); 51 printf("%d\n",f[fx].num); 52 erase(fx); 53 }else printf("-1\n"); 54 } 55 else{ 56 int y = read(); 57 if ((f[x].num==-1) || (f[y].num==-1)) 58 continue; 59 int fx = find(x),fy = find(y); 60 if (fx!=fy) unions(fx, fy); 61 } 62 } 63 return 0; 64 }
P3378 【模板】堆
题目描述
如题,初始小根堆为空,我们需要支持以下3种操作:
操作1: 1 x 表示将x插入到堆中
操作2: 2 输出该小根堆内的最小数
操作3: 3 删除该小根堆内的最小数
呃,我先过「左偏树模板题」却WA了四发「堆模板题」也是很奇妙。
WA第一发:unions开了inline于是MLE
WA第二三次:初始化和并查集的锅
AC的时候:感谢XYZ帮忙调试了一阵子。
由于这个题是动态加入且支持删除的,特别的只有一个主堆,那么我们的关键就是维护主堆的根编号。我之前的想法是保留结构体内的father编号,每一次不路径压缩地自底向上查询祖先。但是事实证明这是会出锅的(虽然理论上看上去毫无问题的样子)。可以注意到,unions是有返回值的,况且返回的是合并之后的根编号。那么我们直接在每一次加入、删除的时候,使$root=unions(root, cnt)$或者$root=unions(root.left\_son, root.right\_son)$就可以了
(然而我依旧不懂为什么记录father每次查询为什么就不可以)
这是并查集的:
1 #include<cctype> 2 #include<cstdio> 3 using namespace std; 4 int n,m,cnt,root; 5 struct node 6 { 7 int lch,rch,dist,num,fa; 8 }f[1000035]; 9 int read() 10 { 11 char ch = getchar();int num = 0;bool fl = 0; 12 for (; !isdigit(ch); ch = getchar()) 13 if (ch=='-')fl = 1; 14 for (; isdigit(ch); ch = getchar()) 15 num = (num<<3)+(num<<1)+ch-48; 16 if (fl)num = -num; 17 return num; 18 } 19 int find(int x){return f[x].fa?find(f[x].fa):x;} 20 void swap(int &a, int &b){a^=b;b^=a;a^=b;} 21 int unions(int x, int y) 22 { 23 if (x==0 || y==0)return x+y; 24 if (f[x].num > f[y].num) 25 swap(x, y); 26 int &ll = f[x].lch,&rr = f[x].rch; 27 rr = unions(rr, y); 28 f[rr].fa = x; 29 f[ll].fa = x; 30 if (f[ll].dist < f[rr].dist)swap(ll,rr); 31 f[x].dist = f[rr].dist+1; 32 return x; 33 } 34 inline void erase(int x) 35 { 36 int lc = f[x].lch,rc = f[x].rch; 37 f[x].num = -1;f[lc].fa = 0;f[rc].fa = 0; 38 unions(lc, rc); 39 root = find(rc); 40 if (f[root].num==-1)root = find(lc); 41 if (f[root].num==-1)root = 0; 42 } 43 int main() 44 { 45 n = read(); 46 f[0].dist = -1; 47 f[0].num = -1; 48 register int i; 49 for (i=1; i<=n; i++) 50 { 51 int opt = read(); 52 if (opt==1){ 53 f[++cnt].num = read(); 54 if (!root)root = cnt; 55 else unions(root, cnt); 56 } 57 else{ 58 root = find(root); 59 if (opt==2) printf("%d\n",f[root].num); 60 else erase(root); 61 } 62 } 63 return 0; 64 }
这是直接利用返回值的:
1 #include<cctype> 2 #include<cstdio> 3 using namespace std; 4 int n,m,cnt,root; 5 struct node 6 { 7 int lch,rch,dist,num; 8 }f[1000035]; 9 int read() 10 { 11 char ch = getchar();int num = 0;bool fl = 0; 12 for (; !isdigit(ch); ch = getchar()) 13 if (ch=='-')fl = 1; 14 for (; isdigit(ch); ch = getchar()) 15 num = (num<<3)+(num<<1)+ch-48; 16 if (fl)num = -num; 17 return num; 18 } 19 void swap(int &a, int &b){a^=b;b^=a;a^=b;} 20 int unions(int x, int y) 21 { 22 if (x==0 || y==0)return x+y; 23 if (f[x].num > f[y].num) 24 swap(x, y); 25 int &ll = f[x].lch,&rr = f[x].rch; 26 rr = unions(rr, y); 27 if (f[ll].dist < f[rr].dist)swap(ll,rr); 28 f[x].dist = f[rr].dist+1; 29 return x; 30 } 31 inline void erase(int x) 32 { 33 int lc = f[x].lch,rc = f[x].rch; 34 f[x].num = -1; 35 root = unions(lc, rc); 36 } 37 int main() 38 { 39 n = read(); 40 f[0].dist = -1; 41 f[0].num = -1; 42 register int i; 43 for (i=1; i<=n; i++) 44 { 45 int opt = read(); 46 if (opt==1){ 47 f[++cnt].num = read(); 48 if (!root)root = cnt; 49 else root=unions(root, cnt); 50 } 51 else{ 52 if (opt==2) printf("%d\n",f[root].num); 53 else erase(root); 54 } 55 } 56 return 0; 57 }
一些参考资料附下
1.并不对劲的左偏树
4.笔试算法题(46):简介 - 二叉堆 & 二项树 & 二项堆 & 斐波那契堆
话说我惊奇地发现我在「堆模板题」写的左偏树和早上写的堆一样长
1 #include<cctype> 2 #include<cstdio> 3 #include<cstring> 4 int f[1000035]; 5 int n,tt,x; 6 int read() 7 { 8 char ch = getchar();int num = 0;bool fl = 0; 9 for (; !isdigit(ch); ch = getchar()) 10 if (ch=='-')fl = 1; 11 for (; isdigit(ch); ch = getchar()) 12 num = (num<<3)+(num<<1)+ch-48; 13 if (fl)num = -num; 14 return num; 15 } 16 void swap(int &a, int &b){a^=b;b^=a;a^=b;} 17 void put(int x) 18 { 19 f[++f[0]] = x; 20 int m = f[0]; 21 for (; m!=1; m>>=1) 22 { 23 if (f[m] < f[m>>1]) 24 swap(f[m], f[m>>1]); 25 else break; 26 } 27 } 28 void del() 29 { 30 f[1] = f[f[0]]; 31 f[0]--; 32 int m = 1,k; 33 for (; (m<<1)<=f[0];) 34 { 35 if (f[m<<1]<f[m<<1|1] || (m<<1|1)>f[0]) 36 k = m<<1; 37 else k = m<<1|1; 38 if (f[m]>f[k]){ 39 swap(f[m],f[k]); 40 m = k; 41 }else break; 42 } 43 } 44 int main() 45 { 46 n = read(); 47 for (int i=1; i<=n; i++) 48 { 49 tt = read(); 50 if (tt == 1)x = read(),put(x); 51 else{ 52 if (tt == 2)printf("%d\n",f[1]); 53 else del(); 54 } 55 } 56 return 0; 57 }
END