树状数组——高级数据结构

树状数组(1)

序言

树状数组与线段树都是为了解决一个问题就是:高效的查询和维护前缀和(区间和)。
前缀和就是给出一个数组和n,求出这个数组前n个数的和,区间和可以通过前缀和求出,例如[i,j]的区间和等于j的前缀和减去i的前缀和。

如果a数组是静态的那前缀和很简单,一次遍历就可以求出前缀和数组时间复杂度0(N),以后每次查询时间复杂度都是0(1)。
但如果数组是动态变化的,例如改变了a[k]的值那么k之后的前缀和都需要从新求。所以如果每次查询前数据都会有所改变那么查询一次的时间复杂度就是0(N)。

有两种数据结构可以高效地处理这个问题:树状数组、线段树。它们实现的两个功能:查询前缀和、修改元素值,复杂度都是O(logn)的。

树状数组

树状数组是一种利用二进制特征进行检索的一种树状结构。
在这里插入图片描述
如图中的右边的结构就是树状数组,设为tree[i]。
可见tree[1]=a1,tree[2]=tree[1]+a2,tree[3]=a3,tree[4]=tree[2]+tree[3]+a4…
有了这个结构就可以实现两个操作。
查询:即求前缀和sum,sum(1)=tree[1],sum(2)=tree[2],sum(3)=tree[2]+tree[3]。。。。。。sum(7)=tree[4]+tree[6]+tree[7]。图中的虚线就是运算sum(7)的过程,显然时间复杂度是0(logn)。这样就可以达到快速求出前缀和。
维护:tree[]数组本身维护也是高效的,当一个值修改时可以用0(logn)的时间维护数组。例如a3的值发生了改变,那么只用修改tree[3],tree[4],tree[8]。。等父节点。

有了方案,剩下的问题就在查询和维护是如何定位到应该求和的数。
查询
例如sum(7)=tree[7]+tree[6]+tree[4]。
观察发现:
7的二进制是111
6的二进制是110
4的二进制是100
可以看出每次去掉了最后一位的1。
维护
例如修改a3,需要修改tree[3],tree[4],tree[8]等
观察发现:
3的二进制是11
4的二进制是100
8的二进制是1000
可以看出每次都加上了最后一位的1。

所以关键就是如何找到一个数二进制的最后一个1。

lowbit(x)

lowbit(x)=x&-x,lowbit是运用了负数的补码表示,补码是原码的反码加一。例如6=00000110,-6=11111010,lowbit(6)=二进制(10)=4
在这里插入图片描述
令m=lowbit(x),tree[x]其实是把ax与其前面这m个数相加的和,例如tree[7]=a6+a7。

tree[]是通过lowbit()计算出的树状数组,它能够以二分的复杂度存储一个数列的数据。 具体地,tree[x]中储存的是[x−lowbit(x)+1,x]中每个数的和。
在这里插入图片描述

代码

#define lowbit(x)  ((x) & - (x))   
int tree[Maxn];
void update(int x, int d) {   //修改元素ax,  ax = ax + d
    while(x <= Maxn) {
        tree[x] += d;  
        x += lowbit(x); 
    }
}
int sum(int x) {           //返回值是前缀和:ans = a1 + ... + ax
    int ans = 0;
    while(x > 0){
        ans += tree[x];  
        x -= lowbit(x);
    }
    return ans;
}

代码使用方法:
初始化:主程序清除tree[],然后读取a1,a2,a3…an,用update函数一个一个的更新数组。代码不用建立a数组因为它已经隐含在tree之中了。
求前缀和:用sum函数计算。
修改数值:用update函数修改。

差分数组(2)

序言

上一节学习了树状数组,它可以用来解决高效地“单点修改”+“区间查询”。相对的“区间修改”+“单点查询”可以使用差分数组解决,且差分数组与树状数组结合可以更加的高效。也可以进一步解决“区间修改”+“区间查询”。

例题(涂气球)

问题描述:N个气球排成一排,从左到右依次编号为1, 2, 3 … N。每次给定2个整数L, R(L<= R),lele从气球L开始到气球R依次给每个气球涂一次颜色。但是N次以后lele已经忘记了第i个气球已经涂过几次颜色了,你能帮他算出每个气球被涂过几次颜色吗?
输入:每个测试实例第一行为一个整数N,(N <= 100000)。接下来的N行,每行包括2个整数L, R(1 <= L<= R<= N)。当N = 0,输入结束。
输出:每个测试实例输出一行,包括N个整数,第I个数代表第I个气球总共被涂色的次数。

定义一个数组a[i],用来储存气球i被涂刷的次数。
如果用暴力处理N次区间修改,是O(N2) 的。用树状数组,如果只是简单把区间[L,R] 内的每个数a[x] 用update()进行单点修改,复杂度更差,是O(N2logN) 的。下面把单点修改处理成区间修改,复杂度O(NlogN)。

下面不是操作数组a而是建立了一个“差分数组”D它的定义是:
D[k]=a[k]-a[k-1]即原数组相邻的两个数的差。
从定义中可推出:
a[k]=D[1]+D[2]…D[k]
从这两个式子可以看出差分是前缀和的逆运算,他把求a的单值转化为了求D的前缀和,前缀和正可以使用树状数组计算。

对于区间[L,R]的修改问题,对D做以下操作:
  (1)把D[L]加上d;
  (2)把D[R+1]减去d。
  然后用树状数组函数sum() 求前缀和sum[x]=D[1]+D[2]+…+D[x] ,有:
  (1)1≤x<L ,前缀和sum[x] 不变;
  (2)L≤x≤R,前缀和sum[x] 增加了d;
  (3)R<x≤N,前缀和sum[x] 不变,因为被D[R+1]中减去的d 抵消了。
  sum[x]的值与直接把[L,R]区间内每个数加上d得到的a[x]是相等的。这样,就利用树状数组高效地计算出了区间修改后的a[x] 。

#include<iostream>
#include<algorithm>
#define lowbit(x)  ((x) & - (x))   
using namespace std;
const int Maxn = 100010;
int tree[Maxn];
void update(int x, int d)
{   //修改元素ax,  ax = ax + d
	while (x <= Maxn) {
		tree[x] += d;
		x += lowbit(x);
	}
}
int sum(int x)
{           //返回值是前缀和:ans = a1 + ... + ax
	int ans = 0;
	while (x > 0) {
		ans += tree[x];
		x -= lowbit(x);
	}
	return ans;
}
int main()
{
	int n;
	while (cin >> n&&n)
	{
		memset(tree, 0, sizeof(tree));
		int l, r;
		for (int i = 0; i < n; i++)
		{
			cin >> l >> r;
			update(l, 1);
			update(r + 1, -1);
		}
		for (int i = 1; i <= n; i++)
		{
			cout << sum(i) << ' ';
		}
		cout << endl;
	}
	system("pause");
	return 0;
}

其实这道题可以只使用差分数组来做,而且对于这道题来说效率更高,主要原因是数组是修改全部完成后在进行的查询,而且查询了整个数组。这样树状数组适用于动态数组的优势就无法显现,查询整个数组直接从前到后遍历时间只需要0(N),树状数组主要用于求单点时效率要高于朴素查询。

不过,遇到“区间修改”这种题型,还是建议用树状数组来求解。原因是差分数组对“区间修改”是很高效的,但是对“单点查询”并不高效。即使只查询一个前缀和,差分数组仍然要计算所有的前缀和,复杂度O(n);而树状数组做一次前缀和计算是O(logn) 的。

区间修改 + 区间查询

前面两节分别解决了高效“单点修改+区间查询”和“区间修改+单点查询”,这章解决“区间修改+区间查询”地问题。

仅用一个树状数组不能解决,这时候可能会想到在此基础上加一个树状数组用来计算第一个树状数组的单点。但是如果仅仅将两个数组简单相加一个用来“区间修改”一个用来“区间查询”,合起来效率并不高,一次的总复杂度0(n2logn)

这两个树状数组需要紧密结合才能高效完成“区间修改 + 区间查询”,称为“二阶树状数组”,它也是“差分数组”概念和树状数组的结合。下面给出一个典型例题。

例题

线段树1 洛谷P3372
问题描述:已知一个数列,进行两种操作:(1)把某区间每个数加上d;(2)求某区间所有数的和。
输入:第一行包含两个整数 n,m,分别表示该数列数字的个数和操作的总个数。第二行包含n个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。接下来m行每行包含3或4个整数,表示一个操作,具体如下:
(1)1 L R d:将区间[L, R]内每个数加上d。
(2)2 L R:输出区间[L, R]内每个数的和。
输出:输出包含若干行整数,即为所有操作(2)的结果。
1≤n,m≤105 ,元素的值在[−263,263)内。

操作(1)是区间修改,操作(2)是区间查询。
首先sum(l,r)=sum(1,r)-sum(1,l)
定义一个差分数组D,他和原数组的关系D[k]=a[k]-a[k-1],有a[k]=D[1]+D[2]+D[3]…+D[k],下面推导区间和,看看有什么关系,如果有关系就可以用树状数组表示。

a1+a2+a3…+ak
=D1+(D1+D2)+(D1+D2+D3)…
=kD1+(k-1)D2+(k-2)D3+…Dk

在这里插入图片描述
最终求导出两个前缀和,可以用两个树状数组分别求和,一个实现Di的和,一个实现(i-1)Di的和。这样的话代码复杂度为0(mlogn)。

代码

#include<iostream>
#include<algorithm>
using namespace std;
#define lowbit(x)  ((x) & - (x))
#define ll long long
const int MAX = 1e5 + 10;

ll int tree[2][MAX];

void update(int flag,ll x,ll d)
{
	while (x <= MAX)
	{
		tree[flag][x] += d;
		x += lowbit(x);
	}
}

ll sum(int flag, int x)
{
	ll ans = 0;
	while (x > 0)
	{
		ans += tree[flag][x];
		x -= lowbit(x);
	}
	return ans;
}

int main()
{
	ll n, m;
	cin >> n >> m;
	ll old = 0, a;
	for (int i = 1; i <= n; i++)
	{
		cin >> a;
		update(0, i, a - old);
		update(1, i, (i - 1)*(a - old));
	}

	while (m--) {                     
		ll q, L, R, d;
		cin >> q;
		if (q == 1) {                   
			cin >> L >> R >> d;
			update(0, L, d);;
			update(0, R + 1, -d);
			update(1, L, d*(L - 1));
			update(1, R + 1, -d * R);
		}
		else {                      
			cin >> L >> R;
			cout << R * sum(0, R) - sum(1, R) - (L - 1)*sum(0, L - 1) + sum(1, L - 1);
		}
	}
	system("pause");
	return 0;
}

二维区间修改+区间查询

思路同一维一样,不一样的就是推导方程更加复杂,如下。
在这里插入图片描述

代码略。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值