『树状数组及其拓展运用』

<更新提示>

<第一次更新>


<正文>

树状数组

单点修改 区间和查询

众所周知,树状数组是一个可以维护区间前缀和的数据结构,普通的树状数组应该能够支持单点修改,区间查询的操作,其修改和查询的时间复杂度均为\(O(log_2n)\)

\(Code:\)

inline int lowbit(int x){return x&(-x);}
inline void modify(int pos,int x)
{
    for (;pos<=n;pos+=lowbit(pos))
        c[pos] += x;
}
inline int query(int pos)
{
    int res=0;
    for (;pos;pos-=lowbit(pos))
        res += c[pos];
    return res;
}

其基础部分我们不再讲解,接下来,我们将利用树状数组的简单变形来解决更多的问题。

区间修改 单点查询

其实,区间修改,单点查询也是可以使用树状数组实现的,我们考虑如下数组:

\[d_i=a_i-a_{i-1} \]

这其实是差分数组,即原序列相邻两项的差,那么由定义可以得到:

\[a_i=\sum_{j=1}^id_j \]

这是一个前缀和,可以用树状数组求得。

对于区间修改问题,我们就可以直接利用差分数组的性质,将\(c_l\)位置和\(c_{r+1}\)进行对应的正负修改,在前缀和中,得到的体现就是区间修改。

\(Code:\)

inline int lowbit(int x){return x&(-x);}
inline void modify(int pos,int x)
{
    for (;pos<=n;pos+=lowbit(pos))
        c[pos] += x;
}
inline int query(int pos)
{
    int res=0;
    for (;pos;pos-=lowbit(pos))
        res += c[pos];
    return res;
}
inline void solve(void)
{
    scanf("%d%d",&n,&m);
    int last=0,val;
    for (int i=1;i<=n;i++)
    {
        scanf("%d",&val);
        modify(i,val-last);
        last=val;
    }
    int op,x,y,k;
    for (int i=1;i<=m;i++)
    {
        scanf("%d",&op);
        if (op==1)
        {
            scanf("%d%d%d",&x,&y,&k);
            modify(x,k);modify(y+1,-k);
        }
        if (op==2)
        {
            scanf("%d",&k);
            printf("%d\n",query(k));
        }
    }
}

区间修改 区间和查询

对于该问题,我们同样需要引入差分数组\(d_i=a_i-a_{i-1}\)。但是,就这样还不足以完成区间和查询的操作,我们考虑展开求和式:

\[\sum_{i=1}^n a_i=\sum_{i=1}^n\ \sum_{j=1}^id_j \\ =\sum_{i=1}^n(d_1+d_2+...+d_i) \\=(d_1)+(d_1+d_2)+...+(d_1+d_2+...+d_n) \\ =\sum_{i=1}^n(n-i+1)d_i \\=n\sum_{i=1}^nd_i-\sum_{i=1}^n(i-1)d_i \]

\(d_i'=(i-1)d_i\),则

\[\sum_{i=1}^n a_i=n\sum_{i=1}^nd_i-\sum_{i=1}^nd_i' \]

用树状数组维护两个前缀和\(d\)\(d'\),就可以解决区间查询问题。对于区间修改,利用差分的方式对两个数组同时进行修改即可。

\(Code:\)

inline int lowbit(int x){return x&(-x);}
inline long long query(int index,int pos)
{
	long long res=0LL;
	if(index) {for(;pos;pos-=lowbit(pos))res+=d[pos];}
	else {for(;pos;pos-=lowbit(pos))res+=d_[pos];}
	return res;
} 
inline void modify(int index,int pos,long long delta)
{
	if(index) {for(;pos<=n;pos+=lowbit(pos))d[pos]+=delta;}
	else {for(;pos<=n;pos+=lowbit(pos))d_[pos]+=delta;}
}
inline void init(void)
{
	for(int i=1;i<=n;i++)
	{
		modify(1,i,a[i]-a[i-1]);
		modify(0,i,(i-1)*(a[i]-a[i-1]));
	}
}
inline void solve(void)
{
	for(int i=1;i<=m;i++)
	{
		char order;int l,r;long long delta;
		cin>>order; 
		if(order=='C')
		{
			scanf("%d%d%lld",&l,&r,&delta);
			modify(1,l,delta); modify(1,r+1,-delta);
			modify(0,l,(l-1)*delta); modify(0,r+1,r*(-delta));
		}
		if(order=='Q')
		{
			scanf("%d%d",&l,&r);
			printf("%lld\n",(r*query(1,r)-query(0,r))-((l-1)*query(1,l-1)-query(0,l-1)));
		}
	}
}

二维树状数组

对于一个二维矩阵内的部分和,其实直接利用树状数组进行简单拓展就可以实现了。

修改如下定义:\(c_{ij}\)代表以\((i,j)\)为右下角,长度为\(lowbit(i)\),高度为\(lowbit(j)\)的矩形中所有元素的和

然后和树状数组一样直接维护就可以了,修改,查询的时间复杂度均为\(O(log_2^2n)\)

当然,求部分和也要用到二维前缀和求部分和的公式。

\(Code:\)

inline int lowbit(int x){return x&(-x);}
inline void modify(int x,int y,int delta)
{
	for(int i=x;i<=n;i+=lowbit(i))
		for(int j=y;j<=n;j+=lowbit(j))
			c[i][j]+=delta;
}
inline int query(int x,int y)
{
	int res=0;
	for(int i=x;i;i-=lowbit(i))
		for(int j=y;j;j-=lowbit(j))
			res+=c[i][j];
	return res;
}

在二维树状数组的定义中,也可以拓展出区间修改,单点查询和区间修改,区间查询等内容,但由于其运用不多,实现价值不大,代码内容繁琐,故不详细讲解。

类树状数组

接下来,我们将讲解类树状数组,即利用树状数组的结构与思想来实现其他不同于前缀和的功能。

单点修改 区间最值

以维护区间最小值为例,还是先重新定义:\(c_i\)代表原序列区间\([i-lowbit_i+1,i]\)中元素的最小值

首先,我们需要能够根据原序列建立\(c\)数组,实现建树操作。

观察树状数组的基本结构图,我们发现对于求解\(c_i\),其树结构上的每一个子节点刚好包括了完整的\([1,i]\)区间。

又因为其子节点的\(c\)值都是已经求得的,所以,我们可以使用类似于递推的方法来更新\(c_i\)的值,更新一个节点的时间复杂度为\(O(log_2n)\),则建树的时间复杂度为\(O(nlog_2n)\)

\(Code:\)

inline void build(void)
{
    for (int i=1;i<=n;i++)
    {
        c[i] = a[i];
        int sec = lowbit(i);
        for (int j=1;j<sec;j*=2)
            c[i] = max( c[i] , c[i-j] );
    }
}

那么对于修改操作,其实我们套用树状数组的修改框架,利用相同的方式进行修改即可。时间复杂度为\(O(log_2^2n)\)

\(Code:\)

inline void modify(int pos,int x)
{
    a[pos] = x;
    for (;pos<=n;pos+=lowbit(pos))
    {
        c[pos] = a[pos];
        int sec = lowbit(pos);
        for (int j=1;j<sec;j*=2)
            c[pos] = max( c[pos] , c[pos-j] );
    }
}

对于区间\([l,r]\)的最小值查询,由于我们已经得到了\(c\)数组,所以直接利用\(lowbit\)函数向左缩小区间,不断使用\(c\)数组更新答案即可,时间复杂度为\(O(log_2(r-l+1))\)

\(Code:\)

inline int query(int l,int r)
{
    int res = a[r];
    while (true)
    {
        res = max( res , a[r] );
        if (l==r)break;r--;
        for (;r-l>=lowbit(r);r-=lowbit(r))
            res = max( res , c[r] );
    }
    return res;
}

简单平衡树

树状数组可以实现基础平衡树中的如下操作:

\(1.\)增加元素
\(2.\)删除元素
\(3.\)查询排名
\(4.\)查询第\(k\)小值
\(5.\)查询前驱
\(6.\)查询后继

我们在值域上建立一个树状数组\(c\)\(c_i\)代表值域区间\([i-lowbit_i+1,i]\)中元素的个数

由定义,我们就可以用树状数组的方法实现增加函数和删除函数了,其时间复杂度为\(O(log_2\max\{a_i\})\)

\(Code:\)

inline void insert(int val,int cnt)
{
    for (;val<=Uplim;val+=lowbit(val))
        c[val] += cnt;
}

对于查询值\(val\)的排名,我们可以用树状数组方便地统计出值域\([1,val-1]\)上元素的个数,进而得到元素\(val\)的排名,实际复杂度为\(O(log_2val)\)

\(Code:\)

inline int rank(int val)
{
    int res=1;val--;
    for (;val;val-=lowbit(val))
        res += c[val];
    return res;
}

对于查询排名为\(rank\)的数,我们需要使用倍增法解决。每一次我们使用\(2\)的整次方倍尝试扩展树状数组的值域下标,并累加元素个数,直到倍增的值恰巧符合要求即可。

\(Code:\)

inline int find(int rank)
{
    int res=0,cnt=0;
    for (int i=30;i>=0;i--)
    {
        res += (1<<i);
        if ( res>Uplim || cnt+c[res]>=rank )//避免有多个值相同的元素
            res -= (1<<i);
        else cnt += c[res];
    }
    return ++res;
}

对于查询前驱和后继,可以直接利用以上两个函数实现。不过,两个函数分别有一些增减细节:

\(1.\)对于查询一个值的前驱,只需要查询排名比他小\(1\)的数即可,所以要减\(1\)
\(2.\)对于查询一个数的后继,只需要查询比它大\(1\)的数它的排名即可,这样可以防止有多个相同的数造成查询到同一个数,所以要加\(1\)

\(Code:\)

inline int prep(int val)
{
    return find( rank(val)-1 );
}
inline int next(int val)
{
    return find( rank(val+1) );
}

总结

树状数组在维护前缀和方面有很好的表现,也能够拓展来维护最值,排名,第\(k\)大等。可以替代简单的线段树,平衡树,并且代码量极小,时间表现也不错,值得我们学习,可以在适时灵活运用。


<后记>

posted @ 2019-04-16 20:24  Parsnip  阅读(614)  评论(0编辑  收藏  举报