树状数组套权值线段树

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\n",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('\n');
}

//普通树状数组求初始逆序对
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;
}

猜你喜欢

转载自www.cnblogs.com/TaylorSwift13/p/11228276.html