【树状数组入门 自学笔记】

文章介绍了树状数组(也称线段树)的基本概念,用于解决数组的单点修改和区间查询问题。树状数组通过数组模拟树形结构,利用lowbit函数实现高效更新和查询,单次操作和查询的时间复杂度为O(logn)。文章还提供了区间查询和单点修改的代码实现,并给出了两个模板题示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

树状数组自学笔记

引入

考虑如下问题:
对于一个 n n n个元素的数组,我们需要对其进行 m m m次操作,操作有两种类型:
①将某数加上 k k k
②求出某个区间 [ l , r ] [l,r] [l,r]上内所有数字的和

数据范围: 1 < = n , k < = 5 × 1 0 5 1<=n,k<=5×10^5 1<=n,k<=5×105

考虑这题的做法:
①最朴素的暴力
对于操作1 直接修改元素 复杂度 O ( 1 ) O(1) O(1)
对于操作2 直接遍历所查区间 复杂度 O ( n ) O(n) O(n)

显然,若查询的次数较多会导致TLE
那为了实现 O ( 1 ) O(1) O(1)查询,我们自然而然想到了前缀和

②用前缀和

对于操作1 输出 s u m [ r ] − s u m [ l − 1 ] sum[r]-sum[l-1] sum[r]sum[l1] 复杂度 O ( 1 ) O(1) O(1)
然而对于操作2,某个元素的修改会对 s u m sum sum数组的值造成影响,在修改了某个元素后,我们需把 s u m sum sum数组中所修改元素后面的值也进行修改,故复杂度 O ( n ) O(n) O(n)

由此,我们发现了,这两种做法都有局限性,所以我们需要学习一种整体复杂度低的做法,也就是今天的主角——树状数组

树状数组

维基百科
简单来说:最朴素的树状数组支持单点修改(修改某一元素的值)和区间查询(查询某一区间的和),二者的时间复杂度都为 O ( l o g n ) O(logn) O(logn),当然此外还有很多拓展。

树状数组,当然不是建树,而是用数组来模拟树形结构。

在这里插入图片描述
设数组 a a a为原数组
对于数组 c c c我们可以写出如下的式子
c [ 1 ] = a [ 1 ] c[1]=a[1] c[1]=a[1]
c [ 2 ] = a [ 1 ] + a [ 2 ] c[2]=a[1]+a[2] c[2]=a[1]+a[2]
c [ 3 ] = a [ 3 ] c[3]=a[3] c[3]=a[3]
c [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] c[4]=a[1]+a[2]+a[3]+a[4] c[4]=a[1]+a[2]+a[3]+a[4]
c [ 5 ] = a [ 5 ] c[5]=a[5] c[5]=a[5]
c [ 6 ] = a [ 5 ] + a [ 6 ] c[6]=a[5]+a[6] c[6]=a[5]+a[6]
c [ 7 ] = a [ 7 ] c[7]=a[7] c[7]=a[7]
c [ 8 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] + a [ 8 ] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
. . . . . . ...... ......
可以看到,我们利用数组模拟出了树形的结构,这便是树状数组

lowbit函数

在讲解树状数组如何实现它的功能之前,我们先了解一个函数: l o w b i t ( x ) lowbit(x) lowbit(x)

l o w b i t ( x ) lowbit(x) lowbit(x)直译为中文意思是:低位。
我们定义它表示二进制表达式最低位的1所表示的数
比如: l o w b i t ( 10100 ) = ( 100 ) 2 = 4 lowbit(10100)=(100)_2=4 lowbit(10100)=(100)2=4
l o w b i t ( 101 ) = 1 lowbit(101)=1 lowbit(101)=1

那么对于一个数的 l o w b i t lowbit lowbit,我们应该怎么求出呢?
根据小学二年级 C语言课上所学,计算机通常以二进制补码的形式存储整数,正数的补码表示是其本身,负数的补码表示是该数取反加一。
然后我们发现,对于一个整数x,它和它的相反数的补码总是满足这样的性质:存在某一位,它和它的相反数的这一位都为1,它和它的相反数在这一位的低位的值均为0,而高位每一位都相反。
例如:
( + 101000 ) 补 = 0101000 (+101000)_补=0101000 (+101000)=0101000
( − 101000 ) 补 = 1011000 (-101000)_补=1011000 (101000)=1011000
( + 101000 ) & ( − 101000 ) = ( 0001000 ) (+101000)\&(-101000)=(0001000) (+101000)&(101000)=(0001000)

可以得出: l o w b i t ( x ) = x & − x lowbit(x)=x\&-x lowbit(x)=x&x

l o w b i t lowbit lowbit函数如下:

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

区间查询

首先考虑树状数组如何实现区间查询
在这里插入图片描述
在此图中,若需要查询前7个元素的和,即
s u m [ 7 ] = a [ 1 ] + a [ 2 ] + . . . + a [ 7 ] sum[7]=a[1]+a[2]+...+a[7] sum[7]=a[1]+a[2]+...+a[7]
事实上,我们可以写成
s u m [ 7 ] = ( a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] ) + ( a [ 5 ] + a [ 6 ] ) + ( a [ 7 ] ) sum[7]=(a[1]+a[2]+a[3]+a[4])+(a[5]+a[6])+(a[7]) sum[7]=(a[1]+a[2]+a[3]+a[4])+(a[5]+a[6])+(a[7]),即
s u m [ 7 ] = ( c [ 4 ] + c [ 6 ] + c [ 7 ] ) sum[7]=(c[4]+c[6]+c[7]) sum[7]=(c[4]+c[6]+c[7]),
在十进制下,我们可能发现不了什么规律,我们用二进制视角来看一下:
s u m [ ( 111 ) 2 ] = c [ ( 111 ) 2 ] + c [ ( 110 ) 2 ] + c [ ( 100 ) 2 ] sum[(111)_2]=c[(111)_2]+c[(110)_2]+c[(100)_2] sum[(111)2]=c[(111)2]+c[(110)2]+c[(100)2]
同样的我们可以写出:
s u m [ ( 110 ) 2 ] = c [ ( 110 ) 2 ] + c [ ( 100 ) 2 ] sum[(110)_2]=c[(110)_2]+c[(100)_2] sum[(110)2]=c[(110)2]+c[(100)2]
s u m [ ( 101 ) 2 ] = c [ ( 101 ) 2 ] + c [ ( 100 ) 2 ] sum[(101)_2]=c[(101)_2]+c[(100)_2] sum[(101)2]=c[(101)2]+c[(100)2]
s u m [ ( 1000 ) 2 ] = c [ ( 1000 ) 2 ] sum[(1000)_2]=c[(1000)_2] sum[(1000)2]=c[(1000)2]

显然 我们可以看出,求 s u m [ i ] sum[i] sum[i]的过程我们可以看作,每次加上当前数 c [ i ] c[i] c[i],并让 i i i去掉最低位的1,怎么实现去掉最低位的1?这就要用到刚刚所讲的 l o w b i t lowbit lowbit函数了,而去掉最低位的1的操作,即为让i减去 l o w b i t ( i ) lowbit(i) lowbit(i)

区间查询的代码如下:

int query(int i)
{
    int res=0;
    while(i>0)
    {
        res+=c[i];
        i-=lowbit(i);
    }
    return res;
}

这样我们可以求得区间 1 − i 1-i 1i的和,那对于区间 j − i j-i ji的和 ( 1 < = j < = i < = n ) (1<=j<=i<=n) (1<=j<=i<=n)
与前缀和的想法类似,只需要求出 q u e r y ( i ) − q u e r y ( j − 1 ) query(i)-query(j-1) query(i)query(j1)即可

单点修改

在树状数组修改某一个元素,虽不需要像前缀和思路一样,将后面的 s u m [ i ] sum[i] sum[i]的值全部修改,但也不是只需要修改一个点就可以了,需要把该节点和其所祖先节点更改。
还是看刚刚的图
在这里插入图片描述
c [ 1 ] = a [ 1 ] c[1]=a[1] c[1]=a[1]
c [ 2 ] = a [ 1 ] + a [ 2 ] c[2]=a[1]+a[2] c[2]=a[1]+a[2]
c [ 3 ] = a [ 3 ] c[3]=a[3] c[3]=a[3]
c [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] c[4]=a[1]+a[2]+a[3]+a[4] c[4]=a[1]+a[2]+a[3]+a[4]
c [ 5 ] = a [ 5 ] c[5]=a[5] c[5]=a[5]
c [ 6 ] = a [ 5 ] + a [ 6 ] c[6]=a[5]+a[6] c[6]=a[5]+a[6]
c [ 7 ] = a [ 7 ] c[7]=a[7] c[7]=a[7]
c [ 8 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] + a [ 8 ] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]

假如我们需要更改 a [ 3 ] a[3] a[3],那我们可以发现需要修改的是c[3],c[4],c[8]
假如我们需要更改 a [ 5 ] a[5] a[5],那我们可以发现需要修改的是c[5],c[6],c[8]
假如我们需要更改 a [ 7 ] a[7] a[7],那我们可以发现需要修改的是c[7],c[8]

还是一样的套路,我们写成二进制的形式来观察一下
a [ ( 11 ) 2 ] 对应 c [ ( 11 ) 2 ] , c [ ( 100 ) 2 ] , c [ ( 1000 ) 2 ] a[(11)_2]对应c[(11)_2],c[(100)_2],c[(1000)_2] a[(11)2]对应c[(11)2],c[(100)2],c[(1000)2]
a [ ( 101 ) 2 ] 对应 c [ ( 101 ) 2 ] , c [ ( 110 ) 2 ] , c [ ( 1000 ) 2 ] a[(101)_2]对应c[(101)_2],c[(110)_2],c[(1000)_2] a[(101)2]对应c[(101)2],c[(110)2],c[(1000)2]
a [ ( 111 ) 2 ] 对应 c [ ( 111 ) 2 ] , c [ ( 1000 ) 2 ] a[(111)_2]对应c[(111)_2],c[(1000)_2] a[(111)2]对应c[(111)2],c[(1000)2]

对于这三个 c c c数组的下标,我们分别 从小到大的看,可以发现,每次下标的增量都是对于当前的 i i i的一个 l o w b i t ( i ) lowbit(i) lowbit(i),直到增大到不超过 n n n的最大数
然后对于每个 c [ i ] c[i] c[i]都加上 k k k,即可实现单点的修改
代码:

void update(int i,int k)
{
    while(i<=n){
        c[i]+=k;
        i+=lowbit(i);
    }
}

模板题

1.【模板】树状数组 1

(不要忘记读入数据的时候就要用update函数来更新树状数组)

代码如下:

#include <bits/stdc++.h>
using namespace std;

int n,m;
int a[500100],c[500100];
int lowbit(int x)//
{
    return x&(-x);
}
void update(int i,int k)
{
    while(i<=n){
        c[i]+=k;
        i+=lowbit(i);
    }
}    
int query(int i)
{
    int res=0;
    while(i>0)
    {
        res+=c[i];
        i-=lowbit(i);
    }
    return res;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        update(i,a[i]);
    }
    while(m--)
    {
        int op,x,y;
        cin>>op>>x>>y;
        if(op==1)
        {
            update(x,y);
        }
        else {
             cout<<query(y)-query(x-1)<<endl;   
        }
    }
    return 0;
}

2.【模板】树状数组 2
此题让我们实现区间修改和单点查询,我们可以考虑读入数据时读入原数组的差分数组( a [ i ] − a [ i − 1 ] a[i]-a[i-1] a[i]a[i1]
然后对于区间修改操作,我们只需要修改端点的元素,在 l l l处加上 k k k,在 r + 1 r+1 r+1处减去 k k k,就实现了整个区间都加上了 k k k。而由于我们存储的是差分数组,所以我们在求和时求出的不是区间和了,而是单点处的值
代码如下:

#include <bits/stdc++.h>
using namespace std;

int n,m;
int a[500100],c[500100];
int lowbit(int x)
{
    return x&(-x);
}
void update(int i,int k)
{
    while(i<=n){
        c[i]+=k;
        i+=lowbit(i);
    }
}
int getsum(int i)
{
    int res=0;
    while(i>0)
    {
        res+=c[i];
        i-=lowbit(i);
    }
    return res;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        update(i,a[i]-a[i-1]);
    }
    while(m--)
    {
        int op,x,y;
        cin>>op;
        if(op==1)
        {   
            int k;
            cin>>x>>y>>k;
            update(x,k);
            update(y+1,-k);
        }
        else {
            cin>>x;
             cout<<getsum(x)<<endl;   
        }
    }
    return 0;
}

后记

树状数组有很多很多的应用,比如求逆序对等等,但由于本身能力有限,在这一篇博客中不再讲解,以后可能会有所补充

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值