主席树又叫可持久化权值线段树,一开始使用来解决第k大的问题,因其发明者黄嘉泰名字的首字母和某人的一样,所以被叫做主席树。
在了解主席树之前,我们先认识一下什么叫做权值线段树。
给你n个数,问你这n个数中第k小的数是哪个。像这种题我们一般都是直接排序然后暴力找,但是我们今天用线段树来试试。
例如a[12]={1,5,7,3,2,6,8,1,3,5,5,2},你要求出a数组中第7小的数的值。我们先建棵线段树用来存每个值出现的次数,按下标从小到大依次将数组中的元素放入插入线段树中。
插入前三个数后,线段树就变成了这样
继续插入元素,直至数组中的元素全部插入线段树中
像这样按权值建的树就叫做权值线段树
当全部插完后,就开始找第7小的数了。我们先从根开始,看看他左儿子的值是否大于k(这里k=7),假如左儿子的值大于等于k那么我们要找的第k小的数就在左边,否则就在右边。
在这里左儿子也就是节点2的值为6,小于7,所以我们要找的数在右边,接下来我们要找以节点3为根的子树中最小的那个数。
还是向上面那样一直递归,直到到了叶子节点,这时我们就找到了在数组中第7小的数了。
接下来就是找区间内第k小的数了。对于任意[l,r]区间,我们先需要知道这个区间内有些什么数,然后再去找第k小。
对于前一步,我们可以考虑建n棵权值线段树,第i棵权值线段树用来存[1,i]内各个数字的出现情况,这样我们就能直接利用前缀和的性质,让第r棵和第l-1棵权值线段树对应相减,这样我们就得到了[l,r]区间内数字的出现情况。
然后我们就只需递归查找第k小就行了
但是,要这样做的话我们就必须建n棵线段树,而每个线段树又要开四倍的空间,这样空间复杂度就太大了,我们必须要想办法去优化空间。
我们仔细观察每次建树就会发现相邻两颗树只有logn个点不同,其余的都一样,就像第7棵权值线段树和第8棵权值线段树,他们只有划杠的那几个点不一样
所以我们只需在上一颗树的基础上再开logn个点就行了,其他不变的结点直接继承上一颗树的对应结点,这样空间的开销就能接受了。
主席树例题:https://www.luogu.com.cn/problem/P3834
给你n个数,m个询问,每个问你[l,r]内的第k小的数是什么,n和m都是在1到2e5之间,数字大小在-1e9到1e9之间。
直接上主席树,先建n棵树然后利用前缀和弄出区间内的数,最后在递归找第k小的那个数。
#include<iostream> #include<algorithm> using namespace std; #define maxn 200005 int n,m,cnt,a[maxn],b[maxn],root[maxn*40],L[maxn*40],R[maxn*40],sum[maxn*40]; //root存根的编号,L存左儿子的编号,R存右儿子的编号,sum存数字数量 int update(int pre,int l,int r,int pos)//pre为上个版本的编号,pos为位置 { int rt=++cnt;//用rt存当前结点编号 L[rt]=L[pre];R[rt]=R[pre]; //先将左儿子和右儿子全部等于上个版本对应的左右儿子,后面再看具体是要更新哪个儿子 sum[rt]=sum[pre]+1;//数量加加 if(l==r)return rt; int mid=l+r>>1; if(mid>=pos)L[rt]=update(L[pre],l,mid,pos);//更新左儿子 else R[rt]=update(R[pre],mid+1,r,pos);//更新右儿子 return rt; } int query(int x,int y,int l,int r,int k)//x和y分别代表第l-1棵树和第r棵树的结点编号,k为第k小 { if(l==r)return l;//返回的是离散化后的数组中的位置 int mid=l+r>>1,tot=sum[L[y]]-sum[L[x]]; //tot为当前结点,两棵树的左儿子所包含的数字数量之差 if(tot>=k)return query(L[x],L[y],l,mid,k);//如果tot>=k那就意味着第k小在左子树中 else return query(R[x],R[y],mid+1,r,k-tot);//否则在右子树中,接下来就是找右子树中第k-tot小的数 } int main() { cin>>n>>m; for(int i=1;i<=n;i++) { cin>>a[i]; b[i]=a[i]; //一般数值范围都很大,直接按数值来建树会超空间,所以经常先进行离散化,然后再按离散化后的数组建树 } sort(b+1,b+1+n);//排序 int len=unique(b+1,b+1+n)-b-1;//去重 for(int i=1;i<=n;i++) { int pos=lower_bound(b+1,b+1+len,a[i])-b;//a[i]在b数组中的位置 root[i]=update(root[i-1],1,len,pos);//建第i棵线段树,第pos个数出现次数加加 } int l,r,k; while(m--) { cin>>l>>r>>k; cout<<b[query(root[l-1],root[r],1,len,k)]<<endl; /*查询第k小,利用第r棵树和第l-1棵树来找,注意query返回的是b数组中的位置 从两棵树的根开始递归查,若左儿子满足则往左儿子递归否则往右儿子递归 */ } return 0; }
对于主席树的空间大小,一般为O(nlogn)(不带修改),一般情况下开个40倍就够了。
原文地址:https://www.cnblogs.com/chen99/p/12191489.html