可持久化线段树
可持久化
可持久化数据结构 (Persistent data structure) 总是可以保留每一个历史版本,并且支持操作的不可变特性 (immutable).
可持久化线段树
如何储存线段树每个版本?显然不可能将每个版本暴力存储一遍。
我们只需要:将各个版本之间的不同之处分别存储,相同部分共用节点。
如图:

单点修改的线段树每次修改只会改变一条链,直接新建一条链,剩余的连上之前版本的节点。
区间修改的线段树类似,注意懒标记的可持久化。
这样,时空复杂度就是 \(O(n+mlogn)\) 的。
主席树
主席树全称是可持久化权值线段树。
最经典的操作是维护静态区间第 \(k\) 小。
静态区间第 \(k\) 小
考虑对序列前缀建立主席树,每个版本的权值线段树表示对应前缀的权值情况。
对于查询 \([L,R]\) 的第 \(k\) 小,可以通过查询 版本 \(L-1\) 与 \(R\) 的线段树,通过前缀和相减得到 \([L,R]\) 的信息,再进行线段树二分即可。
下给出实现:
【模板】可持久化线段树 2
struct Segment_Tree{
int lch[M],rch[M],sum[M],cnt;
int newnode(int x){
int id=++cnt; sum[id]=sum[x];
lch[id]=lch[x],rch[id]=rch[x];
return id;
}void add(int x,int &y,int id,int L,int R){
if(L==R){ y=newnode(x),sum[y]++; return; }
int mid=(L+R)>>1; y=newnode(x);
if(id<=mid)add(lch[x],lch[y],id,L,mid);
else add(rch[x],rch[y],id,mid+1,R);
sum[y]=sum[lch[y]]+sum[rch[y]];
}int query(int x,int y,int k,int L,int R){
if(L==R)return L; int mid=(L+R)>>1;
if(k<=sum[lch[y]]-sum[lch[x]])return query(lch[x],lch[y],k,L,mid);
else return query(rch[x],rch[y],k-sum[lch[y]]+sum[lch[x]],mid+1,R);
}
}tr;
更多应用
考虑主席树的本质,其实就是一堆有关联的线段树。
不难想到,其实 扫描线+线段树 和 主席树 在只需要访问一个版本的时候可以互相替换。
这样主席树其实还可以处理一些二维平面问题。
例题
[SDOI2009] HH 的项链
这道题和值域有很大的关联,故考虑从值域入手。
统计区间不同的颜色个数,就等同于 统计区间中第一次出现的颜色个数。
这些颜色有什么不一样的?它们上一次出现的位置一定在区间之外!
而我们又不希望区间之外的颜色来干扰,故考虑建立主席树,维护每一个颜色上一次出现的位置。
(当然也可以用 扫描线+树状数组)
//转化为维护该颜色上一个颜色不在此区间内的个数
#include<iostream>
using namespace std;
constexpr int N=5e5+5,M=1e7;
int n,m,last[N],head[N],rot[N];
struct Segment_Tree{
int lch[M],rch[M],sum[M],cnt;
int newnode(int x){
int id=++cnt; sum[id]=sum[x];
lch[id]=lch[x],rch[id]=rch[x];
return id;
}void add(int x,int &y,int id,int L,int R){
if(L==R){ y=newnode(x),sum[y]++; return; }
int mid=(L+R)>>1; y=newnode(x);
if(id<=mid)add(lch[x],lch[y],id,L,mid);
else add(rch[x],rch[y],id,mid+1,R);
sum[y]=sum[lch[y]]+sum[rch[y]];
}int query(int x,int y,int l,int r,int L,int R){
if(L==l&&R==r)return sum[y]-sum[x];
int mid=(L+R)>>1;
if(r<=mid)return query(lch[x],lch[y],l,r,L,mid);
else if(mid+1<=l)return query(rch[x],rch[y],l,r,mid+1,R);
else return query(lch[x],lch[y],l,mid,L,mid)+query(rch[x],rch[y],mid+1,r,mid+1,R);
}
}tr;
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1,x;i<=n;i++)
cin>>x,last[i]=head[x],head[x]=i;
for(int i=1;i<=n;i++)
tr.add(rot[i-1],rot[i],last[i],0,n);
for(int i=1,ans=0,L,R;i<=m;i++){
cin>>L>>R,L=(L+ans)%n+1,R=(R+ans)%n+1;
if(L>R)swap(L,R);
ans=tr.query(rot[L-1],rot[R],0,L-1,0,n);
cout<<ans<<'\n';
}return 0;
}
[SDOI2013] 森林
在做这道题之前,可以先考虑一个子问题:Count on a tree
很简单,对树的根链建立主席树,查询时利用树上前缀和。
那么这道题呢?树的形态会改变。
注意到只有连边操作,故考虑 启发式合并,复杂度是 \(O(nlogn)\) 的。
每次合并直接暴力增加主席树,总的复杂度就是 \(O(nlog^2n)\) 的。
(代码太长,就不放了)
[国家集训队] middle
看到中位数,想到二分。
对于区间 \([L,R]\) 中,得到 \(< mid\) 与 \(\ge mid\) 的数的个数 分别为 \(x,y\)。
令满足 \(L\in [a,b],R\in [c,d]\) 的 \(x,y\) 中,\(a_1=max(y-x),a_2=min(y-x)\)
若 \(a_1\ge a_2> 0\),说明 \(mid\) 一定不是中位数,但中位数一定比 \(mid\) 大。
若 \(a_1\ge 0 \ge a_2\),说明 \(mid\) 是中位数,但不一定是最大的中位数。
若 \(0>a_1 \ge a_2\),说明 \(mid\) 一定不是中位数,但中位数一定比 \(mid\) 小。
(可以用连续变化的函数理解,\(f(x)=f(x-1)\pm 1\))
统一一下:
若 \(max(y-x)\ge 0\),\(mid\) 应该变大。
若 \(max(y-x)< 0\),\(mid\) 应该变小。
但是如何处理 \(< mid\) 与 \(\ge mid\) 的数的个数?
考虑由于我们只需知道 \(<mid\) 的数的个数,故考虑将值域与下标互换,对于原本的值域建立主席树。
统计时对于区间求前后缀最大值 并 合并。
时间复杂度为 \(O(nlog^2n)\)。
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
constexpr int N=2e4+5,M=1e7;
int n,a[N],rk[N],tt,m,q[4],rot[N];
vector<int> v[N];
struct State{
int sum,suml,sumr;
State operator +(const State &b)const{
State a=*this,ans;
ans.sum=a.sum+b.sum;
ans.suml=max(a.suml,a.sum+b.suml);
ans.sumr=max(a.sumr+b.sum,b.sumr);
return ans;
}
};
struct Segment_Tree{
int lch[M],rch[M],cnt;
State dp[M];
void build(int &x,int L,int R){
x=++cnt; int mid=(L+R)>>1;
if(L==R){ dp[x]=State{1,1,1}; return; }
build(lch[x],L,mid),build(rch[x],mid+1,R);
dp[x]=dp[lch[x]]+dp[rch[x]];
}int newnode(int x){
lch[++cnt]=lch[x],rch[cnt]=rch[x];
dp[cnt]=dp[x]; return cnt;
}void add(int x,int &y,int id,int L,int R){
if(L==R){ y=newnode(x),dp[y]=State{-1,-1,-1}; return; }
int mid=(L+R)>>1; y=newnode(x);
if(id<=mid)add(lch[x],lch[y],id,L,mid);
else add(rch[x],rch[y],id,mid+1,R);
dp[y]=dp[lch[y]]+dp[rch[y]];
}State query(int x,int l,int r,int L,int R){
if(L==l&&r==R)return dp[x];
if(l>r)return State{0,0,0}; int mid=(L+R)>>1;
if(r<=mid)return query(lch[x],l,r,L,mid);
else if(mid+1<=l)return query(rch[x],l,r,mid+1,R);
else return query(lch[x],l,mid,L,mid)+query(rch[x],mid+1,r,mid+1,R);
}
}tr;
bool check(int x){
x--;
State xx=tr.query(rot[x],q[0],q[1],1,n);
State yy=tr.query(rot[x],q[1]+1,q[2]-1,1,n);
State zz=tr.query(rot[x],q[2],q[3],1,n);
int sum=xx.sumr+yy.sum+zz.suml;
return (sum>=0);
}signed main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n,tr.build(rot[0],1,n);
for(int i=1;i<=n;i++)cin>>a[i],rk[i]=a[i];
sort(rk+1,rk+n+1),tt=unique(rk+1,rk+n+1)-rk-1;
for(int i=1;i<=n;i++)a[i]=lower_bound(rk+1,rk+tt+1,a[i])-rk;
for(int i=1;i<=n;i++)v[a[i]].push_back(i);
for(int i=1;i<=tt;i++){
rot[i]=rot[i-1];
for(int u:v[i])tr.add(rot[i],rot[i],u,1,n);
}cin>>m;
for(int i=1,last=0;i<=m;i++){
for(int j=0;j<4;j++)
cin>>q[j],q[j]=(q[j]+last)%n+1;
sort(q,q+4);
int L=0,R=tt+1,mid;
while(L+1<R){
mid=(L+R)>>1;
if(check(mid))L=mid;
else R=mid;
}last=rk[L],cout<<last<<'\n';
}return 0;
}
[FJOI2016] 神秘数
先考虑如何求 \(S\) 的神秘数。
现将 \(S\) 从小到大排序,设当前的神秘数为 \(a\)。
若 \(S_i\le a\),则新的神秘数为 \(S_i+a\)。
若 \(s_i>a\),则集合的神秘数就为 \(a\),因为以后的数都不会产生贡献。
我们可以得到一个充分条件:若 \(a\) 为神秘数,则 \(\le a\) 的数的和 \(< a\)。
显然不是必要条件,因为无法确定 \(a\) 一定最小。
因此我们考虑一个暴力的算法:
初始时令 \(a=1\),
若 \(\le a\) 的数的和 \(sum<a\),得到神秘数为 \(a\)。
若 \(\le a\) 的数的和 \(sum\ge a\),令 \(a=sum+1\)。
这个算法就是对上面更暴力的方法的优化。
现在我们证明其复杂度:
由于每次产生贡献的值域为 \((a,sum+1]\),那么 \(sum'\ge sum+1+a+1=sum+a+2\),
所以有 \(sum''\ge sum'+1+a'+1\ge (sum+1+a+1)+1+(sum+1)+1=2sum+a+4=sum+sum'+2\)。
也就是 \(sum\) 成斐波那契数列一样增长,又知道斐波那契数列是指数级增长。
故复杂度为 \(logW\)。
由于上面的查询就是为权值线段树设计的,故考虑主席树来统计区间信息,查询区间和。
时间复杂度:\(O(mlognlogW)\)。
#include<iostream>
using namespace std;
constexpr int N=1e5+5,M=6e6+6;
int n,m,rot[N];
struct Segment_Tree{
int lch[M],rch[M],sum[M],sum2[M],cnt;
int newnode(int x){
lch[++cnt]=lch[x],rch[cnt]=rch[x];
sum[cnt]=sum[x],sum2[cnt]=sum2[x]; return cnt;
}void add(int x,int &y,int id,int L,int R){
if(L==R){ y=newnode(x),sum[y]++,sum2[y]+=L; return; }
int mid=(L+R)>>1; y=newnode(x);
if(id<=mid)add(lch[x],lch[y],id,L,mid);
else add(rch[x],rch[y],id,mid+1,R);
sum[y]=sum[lch[y]]+sum[rch[y]];
sum2[y]=sum2[lch[y]]+sum2[rch[y]];
}int query(int x,int y,int l,int r,int L,int R){
if(L==l&&r==R)return sum2[y]-sum2[x];
if(l>r)return 0; int mid=(L+R)>>1;
if(r<=mid)return query(lch[x],lch[y],l,r,L,mid);
else if(mid+1<=l)return query(rch[x],rch[y],l,r,mid+1,R);
else return query(lch[x],lch[y],l,mid,L,mid)+query(rch[x],rch[y],mid+1,r,mid+1,R);
}
}tr;
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n;
for(int i=1,x;i<=n;i++)
cin>>x,tr.add(rot[i-1],rot[i],x,1,1e9);
cin>>m;
for(int i=1,x,y,ans;i<=m;i++){
cin>>x>>y,x--,ans=1;
while(1){
int s=tr.query(rot[x],rot[y],1,ans,1,1e9);
if(!s)break;
if(s>=ans)ans=s+1;
else break;
}cout<<ans<<'\n';
}return 0;
}
动态主席树
世上哪有什么动态主席树,唯有树套树。
Dynamic Rankings
考虑树状数组套权值线段树,外层维护区间,内层维护权值。
修改时修改树状数组对应节点的线段树,
查询时现将所有用到外层节点处理好,再进行线段树二分,同样使用前缀和相减。
(线段树套线段树同理)
时间复杂度:\(O(nlogn+qlog^2n)\)
#include<iostream>
using namespace std;
constexpr int N=1e5+5,M=5e7;
int n,m,a[N];
struct Segment_Tree{
int lowbit(int x){ return x&(-x); }
int lch[M],rch[M],sum[M],cnt,rot[N];
int b[25],c[25];
void add_(int &x,int id,int val,int L,int R){
if(!x)x=++cnt; int mid=(L+R)>>1;
if(L==R){ sum[x]+=val; return; }
if(id<=mid)add_(lch[x],id,val,L,mid);
else add_(rch[x],id,val,mid+1,R);
sum[x]=sum[lch[x]]+sum[rch[x]];
}void add(int x,int id,int val){
for(int i=x;i<=n;i+=lowbit(i))
add_(rot[i],id,val,0,1e9);
}int query(int L,int R,int k){
int t1=0,t2=0; L--;
for(int i=R;i;i-=lowbit(i))b[++t1]=rot[i];
for(int i=L;i;i-=lowbit(i))c[++t2]=rot[i];
L=0,R=1e9;//线段树左右端点
while(L!=R){
int s=0,mid=(L+R)>>1;
for(int i=1;i<=t1;i++)s+=sum[lch[b[i]]];
for(int i=1;i<=t2;i++)s-=sum[lch[c[i]]];
if(k<=s){
for(int i=1;i<=t1;i++)b[i]=lch[b[i]];
for(int i=1;i<=t2;i++)c[i]=lch[c[i]];
R=mid;
}else {
for(int i=1;i<=t1;i++)b[i]=rch[b[i]];
for(int i=1;i<=t2;i++)c[i]=rch[c[i]];
L=mid+1,k-=s;
}
}return L;
}
}tr;
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i],tr.add(i,a[i],1);
for(int i=1,x,y,z;i<=m;i++){
char ch; cin>>ch>>x>>y;
if(ch=='Q')cin>>z,cout<<tr.query(x,y,z)<<'\n';
else tr.add(x,a[x],-1),a[x]=y,tr.add(x,a[x],1);
}return 0;
}
可持久化线段树扩展
可持久化数组
单点修改,单点查询线段树。
可持久化并查集
在可持久化数组的基础上实现。
注意不能路径压缩,只能使用 启发式合并/按轶合并。

浙公网安备 33010602011771号