离散化,动态开点,历史版本,主席树p2086

  

 

  听名字非常的厉害了,它是线段树的高级应用.

  考虑如果你需要很多很多线段树的话,你需要开很多很多空间.但是如果这些线段树之间非常相似,我们可以把这一个线段树和上一个线段树共用大部分的节点.

  比如一个对与一个数组建立主席树,一个个的把元素放进线段树的话当然可以把重新做一个线段树,但每次只有logn个节点遭到更新或者新建,可以考虑如何压缩空间.

  嗯我只理解到了这里,以后继续更新

  先用三行离散化一下,处理出总的大小不同的数的数量sum

  

  o是原数组,a是用来离散化的.sort一下后a固然会变成有序的,但是unique这句话出来把a内从a[1]到a[n]值不同的数量赋值给sum外还会把这些数值不同的数挑一个放到前面,后面放的不知道是啥.这样子每次查找一个数是第几大的话就可以直接写 k=lower_bound(a+1,a+1+sum,要查的数)-a-1;

  然后先用tot++新建节点的方式新建一个树,没有要维护的信息,只是先把左右节点弄一下.设rt[]为最大的根节点的位置,lc[]为左端点,rc[]为右端点,ll[]表示管得到的最小的数(对应离散化之后的数),rr[]表示管得到的最小的数.这里举个栗子:

  对于n=15的这个数组,显然只有十个本质不同的树.拿他来建树的话就是这样的一个结构:

  (哎呀这个紫色也太好看了)

  而普通(正常)的线段树

  如果把树画出来其实可以发现是一个dfs序,这是和普通的线段树不一样的地方.虽然看起来少了一些点,但是它要多开两个数组记录儿子节点的位置,但普通线段树左右儿子一定是now*2与now*2+1所以不用记录,也不知道谁比较好.但是这个时候比没啥必要,人家主席树还要logn的空间记录以后的信息了.

   纪念一下调试代码:

int i;
int a[10000],n,m;
int tot,rt[100],lc[100],rc[100],ll[100],rr[100];
//rt=root lc左儿子 rc右耳子
void build(int &t,int l,int r){
    t=++tot;
    ll[t]=l;
    rr[t]=r;
    if(l==r){
        ll[t]=l;
        rr[t]=r;
        return ;
    }
    int mid=(l+r)/2;
    build(lc[t],l,mid);
    build(rc[t],mid+1,r);
    return ;
}
void put(int hh){
    if(hh/10)
        cout<<hh;
    else
        cout<<' '<<hh;
    cout<<' ' ;
    return ;
}
int main(){
    freopen("123.in","r",stdin);
    freopen("123.out","w",stdout);
    n=read();
    for(i=1;i<=n;i++)
        a[i]=read();
    sort(a+1,a+1+n);
    int sum=unique(a+1,a+1+n)-a-1;
    cout<<sum<<endl;
    build(rt[0],1,sum);
    cout<<"rt  ";
    for(i=0;i<=sum;i++)
        put(rt[i]);
    cout<<endl;
    cout<<"下标";
    for(i=0;i<=tot;i++)
        put(i);
    cout<<endl;
    cout<<"lc  ";
    for(i=0;i<=tot;i++)
        put(lc[i]);
    cout<<endl;
    cout<<"rc  ";
    for(i=0;i<=tot;i++)
        put(rc[i]);
    cout<<endl;
    cout<<"ll  ";
    for(i=0;i<=tot;i++)
        put(ll[i]);
    cout<<endl;
    cout<<"rr  ";
    for(i=0;i<=tot;i++)
        put(rr[i]);
    return 0;
}
调试

 

  然后考虑把各个数插入线段树并形成新的版本.和单点修改一样,它也只改变logn次即管自己的那几个节点,我们可以把新的节点记录一下,从新的根节点出发,一定先新建一个节点,把自己的管的范围内的数的数量sum++,如果插入的数归左儿子管就去更新左儿子,否则去更新右儿子.如何更新呢?这是一个递归的过程:把这个儿子复制成一个新节点,更新sum与左右儿子.   

  每个点被logn个点管着,时间复杂度是n*logn,每次新增logn个点,空间多了一倍而已.

  然后就是查询如何做了.查询区间第k大的本质仍是查询区间内区间数的数量,又是一个递归的过程.

  考虑区间[1,y]的[l,r]查询就与正常的线段树查询一样,只不过进去的时候由根节点=1变为rt[y].查询[x,y]的话就需要用一下小小容斥[1,y][l,r]-[1,x-1][l,r].然后怎么查第k大呢?考虑同时查看两个地方.整个的主席树等价于n个线段树,每个线段树维护的是只有[1,i]这个范围的数时区间[l,r]的sum.要查询根节点为x,y的第k大(这是两个线段树),对于当前的区间[l,r]可以求出sum[l,mid],如果它大于k说明第k小应该在区间[l,r]里,我们递归进入lc[x],lc[y]管这的子树中,他们管的区间是[l,mid],继续查找第k大;否则应该去rc[x],rc[y]子树中,查找[mid+1,r]中的k-sum[l,mid]大值.最后当l==r时返回下标,它是在离散化数组中的位置.

  

  

  最后给出总的代码.

int i,tx,ty,k;
int a[200010],o[200010];
int rt[200010],lc[200010<<5],rc[200010<<5],c[200010<<5],tot;
int n,m,sum;
void build(int &now,int l,int r){
    tot++;
    now=tot;
    if(l==r)
        return ;
    int mid=(l+r)>>1;
    build(lc[now],l,mid);
    build(rc[now],mid+1,r);
    return ;
}
int built(int now,int l,int r){
    int t=++tot;
    lc[t]=lc[now];rc[t]=rc[now];c[t]=c[now]+1;
    if(l==r)
        return t;
    int mid=(l+r)>>1;
    if(k<=mid)lc[t]=built(lc[t],l,mid);
    else      rc[t]=built(rc[t],mid+1,r);
    return t;
}
int ask(int x,int y,int l,int r,int k){
    if(l==r)
        return l;
    int mid=(l+r)>>1,tt=c[lc[y]]-c[lc[x]];
    if(tt>=k) return ask(lc[x],lc[y],l,mid,k);
    else      return ask(rc[x],rc[y],mid+1,r,k-tt);
}
int main(){
    n=read();m=read();
    for(i=1;i<=n;i++)
        a[i]=o[i]=read();
    sort(a+1,a+1+n);
    sum=unique(a+1,a+1+n)-a-1;
    build(rt[0],1,sum);
    for(i=1;i<=n;i++){
        k=lower_bound(a+1,a+1+sum,o[i])-a;
        rt[i]=built(rt[i-1],1,sum);
    }
    for(;m;m--){
        tx=read();ty=read();k=read();
        write(a[ask(rt[tx-1],rt[ty],1,sum,k)]);
        putchar(10);
    }
    return 0;
}
主席树
原文地址:https://www.cnblogs.com/qywyt/p/10206863.html