树状数组!!!
其实我个人一直觉得树状数组要比线段树难qwq
以前一直看不懂别人写的博客或者题解,但是由于今天又充足的时间(没错,在我浪费了四个小时在线段树上之后,我还有剩余时间去复习树状数组(* ̄︶ ̄)),于是我终于下定决心要逼自己学会树状数组(或者至少树状数组1?)
反正就是抱着这样的心理,我在扒拉了半个小时博客和洛谷题解之后我又放弃了,直到临回宿舍一个小时前,就是这个神圣的时刻!我灵光一闪,转头去了哔哩哔哩...
然后(* ̄︶ ̄)
我成功的发现了一个神仙视频:https://www.bilibili.com/video/BV1pE41197Qj?from=search&seid=1322715055342015680
感谢这位做视频的巨神(Thanks♪(・ω・)ノ)
我总算是把树状数组1弄懂了
而且我还发现了一个道理:
其实树状数组不需要你去理解为什么,(毕竟我个人感觉树状数组说不定就是某个巨神某一刻灵光一闪想出来的一种,非常非常非常巧合的东西,你要是说树状数组里面涉及到的知识有什么联系,我觉得是没有的。或者说如果这是一道题,让你想出这种方法来求解这道题,我相信大部分人应该是无从下手的,毕竟谁会想到树状数组还会和二进制,补码反码这种东西联系起来啊(╯‵□′)╯︵┻━┻,所以树状数组在我看来,只是一个因某种巧合而产生的东西,所以你完全没有必要去搞懂这个思路到底是怎么来的,为什么会想着这样去做。毕竟大部分人都是普通人,而不是天才,我们只需要跟在天才的后面捡人家已经给你准备好的结论就好了呗,干嘛要这么为难自己呢╮(╯▽╰)╭)也因此,我们只需要知道结论即可。
(当然,这只是我个人的观点,我认为我就是一个小蒟蒻而已,没必要在考前还这么难为自己,但是如果是某些巨神,您们当然是按照自己的方法去理解就好啦(^-^)V)
所以经过那个神仙视频,我了解到了大体过程之后,就很快可以上手编程了。所以我觉得我这么菜的人都可以看懂那个视频,您们当然也可以喽(^-^)V
如果您在看了视频之后还是有一点迷惑,这里有几个建议给您呢:
1.自己手动模拟一下视频中的过程
2.画个树状数组看一下,注意树状数组要画准确
好啦,到这这个板子就算是完了,但是为了让这篇博客更像一篇博客,我还是转发一篇我认为比较不错的文章来激励敷衍一下您们吧!
当然,我的树状数组的学习还没有完成(好像还有一个,模板题要做qwq),明天继续!!!
--------------------------next------------------------
Y(^o^)Y,开心,终于把树状数组所有的模板(好像也只有两个?)都敲完了!!
其实另一个板子只是把问题改成了区间修改,单点询问罢了,所以其实我们只要设置一个差分数组就可以了,按照差分的思想,可以减少时间复杂度,具体的视频里其实都有讲解,相信如果把第一个板子练熟了,第二个板子也不成问题,当然,个人觉得视频里介绍的可能是简单了一点,详细情况我会把个人认为的博客搬运到下面,如果还是理解有困难的同学可以看一下(* ̄︶ ̄)
树状数组详解
先来看几个问题吧。
1.什么是树状数组?
顾名思义,就是用数组来模拟树形结构呗。那么衍生出一个问题,为什么不直接建树?答案是没必要,因为树状数组能处理的问题就没必要建树。和Trie树的构造方式有类似之处。
2.树状数组可以解决什么问题
可以解决大部分基于区间上的更新以及求和问题。
3.树状数组和线段树的区别在哪里
树状数组可以解决的问题都可以用线段树解决,这两者的区别在哪里呢?树状数组的系数要少很多,就比如字符串模拟大数可以解决大数问题,也可以解决1+1的问题,但没人会在1+1的问题上用大数模拟。
4.树状数组的优点和缺点
修改和查询的复杂度都是O(logN),而且相比线段树系数要少很多,比传统数组要快,而且容易写。
缺点是遇到复杂的区间问题还是不能解决,功能还是有限。
一、树状数组介绍
二叉树大家一定都知道,如下图
如果每个父亲都存的是两个儿子的值,是不是就可以解决这类区间问题了呢。是的没错,但是这样的树形结构,叫做线段树。
那真的的树形结构是怎样的,和上图类似,但省去了一些节点,以达到用数组建树。
黑色数组代表原来的数组(下面用A[i]代替),红色结构代表我们的树状数组(下面用C[i]代替),发现没有,每个位置只有一个方框,令每个位置存的就是子节点的值的和,则有
- C[1] = A[1];
- C[2] = A[1] + A[2];
- C[3] = A[3];
- C[4] = A[1] + A[2] + A[3] + A[4];
- C[5] = A[5];
- C[6] = A[5] + A[6];
- C[7] = A[7];
- C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8];
可以发现,这颗树是有规律的
C[i] = A[i - 2k+1] + A[i - 2k+2] + ... + A[i]; //k为i的二进制中从最低位到高位连续零的长度
例如i = 8(1000)时候,k = 3,可自行验证。
这个怎么实现求和呢,比如我们要找前7项和,那么应该是SUM = C[7] + C[6] + C[4];
而根据上面的式子,容易的出SUMi = C[i] + C[i-2k1] + C[(i - 2k1) - 2k2] + .....;
其实树状数组就是一个二进制上面的应用。
现在新的问题来了2^k该怎么求呢,不难得出2^k = i&(i^(i-1));但这个还是不好求出呀,前辈的智慧就出来了,2^k = i&(-i);
为什么呢?
这里利用的负数的存储特性,负数是以补码存储的,对于整数运算 x&(-x)有
● 当x为0时,即 0 & 0,结果为0;
●当x为奇数时,最后一个比特位为1,取反加1没有进位,故x和-x除最后一位外前面的位正好相反,按位与结果为0。结果为1。
●当x为偶数,且为2的m次方时,x的二进制表示中只有一位是1(从右往左的第m+1位),其右边有m位0,故x取反加1后,从右到左第有m个0,第m+1位及其左边全是1。这样,x& (-x) 得到的就是x。
●当x为偶数,却不为2的m次方的形式时,可以写作x= y *
(2^k)。其中,y的最低位为1。实际上就是把x用一个奇数左移k位来表示。这时,x的二进制表示最右边有k个0,从右往左第k+1位为1。当对x取反时,最右边的k位0变成1,第k+1位变为0;再加1,最右边的k位就又变成了0,第k+1位因为进位的关系变成了1。左边的位因为没有进位,正好和x原来对应的位上的值相反。二者按位与,得到:第k+1位上为1,左边右边都为0。结果为2^k。
总结一下:x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。
而且这个有一个专门的称呼,叫做lowbit,即取2^k。
二、如何建立树状数组
上面已经解释了如何用树状数组求区间和,那么如果我们要更新某一个点的值呢,还是一样的,上面说了C[i] = A[i - 2k+1] + A[i - 2k+2] + ... + A[i],那么如果我们更新某个A[i]的值,则会影响到所有包含有A[i]位置。如果求A[i]包含哪些位置里呢,同理有
A[i] 包含于 C[i + 2k]、C[(i + 2k) + 2k]...;
好,现在已经搞清楚了更新和求和,就可以来建树状数组了。如果上面的求和、更新或者lowbit步骤还没搞懂的化,建议再思考弄懂再往下看。
那么构造一个树状数组则为
1 int n;
2 int a[1005],c[1005]; //对应原数组和树状数组
3
4 int lowbit(int x){
5 return x&(-x);
6 }
7
8 void updata(int i,int k){ //在i位置加上k
9 while(i <= n){
10 c[i] += k;
11 i += lowbit(i);
12 }
13 }
14
15 int getsum(int i){ //求A[1 - i]的和
16 int res = 0;
17 while(i > 0){
18 res += c[i];
19 i -= lowbit(i);
20 }
21 return res;
22 }
这样就构造了一个树状数组。下面看一道模板题目吧。
题目链接:https://vjudge.net/problem/HDU-1166
直接看代码吧
1 #include <bits/stdc++.h>
2 using namespace std;
3
4 int n,m;
5 int a[50005],c[50005]; //对应原数组和树状数组
6
7 int lowbit(int x){
8 return x&(-x);
9 }
10
11 void updata(int i,int k){ //在i位置加上k
12 while(i <= n){
13 c[i] += k;
14 i += lowbit(i);
15 }
16 }
17
18 int getsum(int i){ //求A[1 - i]的和
19 int res = 0;
20 while(i > 0){
21 res += c[i];
22 i -= lowbit(i);
23