现想现写的,没有借鉴别人的任何东西。
可持久化线段树1
考虑不会变得太多,每次该值操作只会改变一个位置的值,其它位置是可以继承的。如果用数组,那就是下标继承。如果把数组分成 \(2\) 半,那改一个值,就一半继承,另一半重新赋值。而用线段树,就可以做到区间继承 \(\log\) 的时间复杂度。
所以就是:当前区间分成 \(2\) 半,一半直接继承原来的,另一半继续递归下去,又可以分。
查询跟普通线段树一样。
时间复杂度 \(O(q\log_{2}n)\)。
#include <bits/stdc++.h> using namespace std; const int MAXN=2e7+4e6; int tr[MAXN],ls[MAXN],rs[MAXN],tot; int Root[MAXN]; void Add(int &u1,int u2,int l,int r,int id,int Val) { if(!u1) { u1=++tot; } if(l==r) { tr[u1]=Val; return; } int Mid=l+r>>1; if(id<=Mid) { rs[u1]=rs[u2]; Add(ls[u1],ls[u2],l,Mid,id,Val); } else { ls[u1]=ls[u2]; Add(rs[u1],rs[u2],Mid+1,r,id,Val); } } int Query(int u,int l,int r,int x) { if(l==r) { return tr[u]; } int Mid=l+r>>1; if(x<=Mid) return Query(ls[u],l,Mid,x); else return Query(rs[u],Mid+1,r,x); } int N,Q; int a[MAXN]; void Build(int &u,int l,int r) { if(!u) { u=++tot; } if(l==r) { tr[u]=a[l]; return; } int Mid=l+r>>1; Build(ls[u],l,Mid); Build(rs[u],Mid+1,r); } int main() { scanf("%d%d",&N,&Q); for(int i=1;i<=N;i++) { scanf("%d",&a[i]); } Build(Root[0],1,N); for(int i=1;i<=Q;i++) { int v,op,Loc,Value; scanf("%d%d%d",&v,&op,&Loc); if(op==1) { scanf("%d",&Value); Add(Root[i],Root[v],1,N,Loc,Value); } if(op==2) { Root[i]=Root[v]; printf("%d\n",Query(Root[v],1,N,Loc)); } } }
可持久化线段树2
离散化,建权值线段树。对于第 \(i\) 个值,才建一棵线段树包含 \(a_1\) 至 \(a_i\) 的值,这可以持久化。
记录每个值域线段树每个值域区间的 Size 表示这里面有多少个有被用的叶子,上述操作相当于每次新建一棵线段树,新将一个叶子赋值为被用过。
然后查询区间 \([L,R]\) 第 \(k\) 大,对于当前递归到的区间,如果左区间的 \(Size_R-Size_{L-1}\) 比 \(k\) 小,那就说明左区间的数不够 \(k\) 个,第 \(k\) 大一定为右区间的第 \(k-(Size_R-Size_{L-1})\) 大,再递归下去。反之就不减,递归到左区间。
最后一定会出结果,除非这个区间一共都没有 \(k\) 个值。
时间复杂度 \(O((n+q)\log_2 n)\)
#include <bits/stdc++.h> using namespace std; const int MAXN=5e6+50; int N,Q; int a[MAXN]; vector<int>lsh; int Size[MAXN]; int ls[MAXN],rs[MAXN]; int tot; int Root[MAXN]; void Add(int &u1,int u2,int l,int r,int Val) { if(!u1) u1=++tot; if(l==r) { Size[u1]=Size[u2]+1; return; } int Mid=l+r>>1; if(Val<=Mid) { rs[u1]=rs[u2]; Add(ls[u1],ls[u2],l,Mid,Val); } else { ls[u1]=ls[u2]; Add(rs[u1],rs[u2],Mid+1,r,Val); } Size[u1]=Size[ls[u1]]+Size[rs[u1]]; } int Query(int u1,int u2,int l,int r,int k) { if(l==r) { return l; } int Mid=l+r>>1; if(k<=Size[ls[u1]]-Size[ls[u2]]) { return Query(ls[u1],ls[u2],l,Mid,k); } else { k-=(Size[ls[u1]]-Size[ls[u2]]); return Query(rs[u1],rs[u2],Mid+1,r,k); } } int Back[MAXN]; int main() { scanf("%d%d",&N,&Q); for(int i=1;i<=N;i++) { scanf("%d",&a[i]); lsh.push_back(a[i]); } sort(lsh.begin(),lsh.end()); int Cnt=unique(lsh.begin(),lsh.end())-lsh.begin(); for(int i=1;i<=N;i++) { a[i]=lower_bound(lsh.begin(),lsh.begin()+Cnt,a[i])-lsh.begin()+1; Back[a[i]]=lsh[a[i]-1]; } for(int i=1;i<=N;i++) { Add(Root[i],Root[i-1],1,Cnt,a[i]); } while(Q--) { int l,r,k; scanf("%d%d%d",&l,&r,&k); printf("%d\n",Back[Query(Root[r],Root[l-1],1,Cnt,k)]); } }
每次持久化都会增加 \(\log_2 n\) 个节点,加上最开始的 \(4n\) 左右个的节点,所以数组大小至少为 \(n\log_2 n+4n\),当然要视具体题目调整。
一般二维数点的题可持久化线段树都能做。二维数点的其它做法也都挺厉害。
经常与整体二分搭配使用。
最常出现的关键词就是“区间第 \(k\) 大”。
注意第二题的 Add 操作递归到底层后,Size 不能直接赋值为 1,而要加上前一棵线段树的同一节点的 Size,当有重复数字时这样做才对,而如果没有重复就不需要考虑。