_树状数组_

1. 什么是树状数组?

树状数组是一个查询和修改复杂度都为 O ⁡ ( log ⁡ n ) \operatorname{O}(\log n) O(logn) 的数据结构。

看到这句话是不是想到了线段树?

是的!

但是,凡是可以使用树状数组解决的问题, 使用线段树一定可以解决, 但是线段树能够解决的问题树状数组未必能够解决。

哦,那还是用线段树吧……

然鹅:

  • 线段树的数组需要开 4 4 4 倍,树状数组只用 1 1 1 倍。

  • 树状数组代码量少,线段树写 1 1 1 题用树状数组可以写 2 2 2 题。

  • 真不错!

2. l o w b i t lowbit lowbit

lowbit(x) ⁡ \operatorname{lowbit(x)} lowbit(x) 就是求 x x x 最低位的 1 1 1

c c c 数组记录原数组的和:

c [ 1 ] = a [ 1 ] → c [ ( 1 ) 2 ] = a [ ( 1 ) 2 ] c[1]=a[1]\to c[(1)_2]=a[(1)_2] c[1]=a[1]c[(1)2]=a[(1)2]

c [ 2 ] = a [ 2 ] + a [ 1 ] → c [ ( 10 ) 2 ] = a [ ( 10 ) 2 ] + a [ ( 1 ) 2 ] c[2]=a[2]+a[1]\to c[(10)_2]=a[(10)_2]+a[(1)_2] c[2]=a[2]+a[1]c[(10)2]=a[(10)2]+a[(1)2]

c [ 3 ] = a [ 3 ] → c [ ( 11 ) 2 ] = a [ ( 11 ) 2 ] c[3]=a[3]\to c[(11)_2]=a[(11)_2] c[3]=a[3]c[(11)2]=a[(11)2]

c [ 4 ] = a [ 4 ] + a [ 3 ] + a [ 2 ] + a [ 1 ] → c [ ( 100 ) 2 ] = a [ ( 100 ) 2 ] + a [ ( 11 ) 2 ] + a [ ( 10 ) 2 ] + a [ ( 1 ) 2 ] c[4]=a[4]+a[3]+a[2]+a[1]\to c[(100)_2]=a[(100)_2]+a[(11)_2]+a[(10)_2]+a[(1)_2] c[4]=a[4]+a[3]+a[2]+a[1]c[(100)2]=a[(100)2]+a[(11)2]+a[(10)2]+a[(1)2]

c [ 5 ] = a [ 5 ] → c [ ( 101 ) 2 ] = a [ ( 101 ) 2 ] c[5]=a[5]\to c[(101)_2]=a[(101)_2] c[5]=a[5]c[(101)2]=a[(101)2]

c [ 6 ] = a [ 6 ] + a [ 5 ] → c [ ( 110 ) 2 ] = a [ ( 110 ) 2 ] + a [ ( 101 ) 2 ] c[6]=a[6]+a[5]\to c[(110)_2]=a[(110)_2]+a[(101)_2] c[6]=a[6]+a[5]c[(110)2]=a[(110)2]+a[(101)2]

c [ 7 ] = a [ 7 ] → c [ ( 111 ) 2 ] = a [ ( 111 ) 2 ] c[7]=a[7]\to c[(111)_2]=a[(111)_2] c[7]=a[7]c[(111)2]=a[(111)2]

更新:

a [ 1 ] a[1] a[1] 为例,要更新 a [ 1 ] , a [ 2 ] , a [ 4 ] a[1],a[2],a[4] a[1],a[2],a[4]

是每次加了二进制的低位 1 1 1

1 → 1 + 1 = 10 → 10 + 10 = 100 → 100 + 100 = 1000 > 111 1\to1+1=10\to10+10=100\to100+100=1000>111 11+1=1010+10=100100+100=1000>111(停止)

查询:

查询 ∑ i = 1 7 a [ i ] → \sum\limits_{i=1}^7{a[i]}\to i=17a[i] 查询 c [ 7 ] + c [ 6 ] + c [ 4 ] c[7]+c[6]+c[4] c[7]+c[6]+c[4]

是每次去掉了二进制中的低位 1 1 1

111 → 111 − 1 = 110 → 110 − 10 = 100 → 100 − 100 = 0 111\to111-1=110\to110-10=100\to100-100=0 1111111=11011010=100100100=0(停止)

l o w b i t lowbit lowbit 的实现 :

int lowbit(int x)
{
	return x & -x;
}

我们知道,一个数的负数就等于对这个数取反 + 1 +1 +1

以二进制数 11010 11010 11010 为例: 11010 11010 11010 取反为 00101 00101 00101,加 1 1 1 后为 00110 00110 00110,两者相与便是最低位的 1 1 1

3. 一维树状数组

1. 单点修改,区间查询

P3374 【模板】树状数组 1

  • 单点修改

由刚才所说可知:

void update(int x, int k) //x 为要更新的数,k 为要加上的值,n 为数组大小
{
	for (int i = x; i <= n; i += lowbit(i))
	{
		c[i] += k;
	}
}
  • 区间查询

其实也就是单点更新的逆操作。

要想求出 x ∼ y x\sim y xy 相当于 1 ∼ y 1\sim y 1y 减去 1 ∼ x − 1 1\sim x-1 1x1

int query(int x)
{
	int res = 0;
	for (int i = x; i; i -= lowbit(i))
	{
		res += c[i];
	}
	return res;
}

int range_query(int x, int y)
{
	return query(y) - query(x - 1);
}

  • A C    C o d e AC\;Code ACCode
#include <iostream>
#include <cstdio>
using namespace std;

const int MAXN = 5e5 + 5;

int n, m, op, x, y;
int c[MAXN];

int lowbit(int x)
{
	return x & -x;
}

void update(int x, int k)
{
	for (int i = x; i <= n; i += lowbit(i))
	{
		c[i] += k;
	}
}

int query(int x)
{
	int res = 0;
	for (int i = x; i; i -= lowbit(i))
	{
		res += c[i];
	}
	return res;
}

int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
	{
		int x;
		scanf("%d", &x);
		update(i, x);
	}
	while (m--)
	{
		scanf("%d%d%d", &op, &x, &y);
		if (op == 1)
		{
			update(x, y);
		}
		else
		{
			printf("%d\n", query(y) - query(x - 1));
		}
	}
	return 0;
}

2. 区间修改,单点查询

P3368 【模板】树状数组 2

  • 区间修改

这里用到了差分的思想

设原数组为 a a a,差分数组 d [ i ] = a [ i ] − a [ i − 1 ] d[i] = a[i] - a[i-1] d[i]=a[i]a[i1]

当给区间 [ l , r ] [l, r] [l,r] 加上 k k k 时, a [ l ] a[l] a[l] a [ l − 1 ] a[l-1] a[l1] 的差增加了 k k k a [ r + 1 ] a[r+1] a[r+1] a [ r ] a[r] a[r] 减少了 k k k,故只需将 d [ l ] d[l] d[l] 加上 k k k a [ r + 1 ] a[r+1] a[r+1] 减去 k k k 即可。

void update(int x, int k)
{
	for (int i = x; i <= n; i += lowbit(i))
	{
		c[i] += k;
	}
}

void range_update(int x, int y, int k)
{
	update(x, k);
	update(y + 1, -k);
}
  • A C    C o d e AC\;Code ACCode
#include <iostream>
#include <cstdio>
using namespace std;

const int MAXN = 5e5 + 5;

int n, m, now, last, op, x, y, k;
int c[MAXN];

int lowbit(int x)
{
	return x & -x;
}

void update(int x, int k)
{
	for (int i = x; i <= n; i += lowbit(i))
	{
		c[i] += k;
	}
}

void range_update(int x, int y, int k)
{
	update(x, k);
	update(y + 1, -k);
}

int query(int x)
{
	int res = 0;
	for (int i = x; i; i -= lowbit(i))
	{
		res += c[i];
	}
	return res;
}

int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
	{
		scanf("%d", &now);
		update(i, now - last);
		last = now;
	}
	while (m--)
	{
		scanf("%d%d", &op, &x);
		if (op == 1)
		{
			scanf("%d%d", &y, &k);
			range_update(x, y, k);
		}
		else
		{
			printf("%d\n", query(x));
		}
	}
	return 0;
}

3. 区间修改,区间查询

P3372 【模板】线段树 1

位置 x x x 的前缀和

∑ i = 1 x a [ i ] = ∑ i = 1 x ∑ j = 1 i d [ j ] \sum\limits_{i=1}^{x}a[i]=\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{i}d[j] i=1xa[i]=i=1xj=1id[j]

其中, d [ 1 ] d[1] d[1] 被用了 x x x 次, d [ 2 ] d[2] d[2] 被用了 x − 1 x-1 x1 … … …… 那么可以写出:

∑ i = 1 x ∑ j = 1 i d [ j ] = ∑ i = 1 x d [ i ] ⋅ ( x − i + 1 ) \sum\limits_{i=1}^{x}\sum\limits_{j=1}^{i}d[j]=\sum\limits_{i=1}^{x}d[i]\cdot(x-i+1) i=1xj=1id[j]=i=1xd[i](xi+1)

   = ( x + 1 ) ⋅ ∑ i = 1 x d [ i ] − ∑ i = 1 x d [ i ] ⋅ i \qquad\qquad\ \ =(x+1)\cdot\sum\limits_{i=1}^{x}d[i]-\sum\limits_{i=1}^{x}d[i]\cdot i   =(x+1)i=1xd[i]i=1xd[i]i

那么可以维护两个数组的前缀和:

一个数组是 c 1 [ i ] = d [ i ] c1[i]=d[i] c1[i]=d[i]

另一个数组是 c 2 [ i ] = d [ i ] ⋅ i c2[i]=d[i]\cdot i c2[i]=d[i]i

  • 区间修改

对于 c 1 c1 c1 数组的修改同上对 d d d 数组的修改。

对于 c 2 c2 c2 数组的修改也类似,我们给 c 2 [ l ] c2[l] c2[l] 加上 l ⋅ x l \cdot x lx,给 c 2 [ r + 1 ] c2[r + 1] c2[r+1] 减去 ( r + 1 ) ⋅ x (r + 1) \cdot x (r+1)x

void update(int x, int k)
{
	for (int i = x; i <= n; i += lowbit(i))
	{
		c1[i] += k;
		c2[i] += k * x;
	}
}

void range_update(int x, int y, int k)
{
	update(x, k);
	update(y + 1, -k);
}
  • 区间查询

位置 x x x 的前缀和即: ( x + 1 ) ⋅ t 1 (x+1)\cdot t1 (x+1)t1 数组中 x x x 的前缀和 − t 2 - t2 t2 数组中 x x x 的前缀和。

int query(int x)
{
	int res = 0;
	for (int i = x; i; i -= lowbit(i))
	{
		res += (x + 1) * c1[i] - c2[i];
	}
	return res;
}

int range_query(int x, int y)
{
	return query(y) - query(x - 1);
}
  • A C    C o d e AC\;Code ACCode
#include <iostream>
#include <cstdio>
#define int long long
using namespace std;

const int MAXN = 1e5 + 5;

int n, m, a, last, op, x, y, k;
int c1[MAXN], c2[MAXN];

int lowbit(int x)
{
	return x & -x;
}

void update(int x, int k)
{
	for (int i = x; i <= n; i += lowbit(i))
	{
		c1[i] += k;
		c2[i] += k * x;
	}
}

void range_update(int x, int y, int k)
{
	update(x, k);
	update(y + 1, -k);
}

int query(int x)
{
	int res = 0;
	for (int i = x; i; i -= lowbit(i))
	{
		res += (x + 1) * c1[i] - c2[i];
	}
	return res;
}

int range_query(int x, int y)
{
	return query(y) - query(x - 1);
}

signed main()
{
	scanf("%lld%lld", &n, &m);
	for (int i = 1; i <= n; i++)
	{
		scanf("%lld", &a);
		update(i, a - last);
		last = a;
	}
	while (m--)
	{
		scanf("%lld%lld%lld", &op, &x, &y);
		if (op == 1)
		{
			scanf("%lld", &k);
			range_update(x, y, k);
		}
		else
		{
			printf("%lld\n", range_query(x, y));
		}
	}
	return 0;
}

4. 二维树状数组

1.单点修改,区间查询

思路跟一维类似,只不过一层循环变两层

  • 区间查询

查询时要用到容斥原理

int query(int x, int y)
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i))
    {
        for (int j = y; j; j -= lowbit(j))
        {
            res += c[i][j];
        }
    }
    return res;
}

int range_query(int a, int b, int c, int d) //左上角 (a,b) 右下角 (c,d)
{
	return query(c, d) - query(c, b - 1) - query(a - 1, d) + query(a - 1, b - 1);
}

2. 区间修改,单点查询

需要用到二维差分。

  • 区间修改

令差分数组
d [ i ] [ j ] = a [ i ] [ j ] − a [ i − 1 ] [ j ] − a [ i ] [ j − 1 ] + a [ i − 1 ] [ j − 1 ] d[i][j]=a[i][j] - a[i-1][j] - a[i][j-1]+a[i-1][j-1] d[i][j]=a[i][j]a[i1][j]a[i][j1]+a[i1][j1]

当我们给一个 5 × 5 5\times5 5×5 的矩阵最中间的 3 × 3 3\times3 3×3 的矩阵加上 k k k 时,差分数组变成了这样:

0  0  0  0  0
0 +k  0  0 -k
0  0  0  0  0
0  0  0  0  0
0 -k  0  0 +k

这样修改差分数组,原数组就变成了:

0  0  0  0  0
0  k  k  k  0
0  k  k  k  0
0  k  k  k  0
0  0  0  0  0
void update(int x, int y, int k)
{
    for (int i = x; i <= n; i += lowbit(i))
    {
        for (int j = y; j <= m; j += lowbit(j))
        {
            c[i][j] += k;
        }
    }
}

void range_update(int a, int b, int c, int d, int k)
{
	update(a, b, k);
	update(a, d + 1, -k);
	update(c + 1, b, -k);
	update(c + 1, d + 1, k);
}

3. 区间修改,区间查询

最最最最 恶心 \color{White}\colorbox{White}{恶心} 重要的部分终于来啦!

下面这个式子表示的是点 ( x , y ) (x, y) (x,y) 的二维前缀和:

∑ i = 1 x ∑ j = 1 y ∑ k = 1 i ∑ h = 1 j d [ h ] [ k ] \sum\limits_{i=1}^{x}\sum\limits_{j=1}^{y}\sum\limits_{k=1}^{i}\sum\limits_{h=1}^{j}d[h][k] i=1xj=1yk=1ih=1jd[h][k]

d [ 1 ] [ 1 ] d[1][1] d[1][1] 出现了 x ⋅ y x\cdot y xy 次, d [ 1 ] [ 2 ] d[1][2] d[1][2] 出现了 x ⋅ ( y − 1 ) x \cdot (y-1) x(y1) … … d [ h ] [ k ] …… d[h][k] d[h][k] 出现了 ( x − h + 1 ) ⋅ ( y − k + 1 ) (x-h+1)\cdot(y-k+1) (xh+1)(yk+1) 次。

原式 = ∑ i = 1 x ∑ j = 1 y d [ i ] [ j ] ⋅ ( x + 1 − i ) ⋅ ( y + 1 − j ) =\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{y}d[i][j]\cdot(x+1-i)\cdot(y+1-j) =i=1xj=1yd[i][j](x+1i)(y+1j)

      = ( x + 1 ) ⋅ ( y + 1 ) ⋅ ∑ i = 1 x ∑ j = 1 y d [ i ] [ j ] − ( y + 1 ) ⋅ ∑ i = 1 x ∑ j = 1 y d [ i ] [ j ] ⋅ i − ( x + 1 ) ⋅ ∑ i = 1 x ∑ j = 1 y d [ i ] [ j ] ⋅ j + ∑ i = 1 x ∑ j = 1 y d [ i ] [ j ] ⋅ i ⋅ j \quad\ \ \ \,=(x+1)\cdot(y+1)\cdot\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{y}d[i][j]-(y+1)\cdot\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{y}d[i][j]\cdot i-(x+1)\cdot\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{y}d[i][j]\cdot j+\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{y}d[i][j]\cdot i\cdot j    =(x+1)(y+1)i=1xj=1yd[i][j](y+1)i=1xj=1yd[i][j]i(x+1)i=1xj=1yd[i][j]j+i=1xj=1yd[i][j]ij

我们要开 4 4 4 c c c 数组,分别维护:

d [ i ] [ j ] , d [ i ] [ j ] ⋅ i , d [ i ] [ j ] ⋅ j , d [ i ] [ j ] ⋅ i ⋅ j d[i][j],d[i][j]\cdot i,d[i][j]\cdot j,d[i][j]\cdot i\cdot j d[i][j],d[i][j]i,d[i][j]j,d[i][j]ij

void update(int x, int y, int k)
{
    for (int i = x; i <= n; i += lowbit(i))
    {
        for (int j = y; j <= m; j += lowbit(j))
        {
            c1[i][j] += k;
            c2[i][j] += k * x;
            c3[i][j] += k * y;
            c4[i][j] += k * x * y;
        }
    }
}
 
void range_update(int a, int b, int c, int d, int k)
{
    update(a, b, k);
    update(a, d + 1, -k);
    update(c + 1, b, -k);
    update(c + 1, d + 1, k);
}
 
int query(int x, int y)
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i))
    {
        for (int j = y; j; j -= lowbit(j))
        {
            res += (x + 1) * (y + 1) * c1[i][j] - (y + 1) * c2[i][j] - (x + 1) * c3[i][j] + c4[i][j];
        }
    }
    return res;
}
 
int range_query(int a, int b, int c, int d)
{
    return query(c, d) - query(a - 1, d) - query(c, b - 1) + query(a - 1, b - 1);
}

P4514 上帝造题的七分钟

A C    C o d e AC\;Code ACCode

#include <iostream>
#include <cstdio>
using namespace std;

const int MAXN = 2050;

int n, m;
int c1[MAXN][MAXN], c2[MAXN][MAXN], c3[MAXN][MAXN], c4[MAXN][MAXN];

int lowbit(int x)
{
	return x & -x;
}

void update(int x, int y, int k)
{
	for (int i = x; i <= n; i += lowbit(i))
	{
		for (int j = y; j <= m; j += lowbit(j))
		{
			c1[i][j] += k;
			c2[i][j] += y * k;
			c3[i][j] += x * k;
			c4[i][j] += x * y * k;
		}
	}
}

void range_update(int a, int b, int c, int d, int k)
{
	update(a, b, k);
	update(a, d + 1, -k);
	update(c + 1, b, -k);
	update(c + 1, d + 1, k);
}

int query(int x, int y)
{
	int res = 0;
	for (int i = x; i; i -= lowbit(i))
	{
		for (int j = y; j; j -= lowbit(j))
		{
			res += (x + 1) * (y + 1) * c1[i][j] - (x + 1) * c2[i][j] - (y + 1) * c3[i][j] + c4[i][j];
		}
	}
	return res;
}

int range_query(int a, int b, int c, int d)
{
	return query(c, d) - query(a - 1, d) - query(c, b - 1) + query(a - 1, b - 1);
}

int main()
{
	getchar();
	scanf("%d%d", &n, &m);
	char op;
	int a, b, c, d, k;
	while (~scanf("\n%c%d%d%d%d", &op, &a, &b, &c, &d))
	{
		if (op == 'L')
		{
			scanf("%d", &k);
			range_update(a, b, c, d, k);
		}
		else
		{
			printf("%d\n", range_query(a, b, c, d));
		}
	}
	return 0;
}

T H E    E N D \mathcal{THE\;END} THEEND

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值