zoukankan      html  css  js  c++  java
  • 树状数组套权值线段树

    1.引言

    树状数组套线段树可以以(O(nlogn))的优秀复杂度维护带修改操作的区间K小值带修改操作的区间大于/小于K的值的个数的问题.

    一些人也把这种树套树的结构叫做树状数组套主席树.事实上,在这种树套树中,内层的每一颗线段树是独立的,并不是类似于可持久化线段树(广泛被接受的"主席树")那样的"互相依存"的线段树.但是由于"主席树"在(OI)界定义并不明确,有些语境下也可以把动态开点的线段树称为主席树.本文对于内层的树统一采用"线段树/动态开点线段树"的称呼.

    2.前置知识(都学到树套树了怎么可能不会)

    3.原理

    3.1从位置到值域

    在用数据结构维护一个数列时,我们通常会看到两种维护方式:

    3.1.1维护方式(1)

    以位置为下标,值为内容,比如最基础的线段树,当我们执行查询操作,比如查询[3,8],得到的是"原数列中第3个数到第8个数"的某些信息(和/最值)等.

    3.1.2维护方式(2)

    以值域为下标,值出现的次数为内容,比如用树状数组求逆序对,如果查询[3,8],得到的结果是值在[3,8]内的数的出现次数.

    我们把采用维护方式(2)的线段树叫做权值线段树,根据线段树自带的"二分"属性(每一个节点二分为左子节点和右子节点),我们可以用权值线段树来求解动态全局(K)小值的问题.

    3.2从前缀和到树状数组

    3.2.1问题(1)

    首先来思考一个很简单的问题:给你一个数列,不修改,多次询问区间和,怎么做?

    太简单了!前缀和搞一搞就可以了.

    具体来说,开一个长度为(n)的数组(记为(a)),(a_i)维护第(1)个数字到第(i)个数字的和,那么要查询([L,R])这个区间的和,只需要用(a_R)减去(a_{L-1})就可以了.

    3.2.2问题(2)

    再来一个问题:给你一个数列,不修改,多次询问区间第K小值,怎么做?

    没错!就是静态区间(K)小,主席树的模板题!

    太简单了!主席树搞一搞就可以了!

    这里就需要理解主席树求静态区间K小值的原理.其实就是前缀和的思想:开(n)权值线段树,第(i)颗维护第(1)个数字到第(i)个数字的值域的信息,那么要查询([L,R])这个区间的权值的(K)小值,只需要用第(R)颗权值线段树减去第(L-1)颗权值线段树,再按上文(3.1.2)的思路求([L,R])区间(K)小值.

    那开(n)颗权值线段树会爆空间怎么办?可持久化一下就好了.

    看不懂?回去复习静态区间(K)

    3.2.3问题(3)

    给你一个数列,边修改边询问,多次询问区间和,怎么做?

    太简单了!树状数组维护前缀和搞一搞就可以了.

    具体来说,开一个长度为(n)的数组(记为(c)),也就是树状数组的那个数组.如果要查询([1,i])前缀和,只需要把不多于(log_2i)(c)值加起来就可以了.修改时,也只需要修改不多于(log_2i)(c)值.复杂度(O(log_2n)).

    看不懂?回去复习树状数组

    3.2.4问题(4)

    给你一个数列,边修改边询问,多次询问区间第K小值,怎么做?

    没错!就是动态区间(K)小值的模板题!

    结合(3.2.2)(3.2.3)的思想,我们可以开(n)颗权值线段树,用树状数组维护(权值线段树相当于树状数组的节点).

    如果要查询区间([1,i])的值域的信息,只需要把不多于(log_2i)颗线段树加起来就可以了.那么,如果要查询区间([L,R]) 的值域信息,我们先(log_2i)颗线段树加起来([1,R])的信息,再把(log_2i)级颗线段树加起来([1,L-1])的信息,然后用([1,R])的信息减掉([1,L-1])的信息,像(3.2.2)那么求就可以了.(不知道怎么加或者怎么求?回去读(3.2.2)).

    修改时,也只需要修改不多于(log_2i)颗线段树.修改(1)颗线段树花费时间(O(log_2n)),那么(1)修改总时间就是(O(log_2^2n)).

    修改的复杂度好像对了,但是查询的相加那一步,累加(1)颗怎么着也得(O(n)),还要累加(log_2i)颗,单次复杂度达到了(O(nlogn)).怎么办?下一节我们再说.

    到这里,我们也解决了刚学树状数组套线段树的人(比如当时的我)很纠结的问题——内层的线段树存的是什么?

    我问你:树状数组的那个数组存的是什么?

    是不是一时语塞,只可意会不可言传?

    没错,这里的线段树存的东西就类似于那个数组存的东西.

    4.代码实现

    4.1离散化

    容易发现,我们的内层权值线段树是基于值域的,如果题目中值的范围过大,需要进行离散化.带修改操作时,需要把修改的值也输入进来,同初始权值一起离散化.

    4.2修改操作

    和普通动态开点线段树的修改一样.如果进入空节点则新建节点.选择进入修改左右子树之一.

    4.2.1示例代码
       //在内层线段树中
        void change(int &x,int L,int R,int Pos,int k)
        {
            if(x==0)x=++Tot;
            v[x]+=k;
            if(L==R)return;
            int Mid=(L+R)>>1;
            if(Pos<=Mid)change(LC[x],L,Mid,Pos,k);
            else change(RC[x],Mid+1,R,Pos,k);
        }
       //在外层树状数组中
    	void change(int p,int val,int v)
        {
            for(int i=p;i<=n;i+=i&-i)
                SegmentTree.change(SegmentTree.Root[i],1,n,val,v);
        }
    
    4.2.2注意

    值得注意的是,如果上面的分析看懂了的话,会发现外层的树状数组是以位置为下标的.这也是我们在外层树状数组修改时既需要传位置的下标(代码里的(p)),也要传(即内层线段树的下标,代码里的(val))的原因.

    4.3权值线段树相加的方法

    为了优化在(3.2.4)中提到的"把线段树加起来这一步复杂度过高的问题,我们有两种思路:

    4.3.1单独计算,累计答案

    有时候,题目所询问的并不是(相对的)排名,而是(绝对的)值,即动态询问区间大/小于(K)的值的个数,我们发现在求解这个问题时,每颗线段树是各自独立的.换句话说,我们不需要真的把这些线段树加起来,只需要在每个线段树中算出这(1)颗线段树的答案,再把这(logn)颗线段树的答案加起来就可以了.这样的话,每颗线段树查询(1)次是(O(logn))的,一共有(O(logn))颗线段树,单次查询的总复杂度就是(O(log_2^2n)).

    没看懂?我们借助图像来理解:

    原来的思路:

    单独计算,累计答案的思路:

    示例代码:

        //内层线段树
    	int query(int x,int L,int R,int X,int Y)
        {
            if(x==0||L>Y||R<X)return 0;
            if(L>=X&&R<=Y)return val[x];
            int Mid=(L+R)>>1;
            return query(LC[x],L,Mid,X,Y)+query(RC[x],Mid+1,R,X,Y);
        }
    	//外层树状数组
        int sum(int p,int Lx,int Rx)
        {
            int x=0;
            for(int i=p;i;i-=i&-i)x+=SegmentTree.query(SegmentTree.Root[i],1,n,Lx,Rx);
            return x;
        }
        int query(int L,int R,int Lx,int Rx)
        {
            if(Lx>Rx||L>R)return 0;
            return sum(R,Lx,Rx)-sum(L-1,Lx,Rx);
        }
    
    4.3.2记录节点,现算现用

    如果我们求的是排名呢?只有把所有的数据汇总到一起才能得到总排名,显然不能向上面那样对于每颗树单独算再相加.

    假设我们已经得到了这(logn)颗线段树的和,现在我们要利用这个和线段树来计算答案.

    容易发现,在每一个节点中,我们是进入左子节点还是右子节点,只与左子节点的大小与(K)的大小的关系有关(不知道为什么?回去看主席树求静态区间K小值),与树中其它任何节点都无关,这启发我们在要用到某个节点的数据的时候,再对这个节点求和.举个例子,现在我们在假想的和线段树中到了节点(u),需要通过(size[LC[u]])的大小来判断是进入左子树还是右子树,那么我们当场从那(logn)颗子树中揪出对应的(LC[u])这个节点,现场求和,并判断进入左子树还是右子树.

    如果可以在(O(1))时间内揪出,显然复杂度也是(O(log_2^2n))的.

    那么怎么个法呢?聪明的你一定可以想到,我们只需要在开始遍历这颗假想的和线段树之前,用一个数组存一下这(logn)颗线段树的根节点,即"应该揪出的节点的编号",然后每次进入左子树时,把"应该揪出的节点的编号"指向其左儿子,进入右子树则指向其右儿子.这样就可以保证(O(1))揪出了.

    没看懂?再来看图片解释:

    需要注意的是,我们现在求的是([L,R])区间,所以要进行现场加上(1...R)(logn)颗子树现场减去(1...L-1)(logn)颗子树两步操作.

    示例代码给出的是求区间(K)小值,显然我们可以把它延伸到求区间大于/小于(K)的值的个数的问题.

    示例代码

        //内层线段树
    	int Query(int L,int R,int K)
        {
            if(L==R)return L;
            int sum=0;
            for(int i=1;i<=C1;i++)sum-=v[LC[X[i]]];//现场减去1...L-1那logn颗子树
            for(int i=1;i<=C2;i++)sum+=v[LC[Y[i]]];//现场加上1...R那logn颗子树
     		if(K<=sum)//进入左子树
     		{
                for(int i=1;i<=C1;i++)X[i]=LC[X[i]];
                for(int i=1;i<=C2;i++)Y[i]=LC[Y[i]];
                return Query(L,Mid,K);	
            }
            else//进入右子树
     		{
                for(int i=1;i<=C1;i++)X[i]=RC[X[i]];
                for(int i=1;i<=C2;i++)Y[i]=RC[Y[i]];
                return Query(Mid+1,R,K-sum);				
            }
        } 
    	//外层树状数组
        int Query(int L,int R,int K)
        {
            //预处理需要查询哪log(n)颗主席树 
            C1=C2=0;
            for(int i=(L-1);i;i-=(i&-i))X[++C1]=SegmentTree.Root[i];
            for(int i=R;i;i-=(i&-i))Y[++C2]=SegmentTree.Root[i];
            //"现算现用"查询区间K大 
            return SegTree.Query(1,n,K);
        }
    
    4.3.3两种方法的比较

    显然,离散化之后,值和排名是相等的,所以两种方法某种程度上是可以互相交换的.

    5.例题

    5.1 LG2617 Dynamic Rankings

    带修改区间(K)小值模板题,按题意操作即可.示例代码采用了记录节点,现算现用的方法.

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    #define SIZE 200005 
    
    int n,m;
    int nx;
    int A[SIZE];//原数组 
    //int B[SIZE];//离散化之后的数组
    int Tem[SIZE];//离散化临时数组 
    int X[SIZE];//计算第[1...L-1]颗主席树的和 需要累加的主席树的编号
    int Y[SIZE];//计算第[1...R]颗主席树的和 需要累加的主席树的编号
    int C1;//计算第[1...L-1]颗主席树的和 需要累加的主席树的数量
    int C2;//计算第[1...R]颗主席树的和 需要累加的主席树的数量
    
    //离散化 
    void D()
    {
        //for(int i=1;i<=n;i++)Tem[i]=A[i]; 
        sort(Tem+1,Tem+1+nx);
        nx=unique(Tem+1,Tem+1+nx)-(Tem+1);
        //for(int i=1;i<=n;i++)B[i]=lower_bound(Tem+1,Tem+1+nx,A[i])-Tem;
    } 
    
    //内层: 动态开点权值线段树
    struct SegTreeX
    {
        int Tot,Root[SIZE*400],v[SIZE*400],LC[SIZE*400],RC[SIZE*400];
        #define Mid ((L+R)>>1)
        void Change(int &x,int L,int R,int Pos,int Val)
        {
            if(x==0)x=++Tot;
            v[x]+=Val;
            if(L==R)return;
            if(Pos<=Mid)Change(LC[x],L,Mid,Pos,Val);
            else Change(RC[x],Mid+1,R,Pos,Val);
        }
        int Query(int L,int R,int K)
        {
            if(L==R)return L;
            int sum=0;
            for(int i=1;i<=C1;i++)sum-=v[LC[X[i]]];
            for(int i=1;i<=C2;i++)sum+=v[LC[Y[i]]];
     		if(K<=sum)
     		{
                for(int i=1;i<=C1;i++)X[i]=LC[X[i]];
                for(int i=1;i<=C2;i++)Y[i]=LC[Y[i]];
                return Query(L,Mid,K);	
            }
            else
     		{
                for(int i=1;i<=C1;i++)X[i]=RC[X[i]];
                for(int i=1;i<=C2;i++)Y[i]=RC[Y[i]];
                return Query(Mid+1,R,K-sum);				
            }
        } 
    }SegTree;
    
    //外层树状数组 
    struct BITX
    {
        void Change(int Pos,int Val)
        {
            int k=lower_bound(Tem+1,Tem+1+nx,A[Pos])-Tem;//离散化之后的权值 也就是权值线段树里的下标
            for(int i=Pos;i<=n;i+=i&(-i))SegTree.Change(SegTree.Root[i],1,nx,k,Val);
        }
        int Query(int L,int R,int K)
        {
            //预处理需要查询哪log(n)颗主席树 
            C1=C2=0;
            for(int i=(L-1);i;i-=(i&-i))X[++C1]=SegTree.Root[i];
            for(int i=R;i;i-=(i&-i))Y[++C2]=SegTree.Root[i];
            //"现算现用"查询区间K大 
            return SegTree.Query(1,nx,K);
        }
    }BIT;
    
    struct qq
    {
        int opp,Lx,Rx,k;
    }q[SIZE];
    
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++){ scanf("%d",&A[i]); Tem[++nx]=A[i]; }
        char op[5];
        for(int i=1;i<=m;i++)
        {
            scanf("%s",op);
            if(op[0]=='Q'){ q[i].opp=1;	scanf("%d%d%d",&q[i].Lx,&q[i].Rx,&q[i].k); }
            else { scanf("%d%d",&q[i].Lx,&q[i].k); Tem[++nx]=q[i].k;}
        } 
        D();//离散化
        for(int i=1;i<=n;i++)BIT.Change(i,1);
        for(int i=1;i<=m;i++)
        {
            if(q[i].opp==1)
            {
                printf("%d
    ",Tem[BIT.Query(q[i].Lx,q[i].Rx,q[i].k)]);
            }
            else
            {
                BIT.Change(q[i].Lx,-1);
                A[q[i].Lx]=q[i].k;
                BIT.Change(q[i].Lx,1);
            }
        } 
        return 0;
    }
    

    5.2 [CQOI2011]动态逆序对

    带删除的区间大于/小于(K)的值的个数的问题,采用了单独计算,累计答案的方法.

    #include<cstdio>
    const int SIZE=100005;
    int n,m,A[SIZE],F[SIZE];
    long long ans;
    
    inline int In()
    {
        char ch=getchar();
        int x=0;
        while(ch<'0'||ch>'9')ch=getchar();
        while(ch>='0'&&ch<='9'){ x=x*10+ch-'0'; ch=getchar(); }
        return x;
    }
    
    char Tem[100];
    inline void Out(long long x)
    {
        int Len=0;
        while(x){ Tem[++Len]=x%10+'0'; x/=10; }
        while(Len)putchar(Tem[Len--]);
        putchar('
    ');
    }
    
    //普通树状数组求初始逆序对
    struct BIT
    {
        long long C[SIZE];
        inline void change(int p,long long v){for(register int i=p;i<=n;i+=i&-i)C[i]+=v;}
        inline long long query(int p){long long x=0;for(register int i=p;i;i-=i&-i)x+=C[i];return x;}	
    }T1;
    
    //内层线段树 
    struct Segtree
    {
        int LC[SIZE*400],RC[SIZE*400],v[SIZE*400],Root[SIZE],Tot;
        void change(int &x,int L,int R,int Pos,int k)
        {
            if(x==0)x=++Tot;
            v[x]+=k;
            if(L==R)return;
            int Mid=(L+R)>>1;
            if(Pos<=Mid)change(LC[x],L,Mid,Pos,k);
            else change(RC[x],Mid+1,R,Pos,k);
        }
        int query(int x,int L,int R,int X,int Y)
        {
            if(x==0||L>Y||R<X)return 0;
            if(L>=X&&R<=Y)return v[x];
            int Mid=(L+R)>>1;
            return query(LC[x],L,Mid,X,Y)+query(RC[x],Mid+1,R,X,Y);
        }
    }T2;
    
    //外层树状数组
    struct BITx
    {
        inline void change(int p,int val,int v){ for(register int i=p;i<=n;i+=i&-i)T2.change(T2.Root[i],1,n,val,v); }
        inline int sum(int p,int Lx,int Rx){ int x=0;for(register int i=p;i;i-=i&-i)x+=T2.query(T2.Root[i],1,n,Lx,Rx);return x;}
        inline int query(int L,int R,int Lx,int Rx)
        {
            if(Lx>Rx||L>R)return 0;
            return sum(R,Lx,Rx)-sum(L-1,Lx,Rx);
        }
    }T3;
    
    int main()
    {
        scanf("%d%d",&n,&m);
        for(register int i=1;i<=n;i++)
        {
            A[i]=In();
            ans+=T1.query(n)-T1.query(A[i]);
            T1.change(A[i],1);
            F[A[i]]=i;
        }
        for(register int i=1;i<=n;i++)T3.change(i,A[i],1);
        for(register int i=1;i<=m;i++)
        {
            Out(ans);
            int x=In();
            ans-=T3.query(1,F[x]-1,x+1,n);//在它前面又比它大
            ans-=T3.query(F[x]+1,n,1,x-1);//在它后面又比它小
            T3.change(F[x],x,-1); 
        }
        return 0;
    }
    
    
  • 相关阅读:
    LeetCode Path Sum II
    LeetCode Longest Palindromic Substring
    LeetCode Populating Next Right Pointers in Each Node II
    LeetCode Best Time to Buy and Sell Stock III
    LeetCode Binary Tree Maximum Path Sum
    LeetCode Find Peak Element
    LeetCode Maximum Product Subarray
    LeetCode Intersection of Two Linked Lists
    一天一个设计模式(1)——工厂模式
    PHP迭代器 Iterator
  • 原文地址:https://www.cnblogs.com/TaylorSwift13/p/11228276.html
Copyright © 2011-2022 走看看