树状数组

下面是树状数组的学习笔记:


目录:

\(1\))什么是树状数组

\(2\))如何使用树状数组

\(3\))树状数组的应用


\(1\))什么是树状数组

​ 在解题过程中,有时候要维护一个数组的前缀\(S[i]=A[1]+A[2]+···+A[i]\)。但是如果我们修改了任何一个\(A[i]\)\(S[1]、S[2]、S[3]···S[i]\)的值都会相应改变。如果我们循环维护每一个\(S[i]\),那样就太慢了。于是我们要考虑使用数据结构来进行优化。树状数组就是这样一个数据结构!

​ 首先介绍一下前置知识:

根据任意正整数关于\(2\)的不重复次幂的唯一分解性质,若一个正整数\(x\)\(2\)进制表示为\(10101\),其中等于\(1\)的位是\(0,2,4\)则正整数可以被分解为\(2^4+2^2+2^0\)。那么,区间\([1,x]\)就可以被分为\(logx\)各小区间

​ 长度为\(2^4\)的区间为\([1,2^4]\);长度为\(2^2\)的区间为\([2^4+1,2^4+2^2]\);长度为\(2^0\)的区间为\([2^4+2^2+1,2^4+2^2+2^0]\)

​ 树状数组就是这样一种结构,它的基本用途就是维护序列的前缀和。对于区间\([1,x]\)树状数组将其分为\(logx\)个子区间,从而满足快速询问区间和。

​ 由上文可知,这些子区间的共同特点是:若区间结尾为\(R\)则区间长度就为\(R\)的“二进制分解”下最小的\(2\)的次幂,我们定义为\(lowbit(R)\)。对于给定的\(A\)序列,我们维护一个\(c\)数组,其实这个\(c\)数组就是我们所说的树状数组。\(c[i]\)中储存的是\(\sum A[i]\space (i\in [x-lowbit(x)+1,x])\)

​ 树状数组的存储结构如下图:

​ 值得注意的是,这样的树状结构满足以下的4个性质:

\(1\). 每个内部节点\(c[i]\)保存以它为根的子树中所有叶节点的和

\(2\). 每个内部节点\(c[i]\)的子节点个数为\(lowbit(i)\)的大小

\(3\). 除树根外,每个内部节点\(c[i]\)的父节点是\(c[i+lowbit(i)]\)

\(4\). 树的深度为\(O(logN)\)


(2)如何使用树状数组

​ 接下来就讲到实现方面了,树状数组的实现是与上面\(4\)个性质分不开的。

第一部分:求\(lowbit(n)\)

\(lowbit(n)\)表示去除非负整数\(n\)在二进制下最低位的\(1\)以及它后边的\(0\)构成的数值。于是\(lowbit(n)=n\)&\((-n)\),但是这是怎么来的呢,请听我细细道来(其实我也不知道

\(n>0\)\(n\)的第\(k\)位是1,其余都是\(0\)

为了实现\(lowbit\)运算,先把\(n\)取反,此时第\(k\) 位变为\(0\),其余都是\(1\)。再另\(n=n+1\),此时因为进位 ,第\(k\)位变为\(1\),其余都为\(0\)。在上面的取反加一操作后,\(n\)的第\(k+1\)到最高位恰好与原来相反,所以\(n\)&$n+1$仅有第$k$位为$1$,其余位都是$0$。而在补码表示下,\(n=-1-n\),因此,\(lowbit(n)=n\)&\((-n)\)

​ 下面是这一部分的代码实现:

int lowbit(int n)
{
	return n&(-n);//这句话水的不能再水了,不说了
}
第二部分:单点操作(就是对某个元素进行加法操作,下面以加法为例)

​ 树状数组支持单点操作,根据性质\(1、3\)。每一个内部节点\(c[i]\)保存以它为根的子树中所有叶节点的和,这就说明,我们一旦对$A[i] \(进行改动,就一定要对\)c[i]\(进行改动,并且将所有\)c[i]\(的父亲节点改动。那么如何找到\)c[i]\(的父亲节点呢?其实我们只需要将\)i\(更新为\)i+lowbit(i)$,就可以完成对父亲节点的遍历了。

​ 下面是这一部分的代码实现:

void update(int x,int y)//表示A[x]加y时维护c数组
{
	for(;x<=n;x+=lowbit(x))	c[x]+=y;
}
第三部分:查询前缀和

​ 树状数组支持单点修改和区间查询,下面介绍一下区间查询前缀和的方式:

​ 在这里我们所定义的前缀和,实际上就是\(\sum A[i] (i\in [1,x])\)。按照(\(1\))中所讲,应该将$[1,x] \(分成\)logn\(个小区间,每个小区间的区间和都已经保存在数组\)c$中。

​ 下面是这一部分的代码实现:(即\(O(logn)\)时间查询前缀和)

int sum(int x)//求1~x的前缀和
{
	int ans=0;//用ans存储前缀和
	for(;x;x-=lowbit(x))	ans+=c[i];//遍历子节点,累加ans
	return ans;//最后的答案就是ans
}
第四部分:统计\(A[x]···A[y]\)的值

​ 调用以上的\(sum\)函数,可以求出\(A[x]···A[y]\)得值就是\(sum(y)-sum(x-1)\)

第五部分:扩展(多维树状数组)

​ 由于处理每一个树状数组的复杂度是\(O(logn)\)。所以它可以扩充到\(m\)维,这样一来,处理树状数组的复杂度就提升到了\(O(log^mn)\),若\(m\)不大的时候,这个复杂度是可以被接受的。但是到底如何实现呢?

​ 其实,扩充树状数组的方式就是讲原来的一个循环改成\(m\)个循环,这一部分的代码如下:

void update(int x,int y,int z)//将(x,y)的值加上z
{
    int i=x;
    while(i<=n)//如果m更大,再多写几个while
    {
        int j=y;
        while(j<=m)
        {
            c[i][j]+=z;
            j+=lowbit(j);
        }
        i+=lowbit(i);
    }
}
int sum(int x,int y)
{
    int res=0,i=x;
    while(i>0)
    {
        int j=y;
        while(j>0)
        {
            res+=c[i][j];
            j-=lowbit(i);
        }
        i-=lowbit(i);
    }
    return res;
}
第六部分:注意事项

​ 要注意树状数组绝对不能出现下标为\(0\)的状况,因为啥啊,因为\(lowbit(0)=0\)啊!


\(3\))树状数组的应用

树状数组的应用有很多,下面重点将洛谷上两个模板题讲解一下

首先是\(P3374\) 【模板】树状数组 \(1\)

【题目描述】

如题,已知一个数列,你需要进行下面两种操作:

\(1\).将某一个数加上\(x\)

\(2\).求出某区间每一个数的和

直接上代码吧,就是彻彻底底的板子题

#include<cstdio>
#include<iostream>

using namespace std;

const int maxn=500010;
int n,m;
int a[maxn],c[maxn];
int h,q,k;//我没得设变量啊TwT

int lowbit(int x)//就是算lowbit(x)
{
    return x&(-x);
}

void update(int x,int y)//就是将y插入到x中
{
    for(;x<=n;x+=lowbit(x))
    {
        c[x]+=y;
    }
}

int sum(int x)//就是计算A[1]+A[2]+···+A[x]的值
{
    int ans=0;
    for(;x;x-=lowbit(x))
    {
        ans+=c[x];
    }
    return ans;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
    {
        scanf("%d",&a[i]);
        update(i,a[i]);
    }
    while(m)//循环读入数据
    {
        m--;
        scanf("%d",&h);
        if(h==1)//h是对类型的判断
        {
            scanf("%d%d",&q,&k);
            update(q,k);
        }
        if(h==2)
        {
            scanf("%d%d",&q,&k);
            printf("%d\n",sum(k)-sum(q-1));
        }
    }
    return 0;
}

上面的这道题是典型的树状数组“单点修改,区间查询题”。但是下面一道题就不是这样了!

\(P3368\) 【模板】树状数组 \(2\)

【题目描述】

如题,已知一个数列,你需要进行下面两种操作:

\(1\).将某区间每一个数数加上\(x\)

\(2\).求出某一个数的值

【题目分析】

​ 诶?这道题好像是“区间修改,单点查询”的题额,怎么做呢?我们引入一个叫做差分的概念:

假设\(A[]=\){\(1,5,3,6,4\)},\(B[]=\){\(1,4,-2,3,-2\)}

我们可以发现,如果规定\(A[0]=0\),那么可以满足\(B[i]=A[i]-A[i-1]\),并且\(A[i]=B[1]+B[2]+···+B[i]\)

如果我们在区间\([2,4]\)上加上\(2\),按照相同的规则进行处理,可以得到\(B[]=\){\(1,7,-2,3,\)},只有\(B[2]\)\(B[5]\)有改动

于是我们可以得到一个规律,即:对于\(A\),将区间\([l,r]\)中的每一个数都加上一个数\(x\)\(B[l]+=x;B[r+1]-=x\)

于是我们可以开一个树状数组维护差分值,每次只需要将\(l\)\(r+1\)进行操作,最后将需要查询的\(A\)值转化为\(B\)值求和就可以得到了!

​ 下面请食用代码:

#include<cstdio>
#include<iostream>

using namespace std;

const int maxn=500010;
int n,m;
int a[maxn],c[maxn];
int h,q,k,d;

int lowbit(int x)
{
	return x&(-x);
}

void update(int x,int y)
{
	for(;x<=n;x+=lowbit(x))
	{
		c[x]+=y;
	}
}

int sum(int x)
{
	int ans=0;
	for(;x;x-=lowbit(x))
	{
		ans+=c[x];
	}
	return ans;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
		scanf("%d",&a[i]);
	while(m)
	{
		m--;
		scanf("%d",&h);
		if(h==1)
		{
			scanf("%d%d%d",&q,&k,&d);
			update(q,d);
			update(k+1,-d);
		}
		if(h==2)
		{
			scanf("%d",&q);
			printf("%d\n",a[q]+sum(q));
		}
	}
	return 0;
}
/*
	相信大家已经发现了,这段代码与前一段没有什么太大的区别,其实就是将读入增加了一个,输出减少了一个罢了
*/

\(end\)

posted @ 2019-06-27 16:08  蒟蒻hqk  阅读(235)  评论(0编辑  收藏  举报