主席树,是一种高级数据结构,是线段树的高级形式
主席树的全名应该叫做可持久化线段树
顾名思义,这种数据结构可以持久化,也即可查询历史记录
至于为什么要把这种数据结构叫做主席树,我就不多说了(我是不会告诉你我其实也不知道啊)
首先声明:本贴中的修改指单点修改
我们先来说说主席树的来源吧。
一开始的关于记录历史记录(也就是可持久化)的做法是:建多个线段树分别存储,然后直接找对应的线段树即可
但是这样有一个非常严重的问题:
MLE!!!
首先,建一棵线段树需要大约4n的空间,那么构造像上面的那种空间复杂度就是n^2
显然,对于n=10^5,操作数=10^5这样的级别,一棵线段树就需要大约4*10^5的空间,然后为了记录历史操作,又要建10^5棵像这样的线段树,妥妥的炸内存
后来通过研究发现,每次修改操作其实只修改了线段树上的一条链,而其他线段树结构并无改变(这个自行脑补,应该很好理解)
那么这么多没有修改的部分就这么被我们浪费掉了。。。(难怪会MLE呢)
那么问题来了,我们能够继承这些原来浪费的空间吗
答案是肯定的
我们可以在每次修改之后,把修改的那条链单独拉出来,新建节点,然后把原来的没有被修改到的节点对应地连到新的节点上去(对应是指儿子、父亲对应)
就像这样:
其中修改之后的8,9,10号节点分别对应修改之前的1,2,4号节点
我们来观察一下这棵很奇怪的树(应该是两棵连体树)
对于原树(root=1),形态本质上并没有发生改变
而对于新树(root=8),被修改的那条链发生改变,其他节点并没有发生改变
我们把两棵树分开来看一下:
这个东西的效果等同于建了两棵线段树,对吧
那么接下来的操作也和上面差不多,每次提取修改的那条链,新建节点,然后把原来的没有被修改到的节点对应地连到新的节点上去
这样空间复杂度就被完美地优化到nlogn啦
这就是主席树,实现了可持久化,并且不影响正常线段树查询操作(只不过关于左右儿子不能像原来一样直接n*2和n*2+1了,不过可以直接记录一下)
关于上面的修改代码如下:
inline void xg(int l,int r,int& nod,int la,int p){//把nod引用方便新建节点
nod=++cnt;t[nod]=新值;
lt[nod]=lt[la];rt[nod]=rt[la];//一开始先复制原来节点
if(l==r)return;
int mid=(l+r)>>1;
if(判定条件向左)xg(l,mid,lt[nod],lt[la],p);//这里左儿子被新建
else xg(mid+1,r,rt[nod],rt[la],p);//同理
}
查询大体同普通线段树,注意一下左右子树即可
接下来我们来说说区间第k值
这里以第k小为例
询问第k小这种问题可以线段树也可以平衡树。。。这里还是用线段树吧,时间都是nlogn(不要和我说直接排序。。。后面还有呢)
查找的方式是建一棵权值线段树,然后分别插入节点记录在节点权值范围内的数的个数
查询时比较k是否小于等于该节点的值,是则往左边走,否则往右边,直到走到底即为答案
那么如何解决带区间的问题呢?
我们可以建一棵主席树,分别记录1~i范围内所有节点的值(意义同上),i从0~n
那么l~r区间就可以表示成(1~r)-(1~l-1)区间
正确性显然
每次统计时减一下即可
只不过。。。这棵主席树不支持在中间对数进行修改
代表题:poj2104:http://poj.org/problem?id=2104
再离散化一下就好了
代码如下:
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<iostream>
using namespace std;
inline int read(){
int k=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){k=k*10+ch-'0';ch=getchar();}
return k*f;
}
int n,m,cnt=0,a[100001],b[100001];
int root[100001],t[2500001],lt[2500001],rt[2500001];
inline int erfen(int x,int sum){
int l=1,r=sum,ans;
while(l<=r){
int mid=(l+r)>>1;
if(b[mid]==x)return mid;
if(b[mid]>x)r=mid-1;
else l=mid+1;
}
return 0;
}
inline void build(int l,int r,int& nod){
nod=++cnt;t[nod]=0;
if(l==r)return;
int mid=(l+r)>>1;
build(l,mid,lt[nod]);
build(mid+1,r,rt[nod]);
}
inline void xg(int l,int r,int& nod,int la,int p){
nod=++cnt;t[nod]=t[la]+1;lt[nod]=lt[la];rt[nod]=rt[la];
if(l==r)return;
int mid=(l+r)>>1;
if(p<=mid)xg(l,mid,lt[nod],lt[la],p);
else xg(mid+1,r,rt[nod],rt[la],p);
}
inline int s(int l,int r,int x,int y,int k){
if(l==r)return l;
int mid=(l+r)>>1,cmp=t[lt[y]]-t[lt[x]];
if(k<=cmp)return s(l,mid,lt[x],lt[y],k);
else return s(mid+1,r,rt[x],rt[y],k-cmp);
}
int main()
{
n=read();m=read();int sum=n;
for(int i=1;i<=n;i++)a[i]=b[i]=read();
sort(b+1,b+n+1);
for(int i=1;i<=sum;i++)if(b[i]==b[i-1]){
sum--;for(int j=i;j<=sum;j++)b[j]=b[j+1];
}
build(1,sum,root[0]);
for(int i=1;i<=n;i++){
a[i]=erfen(a[i],sum);xg(1,sum,root[i],root[i-1],a[i]);
}
for(int i=1;i<=m;i++){
int x=read(),y=read(),z=read();
int k=s(1,sum,root[x-1],root[y],z);
printf("%d\n",b[k]);
}
return 0;
}
如果要在中间对数进行修改,怎么办呢?
我们直接对主席树进行操作是否可行呢?
答案是可行的
首先按照暴力的思想,每次修改时直接暴力在几乎每棵主席树上按链修改,时间肯定爆炸
只不过这个暴力是可以优化的
因为修改之后对后面都有影响,这个修改也类似于前缀和之类的东西
而这个东西其实是可以用树状数组来维护的
我们对于每个数在树状数组上都建立一棵线段树,然后类似于一维树状数组一样处理
修改时先把被修改点的前缀和全部减掉原来的点,再加上新点就好啦
像这样:
int t=erfen(a[A[i]],sum);//离散化下同
for(int j=A[i];j<=n;j+=lowbit(j))xg(1,sum,root[j],root[j],t,-1);//把这个点从树中删掉(那个-1)
a[A[i]]=B[i];//变成新点
t=erfen(B[i],sum);
for(int j=A[i];j<=n;j+=lowbit(j))xg(1,sum,root[j],root[j],t,1);//把新点加到树里面去
统计答案时和树状数组的统计方法差不多,也就是表示区间l~r,答案是sum(1~r)-sum(1~l-1)
这个东西代替原先的t[r]-t[l-1]作为比较,其他就和上面没有修改操作的主席树查询一样啦
代表题:BZOJ1901:http://www.lydsy.com/JudgeOnline/problem.php?id=1901
没有权限号的点这里:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=2112
离散化还是很鬼畜很毒瘤。。。
代码如下:
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<iostream>
using namespace std;
inline int lowbit(int x){return x&(-x);}
inline int read(){
int k=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){k=k*10+ch-'0';ch=getchar();}
return k*f;
}
bool K[100001]={0};int A[100001],B[100001],C[100001];
int n,m,cnt=0,a[100001],b[100001],x[1001],y[1001];
int root[100001],t[2500001],lt[2500001],rt[2500001];
inline int erfen(int x,int sum){
int l=1,r=sum,ans;
while(l<=r){
int mid=(l+r)>>1;
if(b[mid]==x)return mid;
if(b[mid]>x)r=mid-1;
else l=mid+1;
}
return 0;
}
inline void xg(int l,int r,int& nod,int la,int p,int x){
nod=++cnt;t[nod]=t[la]+x;lt[nod]=lt[la];rt[nod]=rt[la];
if(l==r)return;
int mid=(l+r)>>1;
if(p<=mid)xg(l,mid,lt[nod],lt[la],p,x);
else xg(mid+1,r,rt[nod],rt[la],p,x);
}
inline int s(int l,int r,int k){
if(l==r)return l;int t1=0,t2=0;
for(int i=1;i<=x[0];i++)t1+=t[lt[x[i]]];for(int i=1;i<=y[0];i++)t2+=t[lt[y[i]]];
int mid=(l+r)>>1,cmp=t2-t1;
if(k<=cmp){
for(int i=1;i<=x[0];i++)x[i]=lt[x[i]];for(int i=1;i<=y[0];i++)y[i]=lt[y[i]];
return s(l,mid,k);
}else{
for(int i=1;i<=x[0];i++)x[i]=rt[x[i]];for(int i=1;i<=y[0];i++)y[i]=rt[y[i]];
return s(mid+1,r,k-cmp);
}
}
int main()
{
n=read();m=read();int sum=n;
for(int i=1;i<=n;i++)a[i]=b[i]=read();
char c[5];
for(int i=1;i<=m;i++){
scanf("%s",c+1);A[i]=read();B[i]=read();
if(c[1]=='Q')C[i]=read(),K[i]=1;
else b[++sum]=B[i];
}
sort(b+1,b+sum+1);
for(int i=2;i<=sum;i++)if(b[i]==b[i-1]){
sum--;for(int j=i;j<=sum;j++)b[j]=b[j+1];
}
for(int i=1;i<=n;i++){
int t=erfen(a[i],sum);
for(int j=i;j<=n;j+=lowbit(j))xg(1,sum,root[j],root[j],t,1);
}
for(int i=1;i<=m;i++){
if(K[i]){
x[0]=y[0]=0;
for(int j=A[i]-1;j>0;j-=lowbit(j))x[++x[0]]=root[j];
for(int j=B[i];j>0;j-=lowbit(j))y[++y[0]]=root[j];
int k=s(1,sum,C[i]);
printf("%d\n",b[k]);
}else{
int t=erfen(a[A[i]],sum);
for(int j=A[i];j<=n;j+=lowbit(j))xg(1,sum,root[j],root[j],t,-1);
a[A[i]]=B[i];
t=erfen(B[i],sum);
for(int j=A[i];j<=n;j+=lowbit(j))xg(1,sum,root[j],root[j],t,1);
}
}
return 0;
}