传统数组(共n个元素)的元素修改和连续元素求和的复杂度分别为O(1)和O(n)。树状数组通过将线性结构转换成伪树状结构(线性结构只能逐个扫描元素,而树状结构可以实现跳跃式扫描),使得修改和求和复杂度均为O(lgn),大大提高了整体效率。
给定序列(数列)A,我们设一个数组C满足
C[i] = A[i–2^k+ 1] + … + A[i]
其中,k为i在二进制下末尾0的个数,i从1开始算!
则我们称C为树状数组。
下面的问题是,给定i,如何求2^k?
答案很简单:2^k=i&(i^(i-1)) ,也就是i&(-i) 为什么呢?? 请看下面:
整数运算 x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。
因为:x &(-x) 就是整数x与其相反数(负号取反)的按位与:1&1=1,0&1 =0, 0&0 =1。具体分析如下:
□ 当x为0时,x&(-x) 即 0 & 0,结果为0;
□ 当x不为0时,x和-x必有一个为正。不失一般性,设x为正。
●当x为奇数时,最后一个比特为1,取反加1没有进位,故x和-x除最后一位外前面的位正好相反,按位与结果为0。最后一位都为1,故结果为 1。
●当x为偶数,且为2的m次方(m>0)时,x的二进制表示中只有一位是1(从右往左的第m+1位),其右边有m位0,左边也都是0(个数由表示 x的字 节数决定),故x取反加1后,从右到左第有m个0,第m+1位及其左边全是1。这样,x& (-x) 得到的就是x。
●当x为偶数,却不为2的m次方的形式时,可以写作x= y * (2^k)。其中,y的最低位为1。实际上就是把x用一个奇数左移k位来表示。这时,x的 二进制 表示最右边有k个0,从右往左第k+1位为1。当对x取反时,最右边的k位0变成1,第k+1位变为0;再加1,最右边的k位就又变成了0,第 k+1位因为进 位的关系变成了1。左边的位因为没有进位,正好和x原来对应的位上的值相反。二者按位与,得到:第k+1位上为1,左边右边都为 0。结果为2^k,即 x中包含的2的最大次方的因子。
总结一下:x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。 比如x=32,其中2的最大次方因子 为 2^5,故x&(-x)结果为32;当x=28,其中2的最大次方因子为4,故x & (-x)结果为4。当x=24,其中2的最大次方因子为8,故 x&(-x)结果为 8。
下面进行解释:
以i=6为例(注意:a_x表示数字a是x进制表示形式):
(i)_10 = (0110)_2
(i-1)_10=(0101)_2
i xor (i-1) =(0011)_2
i and (i xor (i-1)) =(0010)_2
2^k = 2
C[6] = C[6-2+1]+…+A[6]=A[5]+A[6]
数组C的具体含义如下图所示:
当我们修改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,这个操作的复杂度在最坏情况下就是树的高度即O(logn)。另外,对于求数列的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。不难发现,这些子树的数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数,因此,求和操作的复杂度也是O(logn)。
树状数组能快速求任意区间的和:A[i] + A[i+1] + … + A[j],设sum(k) = A[1]+A[2]+…+A[k],则A[i] + A[i+1] + … + A[j] = sum(j)-sum(i-1)。
下面是别人总结的题目和代码(借鉴一下):
hdu 1541 Stars
题意:略。
思路:
树状数组经典入门题。
ans[i]: the amount of stars of the level i;
sum[i]: 横坐标为x的点,满足的the amount of the stars;
注意的地方:
(1)题目所给的点已经排好序了。
(2)由于x可能取0,而lowbit(0)=0,故add(0,1)会死循环。这就是为什么我一开始TLE的原因。所以将所有的 x++.
const int MAX1 = 15555, MAX2 = 32222;
int ans[MAX1], sum[MAX2], n,x,y;
int lowbit(int x){
return x & (-x);
}
int getsum(int pos){
int ret = 0;
while(pos > 0){
ret += sum[pos];
pos -= lowbit(pos);
}
return ret;
}
void add(int pos, int num){
while(pos < MAX2){
sum[pos] += num;
pos += lowbit(pos);
}
}
int main()
{
while(scanf("%d", &n) != EOF){
memset(sum, 0, sizeof(sum));
memset(ans, 0, sizeof(ans));
FOR(i,1,n){
scanf("%d%d", &x, &y);
x++; //注意加1,不然会在add(0,1)处死循环
ans[getsum(x)]++;
add(x, 1);
}
FOR(i,0,n-1)
printf("%dn", ans[i]);
}
return 0;
}
const int MAX = 8010;
int a[MAX], n, cnt[MAX], ans[MAX];
int lowbit(int x){
return x & (-x);
}
void add(int pos, int val){
while(pos <= n){
cnt[pos] += val;
pos += lowbit(pos);
}
}
int sum(int pos){
int res = 0;
while(pos > 0){
res += cnt[pos];
pos -= lowbit(pos);
}
return res;
}
//二分找到第一个等于x的位置
int binary_search(int x){
int low = 1, high = n, mid;
while(low <= high){
mid = (low + high) >> 1;
int k = sum(mid);
if(k >= x) high = mid-1;
else low = mid+1;
}
while(sum(mid) < x) mid++;
return mid;
}
int main()
{
while(scanf("%d", &n) != EOF){
a[1] = 0;
FOR(i,2,n) scanf("%d", &a[i]);
memset(cnt, 0, sizeof(cnt));
FOR(i,1,n) add(i,1);
for(int i = n; i >= 1; i--){
ans[i] = binary_search(a[i]+1);
add(ans[i], -1);
}
FOR(i,1,n) printf("%d\n", ans[i]);
}
return 0;
}
poj 2481 Cows
题意:两个区间:[Si, Ei] and [Sj, Ej].(0 <= S < E <= 105). 若 Si <= Sj and Ej <= Ei and Ei – Si > Ej – Sj, 则第i个区间覆盖第j个区间。给定N个区间(1 <= N <= 10^5),分别求出对于第i个区间,共有多少个区间能将它覆盖。
思路:初看好像挺复杂的。其实可以把区间[S, E]看成点(S, E),这样题目就转化为hdu 1541 Stars。只是这里是求该点左上方的点的个数。
虽然如此,我还是WA了不少,有一些细节没注意到。给点排序时是先按y由大到小排序,再按x由小到大排序。而不能先按x排序。比如n=3, [1,5], [1,4], [3,5]的例子。另外还要注意对相同点的处理。
const int MAX = 100010;
struct Node{
int x, y, id, ans;
}seq[MAX];
int sum[MAX], n;
int cmp1(const void *n1, const void *n2){
int res = ((Node*)n2)->y - ((Node*)n1)->y;
if(res == 0) return ((Node*)n1)->x - ((Node*)n2)->x;
else return res;
}
int cmp2(const void *n1, const void *n2){
return ((Node*)n1)->id - ((Node*)n2)->id;
}
int lowbit(int x){
return x & (-x);
}
void add(int pos, int val){
while(pos < MAX){ //我这里总是习惯性的写成n,浪费了很多时间。其实是横坐标x的最大范围。
sum[pos]+=val;
pos+=lowbit(pos);
}
}
int getsum(int pos){
int res = 0;
while(pos>0){
res+=sum[pos];
pos-=lowbit(pos);
}
return res;
}
int main()
{
while(scanf("%d", &n) && n){
FOR(i,1,n){
scanf("%d%d", &seq[i].x, &seq[i].y);
seq[i].x++, seq[i].y++;
seq[i].id = i;
}
qsort(seq+1, n, sizeof(Node), cmp1);
memset(sum, 0, sizeof(sum));
seq[1].ans = 0;
add(seq[1].x, 1);
int fa = 1;
FOR(i,2,n){
if(seq[i].x == seq[fa].x && seq[i].y == seq[fa].y){
seq[i].ans = seq[fa].ans;
}else{
fa = i;
seq[i].ans = getsum(seq[i].x);
}
add(seq[i].x, 1);
}
qsort(seq+1, n, sizeof(Node), cmp2);
printf("%d", seq[1].ans);
FOR(i,2,n) printf(" %d", seq[i].ans);
printf("\n");
}
return 0;
}
poj 2155 Matrix
二维树状数组经典题
题意:给一个N*N的矩阵,里面的值不是0,就是1。初始时每一个格子的值为0。
现对该矩阵有两种操作:(共T次)
1.C x1 y1 x2 y2:将左上角为(x1, y1),右下角为(x2, y2)这个范围的子矩阵里的值全部取反。
2.Q x y:查询矩阵中第i行,第j列的值。
(2 <= N <= 1000, 1 <= T <= 50000)
思路:参见国家集训队论文:武森《浅谈信息学竞赛中的“0”和“1”》
1. 根据这个题目中介绍的这个矩阵中的数的特点不是 1 就是 0,这样我们只需记录每个格子改变过几次,即可判断这个格子的数字。
2. 先考虑一维的情况:
若要修改[x,y]区间的值,其实可以先只修改 x 和 y+1 这两个点的值(将这两个点的值加1)。查询k点的值时,其修改次数即为 sum(cnt[1] + … + cnt[k])。
3. 二维的情况:
道理同一维。要修改范围[x1, y1, x2, y2],只需修改这四个点:(x1,y1), (x1,y2+1), (x2+1,y1), (x2+1,y2+1)。查询点(x,y)的值时,其修改次数为 sum(cnt[1, 1, x, y])。
4. 而区间求和,便可用树状数组来实现。
const int MAX = 1010;
int n, cnt[MAX][MAX];
int lowbit(int x){
return x & (-x);
}
void add(int x, int y, int val){
for(int i = x; i <= n; i += lowbit(i))
for(int j = y; j <= n; j += lowbit(j))
cnt[i][j] += val;
}
int sum(int x, int y){
int res = 0;
for(int i = x; i > 0; i -= lowbit(i))
for(int j = y; j > 0; j -= lowbit(j))
res += cnt[i][j];
return res;
}
int main()
{
int t, m, x1, y1, x2, y2;
char op[10];
cin >> t;
while(t--){
scanf("%d%d", &n, &m);
memset(cnt, 0, sizeof(cnt));
while(m--){
scanf("%s%d%d", op, &x1, &y1);
if(op[0] == 'C'){
scanf("%d%d", &x2, &y2);
add(x1, y1, 1);
add(x1, y2+1, 1);
add(x2+1, y1, 1);
add(x2+1, y2+1, 1);
}else{
printf("%d\n", sum(x1,y1) % 2);
}
}
printf("\n");
}
}
树状数组到这里就结束了。
祝大家分区赛加油,也祝我努力去打好酱油!!!!!!