主席树
荣一鸣
2018-10-09 10:12:45
## PART 1 静态主席树
主席树,作为一种线段树的变种,在用做求区间第k小值是十分有用
那么再说主席树之前,我们先思考一下区间第k小值怎样求。首先,我们将所有的数进行离散化,然后,我们对于每一个[1,i]的区间进行建树,见得的树内部的节点拥有三个值,左孩子,右孩子,以及该节点包括的区间l,r中原序列编号小于等于i且大小大于等于l,小于等于r的个数
然后就是可以前缀和了。我们将询问区间的[l,r]分为[1,r]-[1,l-1],这可以看成前缀和,但是怎么前缀和呢?就是将线段树中同一个节点的值相减
举个栗子,某个节点表示[1,i]的区间中大小为[l,r]中的个数,另一个节点表示[1,j]的区间中大小为[l,r]的个数,则两个个数相减,得到的就是在[j+1,i]中,大小在[l,r]中的个数
那么,我们求第k小就是比它小的个数为k-1的数就行了,那么我们就可以写出查询程序
```cpp
int que(int i,int j,int k,int l,int r){
int d=tree[tree[i].lson].val-tree[tree[j].lson].val;//大小从l到mid的数量
if(l==r) return l;
int mid=(l+r)>>1;
if(k<=d) return que(tree[i].lson,tree[j].lson,k,l,mid);//如果数量大于查询的k
else return que(tree[i].rson,tree[j].rson,k-d,mid+1,r);//注意新的k要把之前小于它的d个数的数量减去
}
```
首先,我们求出的d值就是在j-1到i中大小为l到mid的数量
如果这个数量比我们的k要小,那么我们要求的数就比mid要大,那么就要在右孩子里找,但是在右孩子里找的时候找的大小要从k减小到k-d,因为你在查找[l,r]里的第k小就会变成[mid+1,r]的第k-d小,相当于把[l,mid]的数都减去了。
直到查询到l==r才行
那么,我们就把该题的查询做完了,但是想一想,如果真的对于每一个区间[1,i]都做一颗线段树的话会空间太大,那么我们怎么才能节省空间?
我们来看一下这两课线段树,区间[1,i]与[1,i+1]的线段树就只有新加进来的数i+1是需要更新的
比如说我们新插进来的数的大小小于mid,那么对于这两课线段树,只有[1,mid]中不一样,其他地方都是一样的,所以我们就在需要新加点的时候加点,其他地方就沿用之前的点,就可以省空间了
```cpp
void update(int u,int &rt,int l,int r){
tree[cnt++]=tree[rt];//新插入的点的位置
rt=cnt-1;//因为沿用的是地址,所以直接把上一层的左孩子或右孩子编号换掉了
tree[rt].val++;//值得变化
if(l==r) return;
int mid=(l+r)>>1;
if(u<=mid) update(u,tree[rt].lson,l,mid);//继续向下更新
else update(u,tree[rt].rson,mid+1,r);//向下更新
}
```
然后注意一点,我们每个数的根节点的编号也要记录,下面是更新的外面的函数
```cpp
for(int i=1;i<=n;i++)
root[i]=root[i-1],update(rank[i],root[i],1,n);
```
注意刚进来的时候的rt不是左孩子或右孩子,而是根节点编号
那么下面放上完整程序
```cpp
#include<bits/stdc++.h>
using namespace std;
int n,m,cnt=1,rank[200010],root[200010];
struct treepoint{
int lson,rson,val;
}tree[4000010];
struct node{
int id,vl;
bool operator < (const node b)const{
return vl<b.vl;
}
}p[200010];
void update(int u,int &rt,int l,int r){
tree[cnt++]=tree[rt];
rt=cnt-1;
tree[rt].val++;
if(l==r) return;
int mid=(l+r)>>1;
if(u<=mid) update(u,tree[rt].lson,l,mid);
else update(u,tree[rt].rson,mid+1,r);
}
int que(int i,int j,int k,int l,int r){
int d=tree[tree[i].lson].val-tree[tree[j].lson].val;
if(l==r) return l;
int mid=(l+r)>>1;
if(k<=d) return que(tree[i].lson,tree[j].lson,k,l,mid);
else return que(tree[i].rson,tree[j].rson,k-d,mid+1,r);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&p[i].vl),p[i].id=i;
sort(p+1,p+n+1);
for(int i=1;i<=n;i++) rank[p[i].id]=i;
for(int i=1;i<=n;i++) root[i]=root[i-1],update(rank[i],root[i],1,n);
for(int i=1;i<=m;i++){
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",p[que(root[r],root[l-1],k,1,n)].vl);
}
}
```
## PART 2 动态主席树
相比于静态主席树的只能查询不能修改的情况,我们选择用动态主席树来进行单点修改与区间查询
由于直接修改的话修改的大小就是树的个数乘上树的深度,即O(nlog(n))的时间复杂的,肯定爆炸
什么?单点修改区间查询??。。。~~怎么感觉就是线段树呢?~~
然而我们仔细看发信区间第k小,由于k不同就必须套上主席树
我们把每个主席树放进来,这个单点修改区间查询就变成了区间修改单点查询(修改从修改点到n的所有主席树,查询单个主席树上的值)
emmmm,线段树套主席树???不太可能
由于线段树的将O(n)优化为O(log(n))靠的是lazy_tag而我们这里怎么lazy_tag呢?本人想不到,只能选择树状数组
那么我们考虑树状数组,树状数组的期间修改单点查询靠的是差分,那么我们就尝试把它套进主席树内
我们将每一个树状数组的点换成一颗主席树,可能这个比较抽象,我们可以将一棵树先看成一个点,然后如果你加进来一个点,它的位置是i,那么从i到n的所有主席树上都要加进这个点,那么我们就进行差分,在i处加上这个点,在n+1处减去这个点,由于n+1超过了数的范围,怎么都查不到~~(手动滑稽)~~,我们就不用管它了
#### 建树
那么我们就从i到n每次加上lowbit(i)为根的主席树上加入这个点(操作同树状数组)
然后我们的插入的点就从nlog(n)变为了log(n)^2
```
void modify(int &rtt,int l,int r,int pos,int val){
if(!rtt) rtt=++tot;
tree[rtt].val+=val;
if(l==r) return;
int mid=(l+r)>>1;
if(pos<=mid) modify(tree[rtt].lson,l,mid,pos,val);
else modify(tree[rtt].rson,mid+1,r,pos,val);
}
void prepare_modify(int x,int k,int val){
for(int i=x;i<=n;i+=i&-i) modify(rt[i],1,len,k,val);
}
```
主函数中的程序
```
for(int i=1;i<=n;i++) prepare_modify(i,rank[i],1);
```
#### 查询
然后我们看一看查询,同树状数组,我们的查询也要加起来,从i到0,每次减去lowbit(i)为根的主席树都要加起来,然后在做一般的主席树查询(可以理解为从把每一颗主席树差分为多颗主席树,现在要把它合起来,而树状数组的枚举就是把这颗主席树分到了那些树里)
我们就需要多一个数组来存都用到了那几个主席树
```
int query(int l,int r,int k){
if(l==r) return l;
int mid=(l+r)>>1,sum=0;
for(int i=1;i<=cnt[1];i++) sum+=tree[tree[tmp[1][i]].lson].val;
for(int i=1;i<=cnt[0];i++) sum-=tree[tree[tmp[0][i]].lson].val;
if(k<=sum){
for(int i=1;i<=cnt[1];i++) tmp[1][i]=tree[tmp[1][i]].lson;
for(int i=1;i<=cnt[0];i++) tmp[0][i]=tree[tmp[0][i]].lson;
return query(l,mid,k);
}
else{
for(int i=1;i<=cnt[1];i++) tmp[1][i]=tree[tmp[1][i]].rson;
for(int i=1;i<=cnt[0];i++) tmp[0][i]=tree[tmp[0][i]].rson;
return query(mid+1,r,k-sum);
}
}
int prepare_query(int l,int r,int k){
memset(tmp,0,sizeof(tmp));
cnt[0]=cnt[1]=0;
for(int i=l-1;i;i-=-i&i) tmp[0][++cnt[0]]=rt[i];
for(int i=r;i;i-=-i&i) tmp[1][++cnt[1]]=rt[i];//树状数组求出差分进了那几个主席树里
return query(1,len,k);
}
```
上面的tmp数组存了那些差分后的主席树需要查询,然后就求和相减,同时走向左子树或右子树时要将每一个主席树都走过去,也就是循环然后在走向下一层
主函数
```
printf("%d\n",o[prepare_query(q[i].l,q[i].r,q[i].k)].vl)
```
#### 修改
修改其实就是将每一个原来的点减去再把新的点加进来,而减去其实就是把插入一个参数改为负数就行了
直接放主函数,其他的上面有
```
prepare_modify(q[i].pos,rank[q[i].pos],-1);
rank[q[i].pos]=ppos[i];
prepare_modify(q[i].pos,ppos[i],1);
```
#### 离散化
其实我打的离散化比较烦,其实完全可以先全部离散化在二分查大小
那么我这里就放一下离散化程序,只可意会,不可言传,注意修改用的值也要加进来离散化
```
for(int i=1;i<=n;i++) scanf("%d",&o[++len].vl),o[len].id=i;
for(int i=1;i<=m;i++){
opt=getchar();
while(opt!='Q'&&opt!='C') opt=getchar();
q[i].b=(opt=='Q');
if(q[i].b) scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].k);
else scanf("%d%d",&q[i].pos,&q[i].t),o[++len].vl=q[i].t,o[len].id=0,o[len].pos=i;
}
sort(o+1,o+len+1);
o[0].vl=0;
o[0].nrank=0;//新的编号
for(int i=1;i<=len;i++){
o[i].nrank=o[i-1].nrank+1;
if(o[i].id) rank[o[i].id]=o[i].nrank;
//如果是原数组就将它的新编号放入rank,
//即rank[i]为原序列的第i个数的大小
else ppos[o[i].pos]=o[i].nrank;
//如果是修改的就放入ppos中
}
```
放一个总的程序
```
#include<bits/stdc++.h>
using namespace std;
const int MAX=100005;
struct treepoint{
int lson,rson,val;
}tree[MAX*400];
struct operation{
bool b;
int l,r,k,pos,t;
}q[MAX];
struct node{
int vl,id,nrank,pos;
bool operator < (const node b)const{
return vl<b.vl;
}
}o[MAX<<1];
int n,m,rank[MAX],rt[MAX],len,tot,tmp[2][20],cnt[2],ppos[MAX];
char opt;
void modify(int &rtt,int l,int r,int pos,int val){
if(!rtt) rtt=++tot;
tree[rtt].val+=val;
if(l==r) return;
int mid=(l+r)>>1;
if(pos<=mid) modify(tree[rtt].lson,l,mid,pos,val);
else modify(tree[rtt].rson,mid+1,r,pos,val);
}
void prepare_modify(int x,int k,int val){
for(int i=x;i<=n;i+=i&-i) modify(rt[i],1,len,k,val);
}
int query(int l,int r,int k){
if(l==r) return l;
int mid=(l+r)>>1,sum=0;
for(int i=1;i<=cnt[1];i++) sum+=tree[tree[tmp[1][i]].lson].val;
for(int i=1;i<=cnt[0];i++) sum-=tree[tree[tmp[0][i]].lson].val;
if(k<=sum){
for(int i=1;i<=cnt[1];i++) tmp[1][i]=tree[tmp[1][i]].lson;
for(int i=1;i<=cnt[0];i++) tmp[0][i]=tree[tmp[0][i]].lson;
return query(l,mid,k);
}
else{
for(int i=1;i<=cnt[1];i++) tmp[1][i]=tree[tmp[1][i]].rson;
for(int i=1;i<=cnt[0];i++) tmp[0][i]=tree[tmp[0][i]].rson;
return query(mid+1,r,k-sum);
}
}
int prepare_query(int l,int r,int k){
memset(tmp,0,sizeof(tmp));
cnt[0]=cnt[1]=0;
for(int i=l-1;i;i-=-i&i) tmp[0][++cnt[0]]=rt[i];
for(int i=r;i;i-=-i&i) tmp[1][++cnt[1]]=rt[i];
return query(1,len,k);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&o[++len].vl),o[len].id=i;
for(int i=1;i<=m;i++){
opt=getchar();
while(opt!='Q'&&opt!='C') opt=getchar();
q[i].b=(opt=='Q');
if(q[i].b) scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].k);
else scanf("%d%d",&q[i].pos,&q[i].t),o[++len].vl=q[i].t,o[len].id=0,o[len].pos=i;
}
sort(o+1,o+len+1);
o[0].vl=0;
o[0].nrank=0;
for(int i=1;i<=len;i++){
o[i].nrank=o[i-1].nrank+1;
if(o[i].id) rank[o[i].id]=o[i].nrank;
else ppos[o[i].pos]=o[i].nrank;
}
for(int i=1;i<=n;i++) prepare_modify(i,rank[i],1);
for(int i=1;i<=m;i++){
if(q[i].b) printf("%d\n",o[prepare_query(q[i].l,q[i].r,q[i].k)].vl);
else{
prepare_modify(q[i].pos,rank[q[i].pos],-1);
rank[q[i].pos]=ppos[i];
prepare_modify(q[i].pos,ppos[i],1);
}
}
}
```