zoukankan      html  css  js  c++  java
  • 主席树总结

    定义:
      **可持久化线段树**是一种可持久化数据结构,也被称为主席树。这种数据结构在普通线段树的基础之上支持**查询某个历史版本**,同时时间复杂度与线段树是同级,**空间复杂度相较而言更高**。   与大部分可持久化数据结构类似,**可持久化线段树尽可能多地共用先前某一个版本的结点,从而节省大量的空间与时间**。
    思路(以求区间第 (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("%d
    ",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("%d
    ",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("
    ");
        }
        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("%d
    ",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("%lld
    ",query(1,n,l,r,root[ti]));
                }
                else if(ss[0]=='H')
                {
                    scanf("%d%d%d",&l,&r,&t);
                    printf("%lld
    ",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

  • 相关阅读:
    Atitit.数据库存储引擎的原理与attilax 总结
    Atitit.数据库存储引擎的原理与attilax 总结
    atitit.guice3 绑定方式打总结生成非单例对象toInstance toProvider区别 v2 pb29
    atitit.guice3 绑定方式打总结生成非单例对象toInstance toProvider区别 v2 pb29
    atitit.项目设计模式---ioc attilax总结v4 q11
    atitit.项目设计模式---ioc attilax总结v4 q11
    Atitit.attilax的 case list 项目经验 案例列表
    Atitit.attilax的 case list 项目经验 案例列表
    Atitit.python web环境的配置 attilax 总结
    Atitit.python web环境的配置 attilax 总结
  • 原文地址:https://www.cnblogs.com/1024-xzx/p/12395707.html
Copyright © 2011-2022 走看看