树状数组
树状数组是一个优美小巧的数据结构,在很多时候可以代替线段树。一句话概括就是,凡是树状数组可以解决的问题,线段树都可以解决,反过来线段树可以解决的问题,树状数组不一定能解决。
树状数组英文名称为Binary Index Tree
,直译过来就是二进制索引树
,我觉得二进制索引树更能说明其本质。树状数组的本质就是一种通过二进制位来维护一个序列前i和的数据结构。
对于维护的序列A
,定义C[i]=A[j+1]+...+A[i]
,其中j
为i
的二进制表示中把最右边的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的个数。可以看一下这张图,基本就能明白了
我们选择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的事情,二维线段树麻烦的让人不想写。