参考博客:http://www.cppblog.com/menjitianya/archive/2015/11/02/212171.html
# 建立树状数组的目的
树状数组 (Binary Indexed Tree) 代码简洁、常数小于线段树,但功能少。即使如此也很常用。(线段树板子长抄着慢)
树状数组的作用是可以log(n)维护在线前缀和(最大最小值什么的也可以?)
树状数组要解决两个东西:
- int add(int i, int x) //对一个数进行更新,要求时间复杂度为 O(log n)
- int sum(int i) //求[x1,xi]的区间元素和,时间复杂度为 O(log n)
# 定义
树状数组的定义是一个一维数组 tree[]
,其中 tree[i]
表示 [i-(i&(-i))+1,i]
这个区间内 a
数组元素的和。
但这个定义挺迷的,i-(i&(-i))+1
代表什么呢?别急,慢慢往下看,
树状数组虽然是用数组实现的,但他实际上是一棵树形的,如下图。
可以看出这棵树中,对于两个数组下标 , , 如果满足 ( 为 的二进制表示中末尾 的个数,在后文定义 ),则定义 是 的父亲。
如 的 为 ,则 的父亲即是 。
# 是什么
每一个结点 存了它自己和它的所有(递归)子结点的 值之和。
那么能否用数学表达 存的是哪些数呢?我们来看看 。
比较显然的是, 存的是 数列中的连续和,而连续和的右边界一定是 。那么左边界是什么呢?
从图上可以看出,左边界是顺着 的最左儿子一直找直到找到叶子结点。
那能否用数学来直接推导出左边界呢?
根据父子结点关系可以逆推得到 、、 的左边界,过程是这样的(为方便观察规律,标注了每个数字的二进制):
父结点 | 81000 | 6110 | 7111 |
---|---|---|---|
子结点1 | 40100 | 5101 | |
子结点2 | 20010 | ||
子结点3 | 10001 | ||
左边界 | 10001 | 5101 | 7111 |
结论已经呼之欲出了:对于每个数 ,它的左边界是:把 最右边的 变成 ,再在这一位的加上 。
顺便提一句,要遍历某个点的子结点,可以取出最右一位的 以后,依次往后面的位加 ,得到的所有数就是它的所有子结点。
如 1000
的子结点就有 0100
、 0110
、0111
。
也可以换一种理解方式, 的子结点为 $ i-2^k , k \in [0,log_2(lowbit(i))-1]$。
按照上面定义的
为 的二进制表示中末尾 0 的个数
再定义 ,即 为 最右的一个 ,
那么左边界就应该是 ,所以,
# 求和函数 sum(int i)
明白了 的含义以后,我们可以用它来求 了。用 表示 ,则有
上式可以用递归求解,边界是 。
用递归形式写就是:
int sum(int i)
{
return i ? C[i] + sum[lowbit(i)] : 0;
}
可以改写为非递归形式:
int sum(int i)
{
int ans = 0;
while(i)
{
ans += C[i];
i -= lowbit(i);
}
return ans;
}
时间复杂度是 。
# 更新函数 add(int i, int x)
更新操作即是把第 个数增加 。朴素前缀和要更新 及以后的所有前缀和,所以复杂度是 。
可以观察到,树状数组中,所有数的信息只存在该下标对应的结点和它的(递归)父结点的 中。因此,只需要递归对父结点做同样的加减即可。
根据定义, 结点的父结点是 ,代码也就不难写了。
这一过程同样有递归和非递归形式。
递归形式:
int add(int i, int x)
{
if(i <= n)
{
c[i] += x;
add(i + lowbit(i), x);
}
}
非递归形式:
int add(int i, int x)
{
while(i <= n)
{
c[i] += x;
i += lowbit(i);
}
}
# 求 lowbit(x)
可以看到,不管是 add(i,x)
还是 sum(i)
,其精髓在于 lowbit(i)
,因为
结点 的父结点是 结点 的子结点为 $ i-2^k, k \in [0,log_2(lowbit(i))-1]$ 结点 的左边界是
上面这两句,很漂亮的诠释了树状数组何为树状。理解了这句话,就可以自己手写树状数组了。
所以呢,最后一步,也是最关键的一步,就是求 lowbit(i)
了。朴素方法(把 反复除以2)能在 求 ,但是用位运算的方法可以把这个过程变成 。
由补码的知识可以得到如果不知道的去看原博 (opens new window)
lowbit(x) = x & (-x)
补码这种东西虽然不直观,初学者很难懂,但是挺神奇的,比如这里的 x&(-x)
,还有原博的例子
(+5) + (-5) = 00000101 + 11111011 = 1 00000000 (溢出了!!!) = 0
至此,再看文章开头对树状数组的定义:
树状数组的定义是一个一维数组
tree[]
,其中tree[i]
表示[i-(i&(-i))+1,i]
这个区间内a
数组元素的和。
其实就很好理解了。
实现上,由于 &
的优先级低于 -
,可以这么写:
int lowbit(int x){return x & -x;}
时间复杂度是 。
至此,树状数组的一般应用就讲完了。初始化的时候,不需要像线段树一样必须要开 个内存,有多少数开多少内存就够了。把上面提到的三个函数组合起来就能去做最简单的 Point Update Interval Query(点更新,段询问)了。
ll C[maxn] = { 0 };
ll n;
ll lowbit(ll x)
{
return x & -x;
}
void add(ll i, ll x)
{
while (i <= n)
{
C[i] += x;
i += lowbit(i);
}
}
ll sum(ll i)
{
ll ans = 0;
while (i > 0)
{
ans += C[i];
i -= lowbit(i);
}
return ans;
}
# 应用:更新区间、查询单元素
树状数组最基础的模型是 Point Update Interval Query(点更新,段询问),但是做一下差分也可以实现 Interval Update Point Query(段更新,点求值)。具体实现略。
# 应用:求最大值
理论上来说,树状数组也可以 Point Update Interval Query 求区间最大值。
在这篇博客 (opens new window)中实现了树状数组求最大值,初始化的复杂度为 ,单点维护和区间查询的复杂度都是 。
原理其实就是发生更改以后,遍历、更改所有(递归)父结点;查询的时候,就遍历该区间对应所有结点的值的最大值。
利用好这一句话:
结点 的子结点为 $ i-2^k, k \in [0,log_2(lowbit(i))-1]$
const int maxn = 3e5;
int a[maxn], h[maxn];
int n, m;
int lowbit(int x)
{
return x & (-x);
}
void update(int x) //x指下标。在求最大值中,原数列必须保留。
{
int lx, i;
while (x <= n)
{
lx = lowbit(x);
if (a[x] < h[x])
for (i=1; i<lx; i<<=1)
h[x] = max(h[x], h[x-i]);
h[x] = a[x];
x += lowbit(x);
}
}
int query(int x, int y)//求[x,y]区间内最大值
{
int ans = 0;
while (y >= x)
{
ans = max(a[y], ans);
y--;
for (; y-lowbit(y) >= x; y -= lowbit(y))
ans = max(h[y], ans);
}
return ans;
}
int main()
{
//完成对 a[i] 输入以后开始更新 h[i]
memset(h,0,sizeof(h));
for (int i = 1; i <= n; i++)
{
cin >> a[i];
update(i);
}
//查询 [x, y] 最大值
ans = query(x, y);
//更新 a[x] 以后更新单点
a[x] = y;
update(x);
}