参考博客: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 代表什么呢?别急,慢慢往下看,
树状数组虽然是用数组实现的,但他实际上是一棵树形的,如下图。

可以看出这棵树中,对于两个数组下标 , , 如果满足  ( 为  的二进制表示中末尾  的个数,在后文定义 ),则定义  是  的父亲。
如  的  为 ,则  的父亲即是 。
# 是什么
每一个结点 存了它自己和它的所有(递归)子结点的 值之和。
那么能否用数学表达 存的是哪些数呢?我们来看看 。
比较显然的是, 存的是 数列中的连续和,而连续和的右边界一定是 。那么左边界是什么呢?
从图上可以看出,左边界是顺着 的最左儿子一直找直到找到叶子结点。
那能否用数学来直接推导出左边界呢?
根据父子结点关系可以逆推得到 、、 的左边界,过程是这样的(为方便观察规律,标注了每个数字的二进制):
| 父结点 | 8 1000 | 6 110 | 7 111 | 
|---|---|---|---|
| 子结点1 | 4 0100 | 5 101 | |
| 子结点2 | 2 0010 | ||
| 子结点3 | 1 0001 | ||
| 左边界 | 1 0001 | 5 101 | 7 111 | 
结论已经呼之欲出了:对于每个数 ,它的左边界是:把 最右边的 变成 ,再在这一位的加上 。
顺便提一句,要遍历某个点的子结点,可以取出最右一位的  以后,依次往后面的位加 ,得到的所有数就是它的所有子结点。
如  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);
}