参考博客:http://www.cppblog.com/menjitianya/archive/2015/11/02/212171.html

# 建立树状数组的目的

树状数组 (Binary Indexed Tree) 代码简洁、常数小于线段树,但功能少。即使如此也很常用。(线段树板子长抄着慢

树状数组的作用是可以log(n)维护在线前缀和(最大最小值什么的也可以?)

树状数组要解决两个东西:

  1. int add(int i, int x) //对一个数进行更新,要求时间复杂度为 O(log n)
  2. int sum(int i) //求[x1,xi]的区间元素和,时间复杂度为 O(log n)

# 定义

树状数组的定义是一个一维数组 tree[],其中 tree[i] 表示 [i-(i&(-i))+1,i] 这个区间内 a 数组元素的和

但这个定义挺迷的,i-(i&(-i))+1 代表什么呢?别急,慢慢往下看,

树状数组虽然是用数组实现的,但他实际上是一棵树形的,如下图。

树状数组

可以看出这棵树中,对于两个数组下标 xx, yy, 如果满足 x+2k=yx+2^k=ykkxx 的二进制表示中末尾 00 的个数,在后文定义 lowbit(x)=2klowbit(x)=2^k),则定义 yyxx 的父亲。
44kk22,则 44 的父亲即是 4+2k4=4+22=84+2^{k_4}=4+2^2=8

# CiC_i是什么

每一个结点 CiC_i 存了它自己和它的所有(递归)子结点的 AiA_i 值之和。

那么能否用数学表达 CiC_i 存的是哪些数呢?我们来看看 C8C_8

C8=C4+C6+C7+A8=C2+A3+A4+A5+A6+A7+A8=A1+A2+A3+A4+A5+A6+A7+A8\begin{aligned} C_8 &= C_4 + C_6 + C_7 + A_8\\ &= C_2 + A_3 + A_4 + A_5 + A_6 + A_7 + A_8\\ &= A_1 + A_2 + A_3 + A_4 + A_5 + A_6 + A_7 + A_8 \end{aligned}

比较显然的是,CiC_i 存的是 Ai{A_i} 数列中的连续和,而连续和的右边界一定是 ii。那么左边界是什么呢?

从图上可以看出,左边界是顺着 CiC_i 的最左儿子一直找直到找到叶子结点。

那能否用数学来直接推导出左边界呢?

根据父子结点关系可以逆推得到 C8C_8C6C_6C7C_7 的左边界,过程是这样的(为方便观察规律,标注了每个数字的二进制):

父结点 81000 6110 7111
子结点1 40100 5101
子结点2 20010
子结点3 10001
左边界 10001 5101 7111

结论已经呼之欲出了:对于每个数 ii,它的左边界是:把 ii 最右边的 11 变成 00 ,再在这一位的加上 11

顺便提一句,要遍历某个点的子结点,可以取出最右一位的 11 以后,依次往后面的位加 11,得到的所有数就是它的所有子结点。
88 1000 的子结点就有 44 010066 0110770111
也可以换一种理解方式,ii 的子结点为 $ i-2^k , k \in [0,log_2(lowbit(i))-1]$。

按照上面定义的

kkxx 的二进制表示中末尾 0 的个数

再定义 lowbit(x)=2kxlowbit(x) = 2^{k_x},即 lowbit(x)lowbit(x)xx 最右的一个 11
那么左边界就应该是 ilowbit(i)+1i-lowbit(i)+1,所以,

Ci=j=ilowbit(i)+1iAi C_i = \sum_{j=i-lowbit(i)+1}^{i}{A_i}

# 求和函数 sum(int i)

明白了 CiC_i 的含义以后,我们可以用它来求 sum(i)sum(i) 了。用 lowbit(i)lowbit(i) 表示 2ki2^{k_i} ,则有

sum(i)=A[1]+A[2]+...+A[i]=A[1]+A[2]+...+A[ilowbit(i)]+A[ilowbit(i)+1]+...+A[i]=sum(ilowbit(i))+C[i]\begin{aligned} sum(i) &= A[1] + A[2] + ... + A[i]\\ &= A[1] + A[2] + ... + A[i-lowbit(i)] + A[i-lowbit(i)+1] + ... + A[i]\\ &= sum(i-lowbit(i)) + C[i] \end{aligned}

上式可以用递归求解,边界是 sum[0]=0sum[0] = 0

用递归形式写就是:

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;
}

时间复杂度是 O(logn)O(log n)

# 更新函数 add(int i, int x)

更新操作即是把第 ii 个数增加 xx。朴素前缀和要更新 ii 及以后的所有前缀和,所以复杂度是 O(n)O(n)
可以观察到,树状数组中,所有数的信息只存在该下标对应的结点和它的(递归)父结点的 CiC_i 中。因此,只需要递归对父结点做同样的加减即可。
根据定义,ii 结点的父结点是 i+lowbit(i)i+lowbit(i),代码也就不难写了。
这一过程同样有递归和非递归形式。

递归形式:

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);
    }
}

# O(1)O(1)lowbit(x)

可以看到,不管是 add(i,x) 还是 sum(i),其精髓在于 lowbit(i),因为

结点 ii 的父结点是 i+lowbit(i)i+lowbit(i) 结点 ii 的子结点为 $ i-2^k, k \in [0,log_2(lowbit(i))-1]$ 结点 ii 的左边界是 ilowbit(i)+1i-lowbit(i)+1

上面这两句,很漂亮的诠释了树状数组何为树状。理解了这句话,就可以自己手写树状数组了。

所以呢,最后一步,也是最关键的一步,就是求 lowbit(i) 了。朴素方法(把 ii 反复除以2)能在 O(logi)O(log\space i)kik_i,但是用位运算的方法可以把这个过程变成 O(1)O(1)

由补码的知识可以得到如果不知道的去看原博 (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;}

时间复杂度是 O(1)O(1)

至此,树状数组的一般应用就讲完了。初始化的时候,不需要像线段树一样必须要开 2n2^n 个内存,有多少数开多少内存就够了。把上面提到的三个函数组合起来就能去做最简单的 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)中实现了树状数组求最大值,初始化的复杂度为 O(nlogn)O(nlogn),单点维护和区间查询的复杂度都是 O(log2n)O(log^2n)

原理其实就是发生更改以后,遍历、更改所有(递归)父结点;查询的时候,就遍历该区间对应所有结点的值的最大值。

利用好这一句话:

结点 ii 的子结点为 $ 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);
}