一、基本概念
事先声明,树状数组能解决的问题线段树可以解决,树状数组不能解决的线段树也能解决,但树状数组空间小,常数小,代码短(你也不想随便写个题就上百行吧T-T,绝对不是我懒)
那就学一下吧!
什么是树状数组?
一般来说,从名字就可以看出是通过用数组来模拟树形逻辑结构完成一些区间操作。但树状数组并非是线段树那样非常典型的树形结构。
下面是两幅图,分别表示线段树和树状数组。
线段树结构
树状数组结构
最底下一层均表示的是原始数组,线段树会开几倍的空间完成合并,而树状数组不需要。
每个红框下方所连接的方框表示它内部的对象。(画了老久了,给个赞再走吧T-T)
这里用tree[N]表示树状数组,a[N]表示原始数组。
显然tree[1] = a[1],
tree[2] = a[1] + a[2],
tree[3] = a[3],
tree[4] = a[1] + a[2] + a[3] + a[4]
.......
tree[7] = a[7],
tree[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8]
我们通过这样的表示就可以以较少的空间实现区间操作。
那么为什么要这样表示呢?
我也不是很清楚QAQ
仔细观察,tree数组是有规律的,将每一个下标用二进制表示出来:
4 : 0 0100 (第一个0表示符号位) tree[4] = a[1] + a[2] + a[3] + a[4]
6: 0 0110 tree[6] = a[5] + a[6]
8: 0 1000 tree[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8]
令m为每一个二进制数的最后一位1,将其它位变成0(符号位不变),如6 变成 0 0010, 即2,2表示的就是加的个数
当然也可以理解为从最后一位从右往左经过的连续0的长度,6:0 0110 很显然连续0长度为1,那么2的1次方也为2,所以令长度为k, m = 2^k
我们可以发现tree[i] = a[i] + ... + a[i - (m 或者 2^k) + 1]
知道了这些,我们就可以了解一下如何进行求和操作了。
比如求[1,7]的区间和, sum[7] = tree[7] + tree[6] + tree[4]
那么sum[i] = tree[i] + tree[i - m1] + tree[(i - m1) - m2] + ......
我们可以看出i - m1其实是i去掉末位1, (i - m1) - m2 是(i - m1) 去掉末位1
这样我们就实现了区间求和了,至于求[l, r]上的和,就是sum[r] - sum[l - 1]了。
二、具体实现
我们理解了基本的概念后,现在只有一个难点了,怎么求m(也就是2^k)。
这里我们构造了一个新的函数lowbit(有了它,你就会发现我们的每次操作是log(n)的)
int lowbit(int k)
{
return k & -k;
}
为什么k & -k 就是我们要求的m呢?
先举个例子:
6 : 0 0110 (第一个0是符号位)
-6:1 1010 负数在计算机中是以补码的形式存储的(不明白看这个)
这样我们发现进行&运算后就得到了我们想要的m。
其实也有具体证明,真的不是我懒得写QAQ。
既然这样,只需要构造一个树状数组出来就万事大吉了。
根据前面的信息,要想构造出这个数组,就要知道a[i]在哪个tree[]中出现过。
我们已知tree[i]中有哪些a[], 所以a[i]必定出现在tree[i], tree[i + m1], tree[(i + m1) + m2]……
那么就看代码了
int lowbit(int k)
{
return k & -k;
}
void add(int x, int k)
{
for(; x <= n; x += lowbit(x))
tree[x] += k;
}
int sum(int x)
{
int ans = 0;
for(; x; x -= lowbit(x))
ans += tree[x];
return ans;
}
int query(int l, int r)
{
if(l > r) return 0;//这个自己看着写QAQ
return sum(r) - sum(l - 1);
}
三、树状数组完成的操作
1、单点修改,区间查询。
就是上面给的代码,小试牛刀。
2、区间修改,单点查询。
这个问题我们可以利用化归的思想,利用差分将该问题转化为问题1.(不会差分)
利用数组d[N], d[i] = a[i] - a[i - 1](2 <= i <= n), d[1] = a[1].
查询a[i] a[i] = d[1] + d[2] + ... + d[i]
修改: 在[l,r]上加k, 就是d[l] + k, d[r + 1] - k.
int lowbit(int k)
{
return k & -k;
}
void add(int x, int k)
{
for(; x <= n; x += lowbit(x))
tree[x] += k;
}
void range_add(int l, int r, int k)
{
add(l, k);
add(r + 1, -k);
}
int sum(int x)
{
int ans = 0;
for(; x; x -= lowbit(x))
ans += tree[x];
return ans;
}
这里的tree[]表示的是d数组。注意初始化!!!会了就试试
3、区间修改,区间查询。
同样是差分思想, 问题一我们知道要求前缀和,问题二知道单点查询,那么在问题二的单点查询的基础上,再求一个前缀和,我们不就可以实现区间查询了吗?
sum[1,x]的和:
仔细观察式子可以发现,d[1]加了x次,d[2]加了x - 1次,以此类推......
我们可以将式子整理一下:
这样我们就只用维护两个数组的前缀和了。
代码如下:
#define ll long long
inline int lowbit(int k)
{
return k & -k;
}
inline void add(int x, int k)
{
for(ll i = x; i <= n; i += lowbit(i))
sum1[i] += k, sum2[i] += x * k;
}
inline void range_add(int l, int r, int k)
{
add(l, k);
add(r + 1, -k);
}
inline ll sum(int x)
{
ll ans = 0;
for(ll i = x; i; i -= lowbit(i))
ans += (x + 1) * sum1[i] - sum2[i];
return ans;
}
inline ll query(int l, int r)
{
return sum(r) - sum(l - 1);
}
其实还有二维的树状数组,真的不是因为懒不写的, 我太笨了,学不明白。
基本上掌握了上面的知识,树状数组就算入门了。(上面的代码可能有问题,有错可以联系我)
想要真正学好,还得多写题啊。QAQ共勉