写在前面
相对于静态主席树,动态主席树意味着,每个节点可能会修改称为新的值,按照我们平时的想法,一定是在之前要删除的位置上进行-1操作,然后再新插入的位置上+1
解析
静态主席树我们在每个节点处创建一个权值线段树,然后在区间[l,r]求解时,使用第r棵线段树 - 第l-1棵线段树,在这个线段树上进行查询第k大元素的离散位置,然后根据之前进行离散的结果,得到最终的位置
使用ZOJ-2112作为栗子,数据为:3 2 1 4 7 6
这里要把所有的数据,包括在下面需要进行修改的数据,一次性全部列举出来,然后进行离散操作,因为主席树是一种离线结构,在起始要将所有数据都列举出来,为其开辟空间,然后再进行操作,这就是主席树的局限
那么离散化之后的数据为:3 2 1 4 6 5
同静态树状数组相同,使用数组T表示线段树的根节点
对于动态主席树,我们需要增加一些另外的线段树,这些线段树根节点假设用数组S表示,这些线段树只是用来维护修改节点的信息,那么我们要查询区间问题,怎么维护这样一个S数组呢?对于区间维护,树状数组是一个很好的选择
开始,我们的数组长度为5,那么我们就可以只维护S[1] ~S[5],因为区间查询和修改的范围就是这些
当出现修改时,如:C 2 6 (将第二个位置上的数字改为6) 开始a[2] = 2,对应离散后位置2,那么就要在到达2位置的整条路径上进行-1操作,6对应离散后位置为5,那么就要在到达5位置的整条路径上进行+1操作,同时使用树状数组维护,需要同时更新相对应的树
在进行求解的时候,数组T对应的,使用和静态主席树相同的方法,对于S数组,需要使用树状数组区间求和的思想
程序
#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
const int maxn = 6e4+5;
const int maxm = 1e4+5;
//S[i]表示树状数组维护的第i棵线段树的根节点的编号
int T[maxn],S[maxn],L[maxn*32],R[maxn*32],sum[maxn*32]; //T表示每个节点所产生的权值线段树 L某个节点的左儿子 R右儿子 sum节点的权值
int sz[maxn],h[maxn]; //h数组用来进行离散化操作
int ul[maxn],ur[maxn];
int tot,num,n,q; //tot为从0开始对于节点的编号
struct Query{
int l,r,k;
bool flag; //ture代表Q,false代表C
}Q[maxm]; //存储询问
void build(int& rt,int l,int r) //建空树
{
rt = ++tot;
sum[rt]=0;
if(l==r) return;
int mid = (l+r)>>1;
build(L[rt],l,mid);
build(R[rt],mid+1,r);
}
void update(int& rt,int pre,int l,int r,int x,int val) //x表示离散化后插入节点的位置
{
rt = ++tot;
L[rt] = L[pre];
R[rt] = R[pre];
sum[rt] = sum[pre]+val;
if(l==r) return;
int mid = (l+r)>>1;
if(x<=mid) update(L[rt],L[pre],l,mid,x,val); //更新L[rt]节点
else update(R[rt],R[pre],mid+1,r,x,val); //更新R[rt]节点
}
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int val)
{
int res = lower_bound(h+1,h+1+num,sz[x]) - h; //表示离散化之后的位置
while(x <= n)
{
update(S[x],S[x],1,num,res,val); //这里更新节点,不需要知道前一个节点数值,因为初始化时,每个S维护的线段树都是初始化的线段树
x += lowbit(x);
}
}
int Sum(int x,bool flag) //权值线段树维护的区间求和
{
int res=0;
while(x>0)
{
if(flag) res += sum[L[ur[x]]]; //当选择左子树时
else res += sum[L[ul[x]]];
x -= lowbit(x);
}
return res;
}
/*
s:查询区间开始前一个位置 e:查询区间结束位置
ts:s对应的跟节点位置 te:e对应跟节点位置
l、r:表示当前递归所在区间
k:查询区间的第k大元素
*/
int query(int s,int e,int ts,int te,int l,int r,int k)
{
if(l==r) return l;
int mid = (l+r)>>1;
int res = Sum(e,true)-Sum(s,false)+sum[L[te]]-sum[L[ts]];
if(k<=res)
{
for(int i=e;i;i-=lowbit(i)) ur[i] = L[ur[i]];
for(int i=s;i;i-=lowbit(i)) ul[i] = L[ul[i]];
return query(s,e,L[ts],L[te],l,mid,k);
}
else
{
for(int i=e;i;i-=lowbit(i)) ur[i] = R[ur[i]];
for(int i=s;i;i-=lowbit(i)) ul[i] = R[ul[i]];
return query(s,e,R[ts],R[te],mid+1,r,k-res);
}
}
int main()
{
//线段树为一种离线结构,必须预先知道数字的范围
int t;
scanf("%d",&t);
while(t--)
{
char str[5];
num=0;
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++) scanf("%d",sz+i),h[++num]=sz[i];
for(int i=1;i<=q;i++)
{
scanf("%s",str);
if(str[0]=='Q')
{
scanf("%d%d%d",&Q[i].l,&Q[i].r,&Q[i].k);
Q[i].flag=true;
}
else
{
scanf("%d%d",&Q[i].l,&Q[i].r);
Q[i].flag=false;
h[++num]=Q[i].r;
}
}
sort(h+1,h+1+num);
int tmp = unique(h+1,h+1+num)-h-1;
num = tmp; //离散化操作
tot=0;
build(T[0],1,num);
for(int i=1;i<=n;i++) update(T[i],T[i-1],1,num,lower_bound(h+1,h+1+num,sz[i])-h,1);
for(int i=1;i<=n;i++) S[i] = T[0]; //树状数组所维护的线段树开始都初始化为T[0]
for(int i=1;i<=q;i++)
{
if(Q[i].flag) //查询操作
{
//查询时,我们既要查询原数列的信息,又要查询修改信息,所以我们需要把树状数组需要查询的节点
//全部储存到一个数组中,在进行查询,原序列的查询方式和静态主席树一样,树状数组的查询方式和
//树状数组区间查询一样,只不过把每次访问节点改为这个节点代表的值域线段树,注意:每一次查询
//所有有关信息的访问必须同步
for(int j=Q[i].r;j;j-=lowbit(j)) ur[j] = S[j]; //将右边节点查询所需要的查询的节点全部放在ur数组中
for(int j=Q[i].l-1;j;j-=lowbit(j)) ul[j] = S[j];
printf("%d\n",h[query(Q[i].l-1,Q[i].r,T[Q[i].l-1],T[Q[i].r],1,num,Q[i].k)]);
}
else
{
add(Q[i].l,-1);
sz[Q[i].l] = Q[i].r; //将当前位置上的数字更新成后来的数值
add(Q[i].l,1); //需要再进行一次add操作
}
}
}
return 0;
}
参考博客
动态主席树:https://blog.csdn.net/WilliamSun0122/article/details/77885781
静态主席树:https://blog.csdn.net/li1615882553/article/details/80941342