定义:
**可持久化线段树**是一种可持久化数据结构,也被称为主席树。这种数据结构在普通线段树的基础之上支持**查询某个历史版本**,同时时间复杂度与线段树是同级,**空间复杂度相较而言更高**。
与大部分可持久化数据结构类似,**可持久化线段树尽可能多地共用先前某一个版本的结点,从而节省大量的空间与时间**。
思路(以求区间第 (k) 小为例):
主席树在建树上和权值线段树(按权值建的树)类似,利用权值线段树,我们可以求出整个区间的第 (k) 大(小)值。但对于任意区间上的最值查询,就不好处理。主席树之所以能处理该种问题,关键在于其可持久化的结构,把每一个插入的历史状态都保存下来。
首先,先看用权值线段树如何求整体的第 (k) 大。权值线段树中,每个叶子节点存储某个元素出现次数,一条线段的总和表示区间内所有数出现次数的总和。查询过程:从根节点向下走,如果 (k) 小于等于左子树大小,说明第 (k) 大在左子树的区间中,在左子树中继续查找即可;否则,说明第 (k) 大在右子树的区间中,此时将 (k) 减去左子树大小,并在右子树中继续查找。
如果用权值线段树求任意区间第 (k) 大,那么我们需要建立 (n+1) 棵线段树。第 (i) 棵线段树为在第 (i-1) 棵线段树的基础上插入第 (i) 个数的信息所建的。利用前缀和的思想,要求区间 ([l,r]) 内的第 (k) 大,可以用第 (r) 树的信息减去第 (l-1) 棵树的信息。然后,按照权值线段树求整体第 (k) 的查询步骤即可。 虽然这样在时间上可以,时间复杂度认为 (O(nlogn)),但空间上就完全爆炸了。正常一棵线段树开 (4N) 的空间,那么这种做法需要的空间就是 (4N^2),显然不可行。
但是思考上述的求解过程,可以发现。每插入一个点的信息,更改的只是该点到根节点之间这条链的信息,其它信息保持不变。即相邻的两棵线段树仅有 (logn) 个节点不同,不同的点共有 (nlogn) 个。所以,就没必要浪费多余的空间去存相同的信息。之前的每棵树就可以用一条链来替代,相同部分直接用前一棵树。那么,空间就大大缩小了。
贴张图:
实现:
1.为了节约空间,主席树中点的并不像一般线段树那样,整体结构在开数组的时候就固定了。而是采用动态开点的方法,用变量 (cnt) 来记总的点数(类似于字典树的建立)。并且编号为 (rt) 的点,其左右儿子节点并不一定为 ((rt<<1)) 和 ((rt<<1|1)),因此需要两个新的数组来存各个点的左右孩子编号。
2.在初始,要建立空树。此后,每次插入点,都在前一棵树的基础上更改。
应用:
主要解决区间问题,利用前缀和的思想,经常和时间相关
1.静态区间第 (k) 大(小)数值:
例题:洛谷P3834
给定 (n) 个整数构成的序列,将对于指定的闭区间查询其区间内的第 (k) 小值。
(代码见模板)
【模板】:
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int a[N],b[N],root[N];
int tree[N<<5],lson[N<<5],rson[N<<5],cnt;//空间开到N<<5,否则RE
int build(int l,int r)
{
int t=++cnt;
if(l==r)
{
tree[t]=0;
//lson[t]=0;
//rson[t]=0;
return t;
}
int mid=(l+r)>>1;
lson[t]=build(l,mid);
rson[t]=build(mid+1,r);
tree[t]=tree[lson[t]]+tree[rson[t]];
return t;
}
int update(int l,int r,int p,int rt)
{
int t=++cnt;
if(l==r)
{
tree[t]=tree[rt]+1;
//lson[t]=0;
//rson[t]=0;
return t;
}
int mid=(l+r)>>1;
if(p<=mid)
{
rson[t]=rson[rt];
lson[t]=update(l,mid,p,lson[rt]);
}
else
{
lson[t]=lson[rt];
rson[t]=update(mid+1,r,p,rson[rt]);
}
tree[t]=tree[lson[t]]+tree[rson[t]];
return t;
}
int query(int u,int v,int l,int r,int k)
{
int mid=(l+r)>>1;
int num=tree[lson[v]]-tree[lson[u]];
if(l==r)
return l;
if(k<=num)
return query(lson[u],lson[v],l,mid,k);
else
return query(rson[u],rson[v],mid+1,r,k-num);
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(b+1,b+1+n);
int len=unique(b+1,b+1+n)-b-1;
cnt=0;
root[0]=build(1,len);
for(int i=1;i<=n;i++)
{
int pos=lower_bound(b+1,b+1+len,a[i])-b;
root[i]=update(1,len,pos,root[i-1]);
}
int l,r,k;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&l,&r,&k);
printf("%dn",b[query(root[l-1],root[r],1,len,k)]);
}
return 0;
}
练习:
hdu4417【静态区间计数】
代码
2.统计区间内不同数的个数:
例题:SPOJ - DQUERY
题意:
给定一个数组 (a[n]),请你统计 (L) 到 (R) 中不同数字的个数。
分析:和求区间第 (k) 小不同,本题的主席树并非建立在权值上。而是建立在位置上。关键在于一个 (last[a]) 数组,存数值 (a) 最后一次出现的位置。每当要插入一个数时,如果该位置为该数第一次出现的位置,那么该位置 (+1);如果不是,先在前一棵树的基础上,上一次出现的位置 (-1),建立本次对应的树。然后在本次建立的树的基础上,当前位置 (+1)。
#include <bits/stdc++.h>
using namespace std;
const int N=3e4+5;
const int maxn=1e6+5;
int root[N],last[maxn],tree[N*20],lson[N*20],rson[N*20],cnt;
int build(int l,int r)
{
int t=++cnt;
if(l==r)
{
tree[t]=0;
return t;
}
int mid=(l+r)>>1;
lson[t]=build(l,mid);
rson[t]=build(mid+1,r);
tree[t]=tree[lson[t]]+tree[rson[t]];
return t;
}
int update(int l,int r,int p,int val,int rt)
{
int t=++cnt;
if(l==r)
{
tree[t]=tree[rt]+val;
return t;
}
int mid=(l+r)>>1;
if(p<=mid)
{
lson[t]=update(l,mid,p,val,lson[rt]);
rson[t]=rson[rt];
}
else
{
lson[t]=lson[rt];
rson[t]=update(mid+1,r,p,val,rson[rt]);
}
tree[t]=tree[lson[t]]+tree[rson[t]];
return t;
}
int query(int l,int r,int L,int R,int rt)
{
if(L<=l&&r<=R)
return tree[rt];
int mid=(l+r)>>1;
int ans=0;
if(L<=mid)
ans+=query(l,mid,L,R,lson[rt]);
if(R>mid)
ans+=query(mid+1,r,L,R,rson[rt]);
return ans;
}
int main()
{
int n,q,a;
scanf("%d",&n);
cnt=0;
root[0]=build(1,n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a);
if(last[a]==0)
root[i]=update(1,n,i,1,root[i-1]);
else
{//!!!!
root[i]=update(1,n,last[a],-1,root[i-1]);
root[i]=update(1,n,i,1,root[i]);
}
last[a]=i;
}
scanf("%d",&q);
while(q--)
{
int l,r;
scanf("%d%d",&l,&r);//cout<<tree[root[r]]<<endl;
printf("%dn",query(1,n,l,r,root[r]));
}
return 0;
}
练习:
hdu5919 Sequence II
分析:
求区间内不同数的个数还是按照上一题的步骤,但由于要记子序列的个数第一次出现的位置,因此,从后向前插入点就会方便很多,此时 (last[a]) 就表示第一次出现的位置。
代码
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int a[N],last[N],root[N],tree[N*50],lson[N*50],rson[N*50],cnt;
vector<int>an;
int build(int l,int r)
{
int t=++cnt;
if(l==r)
{
tree[t]=0;
return t;
}
int mid=(l+r)>>1;
lson[t]=build(l,mid);
rson[t]=build(mid+1,r);
tree[t]=tree[lson[t]]+tree[rson[t]];
return t;
}
int update(int l,int r,int pos,int val,int rt)
{
int t=++cnt;
if(l==r)
{
tree[t]=tree[rt]+val;
return t;
}
int mid=(l+r)>>1;
if(pos<=mid)
{
lson[t]=update(l,mid,pos,val,lson[rt]);
rson[t]=rson[rt];
}
else
{
lson[t]=lson[rt];
rson[t]=update(mid+1,r,pos,val,rson[rt]);
}
tree[t]=tree[lson[t]]+tree[rson[t]];
return t;
}
int query(int l,int r,int L,int R,int rt)
{
if(L<=l&&r<=R)
return tree[rt];
int mid=(l+r)>>1;
int ans=0;
if(L<=mid)
ans+=query(l,mid,L,R,lson[rt]);
if(R>mid)
ans+=query(mid+1,r,L,R,rson[rt]);
return ans;
}
int query2(int l,int r,int k,int rt)
{
if(l==r)
return l;
int mid=(l+r)>>1;
int num=tree[lson[rt]];
if(k<=num)
return query2(l,mid,k,lson[rt]);
else
return query2(mid+1,r,k-num,rson[rt]);
}
int main()
{
int t,n,m,cot=0;
scanf("%d",&t);
while(t--)
{
memset(last,-1,sizeof(last));
an.clear();
an.push_back(0);
scanf("%d%d",&n,&m);
cnt=0;
root[n+1]=build(1,n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=n;i>=1;i--)
{
if(last[a[i]]==-1)
root[i]=update(1,n,i,1,root[i+1]);
else
{
root[i]=update(1,n,last[a[i]],-1,root[i+1]);
root[i]=update(1,n,i,1,root[i]);
}
last[a[i]]=i;
}
int l,r;
for(int i=1;i<=m;i++)
{
int tl,tr;
scanf("%d%d",&tl,&tr);
l=min((tl+an[i-1])%n+1,(tr+an[i-1])%n+1);
r=max((tl+an[i-1])%n+1,(tr+an[i-1])%n+1);
int k=query(1,n,l,r,root[l]);
int t=query2(1,n,(k+1)/2,root[l]);
an.push_back(t);
}
printf("Case #%d:",++cot);
for(int i=1;i<=m;i++)
printf(" %d",an[i]);
printf("n");
}
return 0;
}
3.树上两点间的查询:
例题:spoj-COT
题意:给定一个包含 (N) 个结点的树. 树节点从 (1) 到 (N) 编号。每个节点有一个整数权值。
(u;v;k) : 询问从节点 (u) 到节点 (v) 的路径上的第 (k) 小的权值。
【主席树+ (LCA)】
分析:
1.插入顺序按 (dfs) 遍历树的顺序进行,每个点在其父亲节点的基础上建树。
2.把路径上的点的信息整合起来放在一起。一开始想过用树链剖分,但会把路径分成很多的片段,不好处理。联系前缀和的思想,有
(num=tree[lson[u]]+tree[lson[v]]-tree[lca(u,v)]-tree[fa[lca(u,v)]])
代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const int mak=20;
vector<int>pic[N];
int a[N],id[N],root[N],fa[mak][N],depth[N],tn[N];
int tree[N*20],lson[N*20],rson[N*20],cnt;
int getid(int val,int len)
{
return lower_bound(id+1,id+1+len,val)-id;
}
int build(int l,int r)
{
int t=++cnt;
if(l==r)
{
tree[t]=0;
return t;
}
int mid=(l+r)>>1;
lson[t]=build(l,mid);
rson[t]=build(mid+1,r);
tree[t]=tree[lson[t]]+tree[rson[t]];
return t;
}
int update(int l,int r,int p,int rt)
{
int t=++cnt;
if(l==r)
{
tree[t]=tree[rt]+1;
return t;
}
int mid=(l+r)>>1;
if(p<=mid)
{
lson[t]=update(l,mid,p,lson[rt]);
rson[t]=rson[rt];
}
else
{
lson[t]=lson[rt];
rson[t]=update(mid+1,r,p,rson[rt]);
}
tree[t]=tree[lson[t]]+tree[rson[t]];
return t;
}
int query(int u,int v,int x,int y,int l,int r,int k)
{
if(l==r)
return l;
int mid=(l+r)>>1;
int num=tree[lson[u]]+tree[lson[v]]-tree[lson[x]]-tree[lson[y]];//前缀和
if(k<=num)
return query(lson[u],lson[v],lson[x],lson[y],l,mid,k);
else
return query(rson[u],rson[v],rson[x],rson[y],mid+1,r,k-num);
}
void dfs(int v,int p,int &t,int len,int d)
{
depth[v]=d;
fa[0][v]=p;
int pos=getid(a[v],len);
root[++t]=update(1,len,pos,root[tn[p]]);
tn[v]=t;
for(int i=0;i<pic[v].size();i++)
{
int u=pic[v][i];
if(u!=p)
dfs(u,v,t,len,d+1);
}
}
void init(int n,int len)
{
int cot=0;
tn[0]=0;
dfs(1,0,cot,len,0);
for(int k=0;k+1<mak;k++)
{
for(int i=1;i<=n;i++)
fa[k+1][i]=fa[k][fa[k][i]];
}
}
int lca(int u,int v)
{
if(depth[u]>depth[v])
swap(u,v);
for(int k=0;k<mak;k++)
{
if((depth[v]-depth[u])>>k&1)
v=fa[k][v];
}
if(u==v)
return u;
for(int k=mak-1;k>=0;k--)
{
if(fa[k][u]!=fa[k][v])
{
u=fa[k][u];
v=fa[k][v];
}
}
return fa[0][u];
}
int main()
{
int n,m,u,v,k;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
id[i]=a[i];
}
for(int i=1;i<n;i++)
{
scanf("%d%d",&u,&v);
pic[u].push_back(v);
pic[v].push_back(u);
}
sort(id+1,id+1+n);
int len=unique(id+1,id+1+n)-id-1;
cnt=0;
root[0]=build(1,len);
init(n,len);
while(m--)
{
scanf("%d%d%d",&u,&v,&k);
int par=lca(u,v);
printf("%dn",id[query(root[tn[u]],root[tn[v]],root[tn[par]],root[tn[fa[0][par]]],1,len,k)]);
}
return 0;
}
练习:
BZOJ 1146【带修改】
4.区间修改:
例题:To the moon HDU - 4348
题意:
一个长度为 (n) 的数组,(m) 个共(4) 种操作:
(1) (C;l;r;d):区间 ([l,r]) 中的数都 (+d),同时当前的时间戳 (+1);
(2) (Q;l;r):查询当前时间戳 (t) 区间 ([l,r]) 中所有数的和 ;
(3) (H;l;r;t):查询时间戳 (t) 区间 ([l,r]) 的和 ;
(4) (B;t):将当前时间戳置为 (t) ;
所有操作均合法,开始时间为 (0) 。
数据范围:(1leq n,m leq 10^5,|A_{[i]}| leq 10^9, 1leq l leq r leq n,|d| ≤ 10^4)
分析:
一般的线段树在区间修改时会把 (lazy) 标记下放,但此处不行,因为是动态开点,所以其孩子节点还没有开出。因此主席树的区间修改不需要把 (lazy) 标记下放,只需要保留在该节点即可。当我们修改时,新开的链的每个节点的值是根据其孩子节点的值向上合并得到的,但是有可能该节点所表示的区间被修改过,所以还要加上修改的部分。
即,修改时的区间合并:
tree[t]=tree[lson[t]]+tree[rson[t]]+1LL*(r-l+1)*lazy[t];
查询时,同样要考虑 (lazy) 标记,因为 (lazy) 标记没有下放,所以向下递归相应区间时求答案的时候记得加上,即 (ans) 的初值:
ll ans=lazy[rt]*1LL*(min(R,r)-max(l,L)+1);
另外建空树时 (lazy) 数组记得清空。
其它和普通线段树操作基本相同。
(d) 的数据类型开错了,找了一个下午 bug,难受。
模板:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
ll tree[N*40],lazy[N*40],a[N];
int lson[N*40],rson[N*40],root[N],cnt;
int build(int l,int r)
{
int t=++cnt;
lazy[t]=0;
tree[t]=0;
if(l==r)
{
tree[t]=a[l];
return t;
}
int mid=(l+r)>>1;
lson[t]=build(l,mid);
rson[t]=build(mid+1,r);
tree[t]=tree[lson[t]]+tree[rson[t]];
return t;
}
int update(int l,int r,int L,int R,ll val,int rt)
{
int t=++cnt;
//左右孩子先赋值,因为是区间修改
lson[t]=lson[rt],rson[t]=rson[rt],tree[t]=tree[rt],lazy[t]=lazy[rt];
if(L<=l&&r<=R)
{
tree[t]+=1LL*(r-l+1)*val;
lazy[t]+=val;
return t;
}
int mid=(l+r)>>1;
if(L<=mid)
lson[t]=update(l,mid,L,R,val,lson[rt]);
if(R>mid)
rson[t]=update(mid+1,r,L,R,val,rson[rt]);
tree[t]=tree[lson[t]]+tree[rson[t]]+1LL*(r-l+1)*lazy[t];//后面注意加上lazy标记
return t;
}
ll query(int l,int r,int L,int R,int rt)
{
if(L<=l&&r<=R)
return tree[rt];
ll ans=lazy[rt]*1LL*(min(R,r)-max(l,L)+1);//注意
int mid=(l+r)>>1;
if(L<=mid)
ans+=query(l,mid,L,R,lson[rt]);
if(R>mid)
ans+=query(mid+1,r,L,R,rson[rt]);
return ans;
}
int main()
{
int n,m,l,r,t;
ll d;
while(scanf("%d%d",&n,&m)!=EOF)
{
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
int ti=0;
cnt=0;
char ss[5];
root[0]=build(1,n);
while(m--)
{
scanf("%s",ss);
if(ss[0]=='C')
{
scanf("%d%d%lld",&l,&r,&d);
ti++;
root[ti]=update(1,n,l,r,d,root[ti-1]);
}
else if(ss[0]=='Q')
{
scanf("%d%d",&l,&r);
printf("%lldn",query(1,n,l,r,root[ti]));
}
else if(ss[0]=='H')
{
scanf("%d%d%d",&l,&r,&t);
printf("%lldn",query(1,n,l,r,root[t]));
}
else if(ss[0]=='B')
scanf("%d",&ti);
}
}
return 0;
}
相关习题:
hdu2665
bzoj2223
poj2104
poj2761
hdu4348
51Nod-1175
zoj2112
SPOJ COT2
SPOJ COT4
HYSBZ 3295
HYSBZ 3932
历年多校主席树题:
Level Up HDU - 5788
原文链接: https://www.cnblogs.com/1024-xzx/p/12395707.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;
也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬
![主席树总结](https://www.ccppcoding.com/wp-content/themes/justnews/themer/assets/images/lazy.png)
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/370926
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!