树状数组

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


目录:

(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 (iin [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] (iin [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
",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
",a[q]+sum(q));
		}
	}
	return 0;
}
/*
	相信大家已经发现了,这段代码与前一段没有什么太大的区别,其实就是将读入增加了一个,输出减少了一个罢了
*/

(end)

原文地址:https://www.cnblogs.com/juruohqk/p/11097678.html