树状数组

树状数组是一个优美小巧的数据结构,在很多时候可以代替线段树。一句话概括就是,凡是树状数组可以解决的问题,线段树都可以解决,反过来线段树可以解决的问题,树状数组不一定能解决。

树状数组英文名称为Binary Index Tree,直译过来就是二进制索引树,我觉得二进制索引树更能说明其本质。树状数组的本质就是一种通过二进制位来维护一个序列前i和的数据结构。

对于维护的序列A,定义C[i]=A[j+1]+...+A[i],其中ji的二进制表示中把最右边的1换成0的值。j的值可以通过lowbit求出,即i-lowbit(i)

lowbit(a)2^(a的二进制表示末尾0的个数)。可以用下面式子求出

lowbit(a)=a&(~a+1)

或者根据补码的性质简化为

lowbit(a)=a&(-a)

修改方式如下

    void modify(int p,int delta)
    {
        while (p<=N)
        {
            C[p]+=delta;
            p+=lowbit(p);
        }
    }

求前缀和如下

    int sum(int p)
    {
        int rs=0;
        while (p)
        {
            rs+=C[p];
            p-=lowbit(p);
        }
        return rs;
    }

树状数组经常用来求一段区间的和,适用于该区间上的值是在不断变化的情景(不然就前缀和处理下就行了),常规数组的修改是O(1),区间查询是O(n),而树状数组的修改和查询都是O(lgn). 尽管线段树也能处理这种情况,并且功能要强大的多,但是树状数组编码上简单太多~
感谢评论区的补充:树状数组往高维扩展时非常方便(加个循环的事情),而线段树则写起来非常麻烦

树状数组需要一个辅助数组C,假设输入数组是A,则C[i]表示A[i-2^k+1]+…+A[i]总共2^k个数的和,其中k为i在二进制表示下从右到左连续0的个数。可以看一下这张图,基本就能明白了
QQ截图20151207170241

我们选择C[6]来解释下这个图,6的二进制表示为110,那么从右向左连续0的个数是1,所以C[6]表示2^1=2个连续数的和,也就是A[5]+A[6],再看C[8],8的二进制表示为1000,从右往左连续0的个数的3,所以C[8]表示2^3=8个连续数的和,也就是A[1]+…A[8],可以用x&(-x)开快速求得2^k

现在来看下对数组中的某个数做修改,数组C会怎么变,假设我们对A[3]进行了修改,那么C[3]肯定是要改变的,因为C[3] = A[3],C[4]也是要改变的,C[4]=A[3]+A[4],然后改变的就是C[8],C[16]等等。。。可以看到,其实是和二进制相关的,故修改的复杂度是O(lgn)

然后来看下对数组中某个区间做查询该怎么处理。假设查询q = (l,r)表示求区间l到r之间的数的和,用sum(x)表示下标从1到x之间的数据之和,则q = sum(r)-sum(l-1),那么如何快速的来求sum(x)呢?我们可以看一个例子,假设我们要求sum(7),sum(7) = C[7]+C[6]+C[4],首先C[7]只表示1个数的和A[7],处理了A[7]之后,我们要求前6个数的和,这时候C[6]表示两个数的和A[5]+A[6],接下来只需要处理前4个数据的和就行了,刚好C[4]表示前4个数据的和,可以看到并不是一个个的去加,而是利用二进制的思想,一段一段的加,复杂度为O(lgn)

对具体如何实现如果有疑问可以看看下面的代码,实现起来非常简单明了

hdu1166:http://acm.hdu.edu.cn/showproblem.php?pid=1166

#include <iostream>
#include <cstdio>
#include <string>
#include <string.h>
using namespace std;
 
const int N = 50010;
int c[N];
int n,t;
 
int lowbit(int i)
{
	return i&(-i);
}
 
void add(int index, int addValue)
{
	while (index <= n)
	{
		c[index] += addValue;
		index += lowbit(index);
	}
}
 
int sum(int index)
{
	int ret = 0;
	while (index >= 1)
	{
		ret += c[index];
		index -= lowbit(index);
	}
	return ret;
}
int main()
{
	scanf("%d", &t);
	for (int cas = 1; cas <= t;cas++)
	{
		printf("Case %d:\n", cas);
		memset(c, 0, sizeof c);
		cin >> n;
		for (int i = 1; i <= n; i++)
		{
			int x;
			scanf("%d", &x);
			add(i, x);
		}
		string query;
		while (cin>>query)
		{
			if (query == "End")
				break;
			else if (query == "Add"||query=="Sub")
			{
				int index, value;
				scanf("%d%d", &index, &value);
				add(index, value*(query=="Add"?1:-1));
			}else
			{
				int from, to;
				scanf("%d%d", &from, &to);
				printf("%d\n", sum(to) - sum(from - 1));
			}
		}
	}
	return 0;
}
hdu1541: http://acm.hdu.edu.cn/showproblem.php?pid=1541
#include <iostream>
#include <cstdio>
#include <string>
#include <string.h>
#include <algorithm>
#include <map>
using namespace std;
 
const int N = 50010;
int c[N];
pair<int, int> input[N];
int n,t;
 
int lowbit(int i)
{
	return i&(-i);
}
 
int sum(int index)
{
	int ret = 0;
	while (index > 0)
	{
		ret += c[index];
		index -= lowbit(index);
	}
	return ret;
}
 
void  add(int index, int addValue)
{
	while (index < N)
	{
		c[index] += addValue;
		index += lowbit(index);
	}
}
int main()
{
	while (cin >> n)
	{
		memset(c, 0, sizeof c);
		for (int i = 0; i < n; i++)
		{
			cin >> input[i].first >> input[i].second;
			input[i].first++;
			input[i].second++;
		}
		sort(input, input + n);
		map<int, int> ret;
		for (int i = 0; i < n; i++)
		{
			int y = input[i].second;
			int cnt = sum(y);
			ret[cnt]++;
			add(y, 1);
		}
		for (int i = 0; i < n ; i++)
			cout << ret[i] << endl;
	}
	return 0;
}

二维树状数组:add 的功能是改变元素(x, y),sum的功能则是求从元素(1, 1)开始到(x, y)的总和,同样,可以求出任意一个子矩阵内的所有元素之和,即sum(x2, y2) – sum(x1-1, y2) – sum(x2, y1-1) + sum(x1-1, y1-1) 代码也很简单,可以两个for循环,也可以两个while来写,最显而易见的应用就是快速求子矩阵的和

poj1195:http://poj.org/problem?id=1195

#include <iostream>
#include <cstdio>
#include <string>
#include <string.h>
#include <algorithm>
#include <map>
using namespace std;
 
const int N = 1100;
int c[N][N];
 
int n,t,k;
 
int lowbit(int i)
{
	return i&(-i);
}
 
void add(int x, int y,int v)
{
	while (x <= n)
	{
		int tempy = y;
		while (tempy <=n )
		{
			c[x][tempy] += v;
			tempy += lowbit(tempy);
		}
		x += lowbit(x);
	}
}
int sum(int x, int y)
{
	int ret = 0;
	while (x>0)
	{
		int tempy = y;
		while (tempy > 0)
		{
			ret += c[x][tempy];
			tempy -= lowbit(tempy);
		}
		x -= lowbit(x);
	}
	return ret;
}
int main()
{
	int q;
	memset(c, 0, sizeof c);
	while (cin >> q)
	{
		if (q == 0)
		{
			cin >> n;
			n++;
		}
		else if (q == 1)
		{
			int x, y, v;
			cin >> x >> y >> v;
			x++, y++;
			add(x, y, v);
		}
		else if (q == 2)
		{
			int x1, y1, x2, y2;
			cin >> x1 >> y1 >> x2 >> y2;
			x1++, x2++, y1++, y2++;
			cout << sum(x2, y2) - sum(x2, y1 - 1) - sum(x1 - 1, y2) + sum(x1 - 1, y1 - 1) << endl;
		}
		else
			break;
	}
	return 0;
}

poj2155:http://poj.org/problem?id=2155
这题其实算是二维数组的反向思维题,假定要翻转区间(x1,y1)-(x2,y2)的值,可以在(x1,y1)(x1,y2+1)(x2+1,y1)(x2+1,y2+1)四个点分别加1,然后可以发现,求某个点的值,就是求sum(i,j)%2

#include <iostream>
#include <cstdio>
#include <string>
#include <string.h>
#include <algorithm>
#include <map>
using namespace std;
 
const int N = 1010;
int c[N][N];
 
int n,t,k;
 
int lowbit(int i)
{
	return i&(-i);
}
 
void add(int x, int y)
{
	while (x < N)
	{
		int tempy = y;
		while (tempy < N)
		{
			c[x][tempy] += 1;
			tempy += lowbit(tempy);
		}
		x += lowbit(x);
	}
}
int sum(int x, int y)
{
	int ret = 0;
	while (x>0)
	{
		int tempy = y;
		while (tempy > 0)
		{
			ret += c[x][tempy];
			tempy -= lowbit(tempy);
		}
		x -= lowbit(x);
	}
	return ret;
}
int main()
{
	cin >> t;
	while (t--)
	{
		memset(c, 0, sizeof c);
		cin >> n >> k;
		n++;
		while (k--)
		{
			char query;
			int x1,y1,x2,y2;
			cin >> query >> x1 >> y1;
			if (query == 'C')
			{
				cin >> x2 >> y2;
				add(x2+1, y2+1);
				add(x2+1, y1 );
				add(x1, y2+1);
				add(x1, y1);
			}
			else
			{
				cout << sum(x1, y1)%2 << endl;
			}
		}
		cout << endl;
	}
	return 0;
}

树状数组另一个强大的地方在于往高维扩展非常方便,比如求子矩阵和会非常方便。而二维线段树则要麻烦得多。二维树状数组就是加个for的事情,二维线段树麻烦的让人不想写。


posted @ 2016-04-05 11:52  _tham  阅读(188)  评论(0编辑  收藏  举报