左偏树(可并堆)实现
题目描述 如题,一开始有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所得的结果。 输入输出样例 输入样例#1: 5 5 1 5 4 2 3 1 1 5 1 2 5 2 2 1 4 2 2 2 输出样例#1: 1 2 说明 当堆里有多个最小值时,优先删除原序列的靠前的,否则会影响后续操作1导致WA。 时空限制:1000ms,128M 数据规模: 对于30%的数据:N<=10,M<=10 对于70%的数据:N<=1000,M<=1000 对于100%的数据:N<=100000,M<=100000 样例说明: 初始状态下,五个小根堆分别为:{1}、{5}、{4}、{2}、{3}。 第一次操作,将第1个数所在的小根堆与第5个数所在的小根堆合并,故变为四个小根堆:{1,3}、{5}、{4}、{2}。 第二次操作,将第2个数所在的小根堆与第5个数所在的小根堆合并,故变为三个小根堆:{1,3,5}、{4}、{2}。 第三次操作,将第2个数所在的小根堆的最小值输出并删除,故输出1,第一个数被删除,三个小根堆为:{3,5}、{4}、{2}。 第四次操作,将第4个数所在的小根堆与第2个数所在的小根堆合并,故变为两个小根堆:{2,3,5}、{4}。 第五次操作,将第2个数所在的小根堆的最小值输出并删除,故输出2,第四个数被删除,两个小根堆为:{3,5}、{4}。 故输出依次为1、2。
左偏树模板
什么是左偏树呢?首先,从名字上看,它是一棵树。其实它还是一棵二叉树。它的节点上存4个值:左、右子树的地址,权值,距离。
权值就是堆里面的值。距离表示这个节点到它子树里面最近的叶子节点的距离。叶子节点距离为0。
既然是一种特殊的数据结构,那肯定有它自己的性质。左偏树有几个性质(小根为例)。
性质一:节点的权值小于等于它左右儿子的权值。
堆的性质,很好理解。
性质二:节点的左儿子的距离不小于右儿子的距离。
在写平衡树的时候,我们是确保它的深度尽量的小,这样访问每个节点都很快。但是左偏树不需要这样,它的目的是快速提取最小节点和快速合并。
所以它并不平衡,而且向左偏。但是距离和深度不一样,左偏树并不意味着左子树的节点数或是深度一定大于右子树。
性质三:节点的距离等于右儿子的距离+1。
没什么好说的= =
性质四:一个n个节点的左偏树距离最大为 log(n+1)-1log(n+1)−1
这个怎么证明呢?我们可以一点一点来。
若左偏树的距离为一定值,则节点数最少的左偏树是完全二叉树。
节点最少的话,就是左右儿子距离一样,这就是完全二叉树了。
若一棵左偏树的距离为k,则这棵左偏树至少有2k+1−1 个节点。
距离为k的完全二叉树高度也是k,节点数就是 2k+1−1 。
这样就可以证明性质四了。因为 n>=2k+1−1 ,所以 k<=log(n+1)−1
有了性质,我们来讲讲它的操作。
0.查找最小值
我们维护了小根堆,不停地从当前节点找父亲就能找到最小值(根),但如果树退化,查询就会变成O(N)的
于是我们用f[x]表示第i个点所在的树的根,用并查集findset的方法查找根
inline int get(int x){ return f[x]==x?x:f[x]=get(f[x]); }
1.合并
我们假设A的根节点小于等于B的根节点(否则交换A,B),把A的根节点作为新树C的根节点,剩下的事就是合并A的右子树和B了。
合并了A的右子树和B之后,A的右子树的距离可能会变大,当A的右子树 的距离大于A的左子树的距离时,性质二会被破坏。在这种情况下,我们只须要交换A的右子树和左子树。
而且因为A的右子树的距离可能会变,所以要更新A的距离=右儿子距离+1。这样就合并完了。
我们来分析一下复杂度。我们可以看出每次我们都把它的右子树放下去合并。
因为一棵树的距离取决于它右子树的距离(性质三),所以拆开的过程不会超过它的距离。
根据性质四,不会超过log(nx+1)+log(ny+1)−2 ,复杂度就是O(lognx+logny)
int merg(int x,int y){ if(!x||!y){ return x+y; } if(val[x]>val[y]||(val[x]==val[y]&&x>y)){ swap(x,y); } ch[x][1]=merg(ch[x][1],y); f[ch[x][0]]=f[ch[x][1]]=f[x]=x; if(dis[ch[x][0]]<dis[ch[x][1]]){ swap(ch[x][0],ch[x][1]); } dis[x]=dis[ch[x][1]]+1; return x; }
2.插入
插入一个节点,就是把一个点和一棵树合并起来。
因为其中一棵树只有一个节点,所以插入的效率是 O(logn)
3.删除最小/大点
因为根是最小/大点,所以可以直接把根的两个儿子合并起来。
因为只合并了一次,所以效率也是 O(logn) 。
inline void del(int x){ val[x]=-1; f[ch[x][0]]=ch[x][0]; f[ch[x][1]]=ch[x][1]; f[x]=merg(ch[x][0],ch[x][1]); }
代码
1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long LL; 4 const int INF=1e9+7,MAXN=1e5+1,MAXM=1e5+1; 5 int N,M; 6 int val[MAXN],dis[MAXN],f[MAXN],ch[MAXN][2]; 7 int merg(int x,int y){ 8 if(!x||!y){ 9 return x+y; 10 } 11 if(val[x]>val[y]||(val[x]==val[y]&&x>y)){ 12 swap(x,y); 13 } 14 ch[x][1]=merg(ch[x][1],y); 15 f[ch[x][0]]=f[ch[x][1]]=f[x]=x; 16 if(dis[ch[x][0]]<dis[ch[x][1]]){ 17 swap(ch[x][0],ch[x][1]); 18 } 19 dis[x]=dis[ch[x][1]]+1; 20 return x; 21 } 22 inline int get(int x){ 23 return f[x]==x?x:f[x]=get(f[x]); 24 } 25 inline void del(int x){ 26 val[x]=-1; 27 f[ch[x][0]]=ch[x][0]; 28 f[ch[x][1]]=ch[x][1]; 29 f[x]=merg(ch[x][0],ch[x][1]); 30 } 31 int main(){ 32 scanf("%d%d",&N,&M); 33 dis[0]=-1; 34 for(int i=1;i<=N;i++){ 35 f[i]=i; 36 scanf("%d",val+i); 37 } 38 for(int i=1;i<=M;i++){ 39 int sign,ii,jj; 40 scanf("%d",&sign); 41 if(sign==1){ 42 scanf("%d%d",&ii,&jj); 43 if(val[ii]==-1||val[jj]==-1||ii==jj){ 44 continue; 45 } 46 int f1=get(ii),f2=get(jj); 47 merg(f1,f2); 48 }else{ 49 scanf("%d",&ii); 50 if(val[ii]==-1){ 51 puts("-1"); 52 }else{ 53 ii=get(ii); 54 printf("%d\n",val[ii]); 55 del(ii); 56 } 57 } 58 } 59 return 0; 60 }