树状数组

转自:https://blog.youkuaiyun.com/yhf_2015/article/details/53844284

前言:
可能是因为学习了很多高级数据结构的缘故,突然感觉好像明白了树状数组,重新总结一下。
本文通过从根源深处挖掘树状数组所解决的问题,深刻的理解树状数组的操作本质,若要系统的研究树状数组,建议学习一下“二进制分解”“倍增”的概念。
考虑到初学者,文章写的比较长,废话比较多,还望耐心的看下去,相信你也能有新的收获。
温馨提示:文章中的代码仅供参考,虽然保证100%正确,但使用时请根据原题情况自行编写。


简介

树状数组是一种可以在O(log⁡n)’>O(logn)O(log⁡n)内完成单点数据修改的数据结构(以上两种为基本的操作)。
它的代码量小,常数较小,但是不支持求区间最值(可以使用线段树)。

原理

其实树状数组的核心思想就是倍增,从根本上来说,树状数组是ST算法的强化版。
ST表主要处理的区间不可加性的问题,而与之类似的树状数组可以求得区间可加性问题。

请暂时忽略网上所流传的一些树状数组的写法,我们先从另一个角度理解一下。

朴素的倍增算法

sum[i][j]’>sum[i][j]sum[i][j]区间的和。

预处理:

void init(){
    for(int i = 1; i <= n; i ++) sum[i][0] = v[i];
    for(int j = 1; (1<<j) <= n; j ++)
        for(int i = 1; i+(1<<j)-1 <= n; i ++)
            sum[i][j] = sum[i][j-1] + sum[i+(1<<(j-1))][j-1];
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

[l,r]’>[l,r][l,r]区间的和:

int que_sum(int l, int r){
    int res = 0;
    for(int i = MAXLOG; i >= 0; i --)
        if(l + (1<<i)-1 <= r) res += sum[l][i], l += (1<<i);
    return res;
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

让我们计算一下复杂度:
时间复杂度:O((n+q)log&#x2061;n)’>O((n+q)logn)O((n+q)log⁡n)
空间复杂度:O(nlog&#x2061;n)’>O(nlogn)O(nlog⁡n)

这种方法的log&#x2061;n’>lognlog⁡n常数比较大,而且空间复杂度有些大,还有就是因为预处理的原因不支持修改操作,我们考虑改进。

优化1:对二进制数位操作

上面的算法无论询问的区间大小是多少,都要从MAXLOG开始循环到0,但对于比较小的数,是完全没有必要的。

当查询区间[1,5]’>[1,5][1,5]时,其示意图如下图所示:
数轴
操作时,我们依此跳过上面的1,2,3,4’>1,2,3,41,2,3,4
也就是说15=23+22+21+20’>15=23+22+21+2015=23+22+21+20

所以我们可以二进制拆分以后正着循环,进行优化。

int que_sum(int l, int r){
    int res = 0, x = r-l+1;
    for(int i = 1; x; x >>= 1, i ++)
        if(x&1) res += sum[l][i], l += (1<<i);
    return res;
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这种写法类似与快速幂,原理还是倍增。

优化2:引入lowbit优化

考虑一个数129’>129129),在if(x&1)就会被pass掉。
这里介绍一种常数优化:lowbit(x)’>lowbit(x)lowbit(x)的。

int lowbit(int x){
    return (x)&(-x);
}
   
  • 1
  • 2
  • 3

这个函数的实现原理与计算机补码有关,这里不再介绍。
这里说一下功能,有兴趣的话可以点击此处阅读维基百科原文。打不开的话,这里摘录下来了一部分。

定义一个lowbit函数,返回参数转为二进制后,最后一个1的位置所代表的数值。
例如,lowbit(34)的返回值将是2;而Lowbit(12)返回4;Lowbit(8)返回8。
将34转为二进制(00100010)2’>(00100010)2(00100010)2位上的1。

也就是说,这个函数可以返回最小的2的幂次。
例如:
lowbit((10000001)2)=(1)2’>lowbit((10000001)2)=(1)2lowbit((10000001)2)=(1)2
lowbit((10001000)2)=(1000)2’>lowbit((10001000)2)=(1000)2lowbit((10001000)2)=(1000)2
所以我们用两次计算就可以得到129’>129129的二进制分解。

int que_sum(int l, int r){
    int res = 0, x = r-l+1;
    for( ; x; x -= lowbit(x)){
        int t = lowbit(x);
        int add = (int)(log(t)/log(2)+0.01);
        res += sum[l][add], l += t;
    }
    return res;
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我们姑且把里面的log()’>log()log()的,这样程序会得到一个常数优化。

优化3:成型的树状数组

那么优化2的瓶颈又在什么地方呢?
不难发现,难点在于如何让l不断的向r跳,这并不好处理。
因为不同位置的区间,可能要求l向后跳不同多的长度,但是如果我们处理的是[1,p]’>[1,p][1,p],从1开始,p的二进制分解就是区间长度的二进制分解。
分解完了以后,考虑从p向1的方向跳,这样每个点都只需要记录从[p,p&#x2212;lowbit(p)+1]’>[p,plowbit(p)+1][p,p−lowbit(p)+1],可以直接用一个一位数组进行储存。
每操作一次都会减去这个数2的最小次幂,使操作的规模不断缩小,执行下去就可以处理了。
而一个数的2的次幂最多有log&#x2061;n’>lognlog⁡n
比如:
15=(1111)2’>15=(1111)215=(1111)2,通过lowbit分解,它可以变成4个数的和:
(1111)2=(1)2+(10)2+(100)2+(1000)2’>(1111)2=(1)2+(10)2+(100)2+(1000)2(1111)2=(1)2+(10)2+(100)2+(1000)2,然后我们分析这个倒着跳的过程。
减去15的最小的2的幂次20’>2020得到14。
减去14的最小的2的幂次21’>2121得到12。
减去12的最小的2的幂次22’>2222得到8。
减去8的最小的2的幂次23’>2323得到0。
所以答案就是15,14,12,8这4个点上的信息之和。
之后,区间操作可以使用差分,对于一个区间[l,r]’>[l,r][l,r],然后做减法,就可以得到答案。
对于修改的操作,每次修改一个点,我们只要更新有覆盖这个点的信息段就好了,找到下一个覆盖数字x的信息段的方法是x+=lowbit(x)’>x+=lowbit(x)x+=lowbit(x),这样就可以把当前的最低位进位,那个数一定是覆盖修改点里面最小的,这样一直加到大于n就停止。
这个优化是树状数组对朴素倍增最根本的优化,因为二进制分解的唯一行,所以减少了维护的信息,使维护的信息支持修改,常数变的非常小。
构造的信息段

代码

int lowbit(int x){return x&(-x);}
int que_sum(int x){
    int sum = 0;
    for( ; x > 0; x -= lowbit(x)) sum += val[x];
    return sum;
}
void update(int x, int k){for( ; x <= n; x += lowbit(x)) val[x] += k;}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我的理解方式可能与大多数人不太一样,但是用这样的方式可以很好的体会树状数组的来源,深层度理解倍增算法,还希望对大家有帮助。
最后推荐一些博客:
int64Ago的专栏——搞懂树状数组
N3verL4nd——树状数组学习笔记

        <link href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css" rel="stylesheet">
            </div>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值