以前学过可持久化线段树,但是只会做区间第k小qwq(逃)。决定这段时间重新捡回来。
推荐博客 https://www.cnblogs.com/flashhu/p/8301774.html
https://yjzoier.gitee.io/hexo/p/af72.html
首先是可持久化线段树
可持久化线段树即可以访问历史版本的线段树,暴力做法我们可以对每个历史版本建一棵线段树,容易发现这样会MLE。因为连续版本只是插入一个数的关系,我们发现其实版本i和版本i-1只有一条链的差别(从根节点到插入数的叶子结点这条链),所有我们可以让版本i和版本i-1公用相同结点而只建立新的不同链。
这样的可持久化线段树插入和查询时间依然是O(logn),且这N棵线段树的空间只要NlogN。
但是要注意的是因为结点公用的关系,其实我们不能修改历史版本的信息,而只能发布新版本。
以洛谷P3919为模板
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
#include<bits/stdc++.h> using namespace std; const int N=1e6+10; int n,m,a[N],root[N]; int cnt=0,lc[N<<6],rc[N<<6],val[N<<6]; //动态建树函数 一般都会返回新建结点编号 int build(int l,int r) { int q=++cnt; //动态建树 if (l==r) { lc[q]=rc[q]=0; val[q]=a[l]; return q; } int mid=l+r>>1; lc[q]=build(l,mid); rc[q]=build(mid+1,r); return q; } //这个其实是插入函数,因为公用结点的缘故并不能修改版本信息导致其他版本受影响 int update(int p,int l,int r,int x,int v) { //新版本线段树是基于版本p而来 int q=++cnt; //动态建树 if (l==r) { lc[q]=rc[q]=0; val[q]=v; return q; } int mid=l+r>>1; lc[q]=lc[p]; rc[q]=rc[p]; val[q]=val[p]; //为了公用信息,先复制一份 if (x<=mid) lc[q]=update(lc[p],l,mid,x,v); if (x>mid) rc[q]=update(rc[p],mid+1,r,x,v); return q; } int query(int p,int l,int r,int x) { //查询版本p位置x的值 if (l==r) return val[p]; int mid=l+r>>1; if (x<=mid) return query(lc[p],l,mid,x); if (x>mid) return query(rc[p],mid+1,r,x); } int main() { cin>>n>>m; for (int i=1;i<=n;i++) scanf("%d",&a[i]); root[0]=build(1,n); for (int i=1;i<=m;i++) { int k,opt,x,v; scanf("%d%d",&k,&opt); if (opt==1) { scanf("%d%d",&x,&v); root[i]=update(root[k],1,n,x,v); } else { scanf("%d",&x); printf("%d\n",query(root[k],1,n,x)); root[i]=root[k]; } } return 0; }
然后我们接着讲主席树,主席树和可持久化线段树到底是什么关系我也不太明白(知乎上有人说是包含关系,有人说是同一样东西),没所谓反正这又不是重点(逃)。不管怎样主席树和可持久化线段树确实结构上很像,但是主席树的思想要更为复杂一些。这里说一下本蒟蒻的理解,如果错了还请各位大佬指出,蒟蒻感激不尽 。
先是静态的主席树,我们以洛谷P3834为例。
首先给出一句话:可持久化线段树+权值线段树+前缀和思想=主席树
这怎么理解呢?我们先考虑一种暴力的做法:对于前1个数建第一棵权值线段树(当然你得先去了解什么是权值线段树不然没法往下看),对于前2个数建第二棵权值线段树,对于前3个数建第三棵权值线段树......对于前n个数建第n棵权值线段树。然后我们开始处理询问,例如询问为区间[ql,qr]中第K小的数是那个?那么我们看第ql-1棵权值线段树和第qr棵权值线段树,假设在第ql-1棵线段树结点p代表[l,r]区间而在第qr棵线段树结点q代表[l,r]区间,仔细观察因为我们上诉建权值线段树的时候是用了一种前缀和思想建立的,那么此时的sum[lson[q]]-sum[lson[p]]是不是就代表权值位于[l,mid] (mid=l+r>>1) 的数的个数(并且这些数一定是询问中[ql,qr]区间中的数:这是前缀和相减的缘故)。那么我们不断左右细分最终就能得到答案。
上诉算法在时间上询问是logn的,但问题是建立n棵权值线段树会MLE。回想起我们先讲的可持久化线段树,我们发现:哎,这n棵权值线段树不就可以公用结点信息变成可持久化线段树吗?于是我们共用一下信息,呼呼呼,终于得到主席树啦。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
#include<bits/stdc++.h> using namespace std; const int N=2e5+10; int n,q,tot,cnt=0; int a[N],b[N],root[N]; struct node{ int lc,rc,sum; }tree[N<<5]; void pushup(int x) { tree[x].sum=tree[tree[x].lc].sum+tree[tree[x].rc].sum; } int Build(int l,int r) { //初始版本的树,动态建树 int q=++cnt; if (l==r) { tree[q].sum=0; tree[q].lc=tree[q].rc=0; return q; } int mid=l+r>>1; tree[q].lc=Build(l,mid); tree[q].rc=Build(mid+1,r); return q; } int Insert(int p,int l,int r,int val) { //增加新点并返回编号 int q=++cnt; if (l==r) { tree[q].lc=tree[q].rc=0; tree[q].sum=tree[p].sum+1; return q; } int mid=l+r>>1; tree[q]=tree[p]; if (val<=mid) tree[q].lc=Insert(tree[p].lc,l,mid,val); //插入到左边,所以只有左儿子改变,其他沿用p点 if (val>mid) tree[q].rc=Insert(tree[p].rc,mid+1,r,val); //插入到右边,所以只有右儿子改变,其他沿用p点 pushup(q); //更新新增点 return q; } int query(int p,int q,int l,int r,int k) { //在p版本和q版本线段树查找[l,r]第k小 if (l==r) return a[l]; int mid=l+r>>1; int tmp=tree[tree[q].lc].sum-tree[tree[p].lc].sum; if (k<=tmp) return query(tree[p].lc,tree[q].lc,l,mid,k); //pq版本通史左跳 if (k>tmp) return query(tree[p].rc,tree[q].rc,mid+1,r,k-tmp); //pq版本同时右跳 } int main() { scanf("%d%d",&n,&q); root[0]=Build(1,n); //先建一棵空树 for (int i=1;i<=n;i++) scanf("%d",&a[i]); memcpy(b,a,sizeof(a)); sort(a+1,a+n+1); tot=unique(a+1,a+n+1)-(a+1); for (int i=1;i<=n;i++) { int x=lower_bound(a+1,a+tot+1,b[i])-a; root[i]=Insert(root[i-1],1,tot,x); //逐个插入数并保留历史版本 } for (int i=1;i<=q;i++) { int l,r,k; scanf("%d%d%d",&l,&r,&k); printf("%d\n",query(root[l-1],root[r],1,tot,k)); } return 0; }
动态主席树
好了,我们发现上面的主席树因为结点公用的缘故不能修改某个版本的信息,那么要修改的主席树怎么办呢?这就是动态的主席树了。更准确来说这应该是树套树,也有很多人也叫它为动态主席树或者带修改的主席树,但我感觉树套树和主席树差别还是有点大的。
我们以洛谷P2617为例
我们回想静态的主席树,它其实就是一棵前缀和线段树然后共用了结点,相当于root[i]=a[i]+root[i-1](当然这里的运算不是简单的加减运算,这是一种抽象的理解方式qwq)。于是我们一旦需要修改某个位置x的值,势必会使得root[x]到root[n]的值都改变了,如果暴力修改就得把root[x]到root[n]的值都要改变一次,超时。
其实这个问题就是怎样能满足root单点修改,区间查询?看到这个要求我们很熟悉,这不就是树状数组吗?
所以我们线段树建成一棵树状数组就得到了树套树,即从外面来看是一棵树状数组,但是树状数组的每一个结点都是一棵权值线段树。这里要和主席树区分开来:例如主席树的root[3]=a[1]+a[2]+a[3]这是主席树的前缀和思想,但是树套树的root[3]=a[3]因为树状数组的构造里sum[3]只有a[3]这是树状数组的思想。也就是说主席树一个点就是前缀,树套树要一堆点和和才是前缀。
这也启发我们学习树套树的,不妨把一棵树当成一个结点去思考,这样会更容易想象。
树套树的细节还是很多的,细节实现建议看代码,蒟蒻学习的是上面博客大佬的写法。因为使用的是动态建树所以时间和空间复杂度都是O(n log^2n),对于这个问题贴一句上面博客大佬的话:其实还有一个问题,一开始本蒟蒻想不通,就是N棵线段树已经无法共用内存了,那空间复杂度不会是O(N2logN)吗?其实没必要担心的。。。。。。只考虑修改操作,每次有log棵线段树被挑出来,每个线段树只修改log个节点,因此程序一趟跑下来,仅有Nlog2N个节点被访问过,我们只需要动态开点就好了。。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
#include<bits/stdc++.h> using namespace std; const int N=1e5+10; int n,m,num,tot,a[N],root[N],b[N<<1]; int n1,n2,t1[N],t2[N]; int opt[N],ql[N],qr[N],qv[N]; int lc[N<<9],rc[N<<9],val[N<<9]; void update(int &rt,int l,int r,int x,int v) { //修改rt这棵线段树的x点加上v if (!rt) rt=++num; val[rt]+=v; if (l==r) return; int mid=l+r>>1; if (x<=mid) update(lc[rt],l,mid,x,v); if (x>mid) update(rc[rt],mid+1,r,x,v); } void update_pre(int id,int v) { //树状数组修改部分 int x=lower_bound(b+1,b+tot+1,a[id])-b; for (int i=id;i<=n;i+=i&-i) update(root[i],1,tot,x,v); } //这里的查询部分还是有主席树的前缀和相减思想 int query(int l,int r,int k) { //查询部分 if (l==r) return l; int mid=l+r>>1,sum=0; for (int i=1;i<=n2;i++) sum+=val[lc[t2[i]]]; //这一堆组成1~r的前缀 for (int i=1;i<=n1;i++) sum-=val[lc[t1[i]]]; //这一堆组成1~l-1的前缀 if (sum>=k) { for (int i=1;i<=n1;i++) t1[i]=lc[t1[i]]; //一起向左跳 for (int i=1;i<=n2;i++) t2[i]=lc[t2[i]]; return query(l,mid,k); //答案在左子树 } else { for (int i=1;i<=n1;i++) t1[i]=rc[t1[i]]; //一起向右跳 for (int i=1;i<=n2;i++) t2[i]=rc[t2[i]]; return query(mid+1,r,k-sum); //答案在右子树 } } int query_pre(int l,int r,int k) { //树状数组查询部分 n1=n2=0; for (int i=l-1;i>=1;i-=i&-i) t1[++n1]=root[i]; //先把所有相关的线段树找出来, for (int i=r;i>=1;i-=i&-i) t2[++n2]=root[i]; //因为这一堆线段树加起来才是前缀 return query(1,tot,k); } int main() { cin>>n>>m; for (int i=1;i<=n;i++) scanf("%d",&a[i]),b[++tot]=a[i]; for (int i=1;i<=m;i++) { char s[3]; scanf("%s",s); if (s[0]=='Q') scanf("%d%d%d",&ql[i],&qr[i],&qv[i]); else { opt[i]=1; scanf("%d%d",&ql[i],&qv[i]); b[++tot]=qv[i]; //因为要离散化所以把修改的值也先读入 } } sort(b+1,b+tot+1); tot=unique(b+1,b+tot+1)-(b+1); for (int i=1;i<=n;i++) update_pre(i,1); //建树 for (int i=1;i<=m;i++) if (!opt[i]) { printf("%d\n",b[query_pre(ql[i],qr[i],qv[i])]); } else { update_pre(ql[i],-1); //先清除 a[ql[i]]=qv[i]; update_pre(ql[i],1); //再插入 } return 0; }
题目练习:
BZOJ 1878
BZOJ 2653
BZOJ 484E