树状数组详解

lmrttx

2020-11-23 13:26:23

Personal

前:我写的线段树~~深受好评~~。于是想写树状数组。持续更新。 树状数组与二进制位有关。和线段树有些相像之处,都是把序列转化成一棵树进行操作。但是有区别。 update 2021.1.3:发现咕了接近2个月,决定写写 ### part 1 :基本思想 ##### 1 二进制分解 如果1个数 $a$ 的二进制表示中,为1的为分别为: $$a_{k,1},a_{k,2},a_{k,3}...a_{k,cnt}$$ 则这个数= $$2^{k,1}+2^{k,2}...+2^{k,cnt}$$ 这就是二进制分解了。 ##### 2 树的由来 我们假设 $k_1>k_2>k_3...>k_{cnt}$,则区间 $[1,n]$ 可以被分成 $O(log n)$ 个小区间。 它们分别为: $$[1,2^{k_1}],[2^{k_1}+1,2^{k_1}+2^{k_2}]...$$ 于是有了一棵树。 ##### 3 小区间特点 如果区间结尾为 $r$,则区间长度就等于 $r$ 的二进制分解中最小的2的次幂。我们用函数 $lowbit$来求区间长度。 ```cpp int lowbit(int x){return x & -x;} ``` 有了这个函数,我们就可以用类似标记的**整块修改**来**维护前缀和**啦!!! ##### 4 树的性质 我们用数组 $sum[x]$ 来保存序列 $a$ 的区间 $[x-lowbit(x)+1,x]$ 中所有数的和,则数组 $sum$ 就是一个树形结构。 这就是我们的树状数组啦! 图解: ![](http://aliyunzixunbucket.oss-cn-beijing.aliyuncs.com/png/20180111003533563694.png) **图中红色节点忽略不计qwq** 在这个结构中: 1.每个节点保存以它为根的子树中所有叶节点的和 2.每个节点 $sum[x]$ 的子节点(**指最底层的叶子结点**!)个数等于 $lowbit(x)$ 的值。 笔者的书本上没有括号内容,然而我经过研究后发现,是指叶子结点的值。如4有1.2.3.4这4个叶子结点,对吧?所以 $lowbit(4)=4$ 。但是4有7个子节点(A4个,C3个)。 3.除了根节点,每个节点 $sum[x]$ 的父亲节点是 $sum[x+lowbit(x)]$ 4.一整颗树的深度为 $O(log N)$。 基本思想讲完了! ### part 2 :常支持的操作 树状数组资瓷查询前缀和(自然可以区间和啦!),单点修改,求逆序对(和正序对),区间修改等。 好的,下一部分qwq。 ### part 3 :代码 像这种数据结构(线段树,分块......)基本都有整体修改的标记,对吧qwq? 前缀和: 遍历一个节点的子树中的子节点,就可以了。 ```cpp int ask(int x){ int res=0; for(;x;x-=lowbit(x))res+=sum[x]; return res; } ``` 单点修改: 这个操作除了修改,还要维护前缀和,所以我们要对一个节点的祖先进行遍历,从下往上修改。这样子修改完,前缀和一定也是正确维护的。 ```cpp void add(int x,int val){ for(;x<=n;x+=lowbit(x))sum[x]+=val; }//n为原始数列中元素个数 ``` 区间修改: 我们新建一个数组 $b$,把一次区间修改(在区间 $[l,r]$ 中,把每一个值加上 $val$)转化为: **在 $b[l]$ 处加上 $val$,在 $b[r+1]$ 处减去 $val$。** 原理是前缀和。在区间 $[1,l-1]$ 中,前缀和不变;在区间 $[l,r]$ 中,前缀和增加了 $val$;在区间 $[r+1,n]$ 中,前缀和不变(加 $val$ 和减去 $val$ 抵消)。 正确性使然。 **这里的加是指单点修改。** ~~不要直接 $sum[x]+=val$,会气死我的~~ 区间和(区间查询): 这种题的区间修改和上面类似,但是要改一下,因为还要执行区间和操作。我们使用两个树状数组 $c1$ 和 $c2$。 它们初始值为0。 对于修改区间 $[l,r]$,我们执行四次操作: 在 $c1$ 中,`add(l,val);add(r+1,-val);` 在 $c2$ 中,`add(l,l*val);add(r+1,-(r+1)*val);` 我们还要用一个数组储存数列原本的前缀和。 下面,对于每一个区间查询,我们分成1到 $r$ 和1到 $l-1$两个部分,查询结果就是二者相减。 ```cpp int ask_lr(int l,int r){ int ans=sum[r]+(r+1)*ask_c1(r)-ask_c2(r); ans-=(sum[l-1]+l*ask_c1(l-1)-ask_c2(l-1)); return ans; } //ask_c1是对c1的操作,ask_c2是对c2的操作,sum记录原始前缀和 ``` 这个式子好好理解下吧! 讲完啦!(注意:根据不同的题目,大部分时候请开long long) ### part 4 :练习题 [一个比较不错的题单,做做吧](https://www.luogu.com.cn/training/1143) 结: 写了怎么久,终于写完啦! 谢谢阅读。