树状数组

(作用)能够同时实现:

  • 查询区间 O ( l o g N ) O(logN) O(logN)
  • 修改单点 O ( l o g N ) O(logN) O(logN)
  • 修改区间
  • 查询单点

实现原理:

  • 在使用使用二进制来表示一个数时:
    假如有一个数为x,那么就可以将其表示为: 2 i k + 2 i k − 1 + . . . . . . + 2 i 1 2^{i_k} + 2^{i_{k-1}} + ...... + 2^{i_1} 2ik+2ik1+......+2i1,其中, i k > = i k − 1 > = . . . > = i 1 i_k >= i_{k-1} >= ... >= i_1 ik>=ik1>=...>=i1 k < = l o g x k <= logx k<=logx

  • 假如要求 1 1 1 ~ x x x 的和,那么可以将其化为一些区间的和:
    s u m = sum = sum= ( x − 2 i 1 , x ] + ( x − 2 i 1 − 2 i 2 , x − 2 i 1 ] + . . . + ( 0 , x − 2 i 1 − 2 i 2 − . . . − 2 i k − 1 ] (x - 2^{i_1} , x] + (x - 2^{i_1} - 2^{i_2},x - 2^{i_1}] + ... + (0 , x - 2^{i_1} - 2^{i _2} - ... - 2^{i_{k-1}}] (x2i1,x]+(x2i12i2,x2i1]+...+(0,x2i12i2...2ik1]

    那么当务之急就是求出每个区间的和,观察得知:
    第一个区间包含 2 i 2^i 2i 个数,第二个区间包含 2 i 2 2^{i_2} 2i2 个数,…最后一个区间包含 2 i k 2^{i_k} 2ik 个数。

    所以: ( L , R ] (L,R] (L,R] 的长度一定是 R R R 的二进制表示的最后一位 1 1 1 所对应的次幂。
    c [ x ] = a [ x − l o w b i t ( x ) + 1 , x ] c[x] = a[x - lowbit(x) + 1 , x] c[x]=a[xlowbit(x)+1,x],那么这个区间的长度为 l o w b i t ( x ) lowbit(x) lowbit(x),右端点为 x x x。列出所有 c [ x ] c[x] c[x] 会得到类似于下图。
    在这里插入图片描述
    想要求出 C [ 16 ] C[16] C[16],那么只需要求出 C [ 8 ] + C [ 12 ] + C [ 14 ] + C [ 15 ] + a [ 16 ] C[8] +C[12] +C[14] + C[15] + a[16] C[8]+C[12]+C[14]+C[15]+a[16],其他同理。
    x x x 大于 0 0 0 时,假如 x = . . . . . . 1000......00 x = ......1000......00 x=......1000......00,其中最后有 k k k 0 0 0
    C [ x ] = a [ x ] + [ x − 2 k + 1 , x − 1 ] C[x] = a[x] + [x - 2^k + 1 , x - 1] C[x]=a[x]+[x2k+1,x1]。所以 x − 1 = . . . . . . 0111......11 x - 1 = ......0111......11 x1=......0111......11,所以当前 k k k 0 0 0 变为了 k k k 1 1 1,这时候我们要求 x − 1 x - 1 x1 就可以按照之前 k k k 个区间的划分的方法来求即求出:
    ( . . . . . . 0111...10 , . . . . . . 0111...11 ] + ( . . . . . . 0111......00 , . . . . . 0111...10 ] + . . . . . . + ( . . . . . . 0000....00 , . . . . . . 0100......00 ] (......0111...10 , ......0111...11] + (......0111......00 , .....0111...10] + ...... + (......0000....00 , ......0100......00] (......0111...10,......0111...11]+(......0111......00,.....0111...10]+......+(......0000....00,......0100......00]
    所以 C [ x ] = a [ x ] + C [ x − 1 ] + C [ x − 1 − l o w b i t ( x − 1 ) ] + C [ ( x − l o w b i t ( x − 1 ) ) − 1 − l o w b i t ( x − l o w b i t ( x − 1 ) ) ] + . . . . . . C[x] = a[x] + C[x-1] + C[x - 1 - lowbit(x - 1)] + C[(x - lowbit(x - 1) )- 1 - lowbit(x - lowbit(x - 1))] + ...... C[x]=a[x]+C[x1]+C[x1lowbit(x1)]+C[(xlowbit(x1))1lowbit(xlowbit(x1))]+......

  • 所以现在我们可以通过父节点找到子节点,比如X的子节点就是 x − 1 , x − 1 − l o w b i t ( x − 1 ) , ( x − l o w b i t ( x − 1 ) − 1 − l o w b i t ( x − l o w b i t ( x − 1 ) ) x - 1,x - 1 - lowbit(x - 1),(x - lowbit(x - 1) - 1 - lowbit(x - lowbit(x - 1)) x1x1lowbit(x1)(xlowbit(x1)1lowbit(xlowbit(x1)),那么如何通过子节点找到父节点?

    假如有一个父节点为 P = . . . . . . 1000....00 P = ......1000....00 P=......1000....00,其中后导 0 0 0 k k k 个,那么他的子节点一定满足 . . . . . . 0 ......0 ......0_ _ _ _ _,后面一定是有前面 a a a 1 1 1 和后面 b b b 0 0 0 组成,且 a + b = k a + b = k a+b=k,且从后往前数第 k + 1 k+1 k+1 个数一定是 0 0 0,那么反过来,满足这样形式的子节点,他的父节点一定就是从后往前数第 k + 1 k+1 k+1 位是 1 1 1,并且后面 k k k 位都是 0 0 0 的数注意,这里找到的父节点不是最根部的父节点,是此层子节点的直接上层,所以一个子节点能够找到的父节点一定是唯一的,这样就满足了树的定义,成了树状数组。

    所以就能够得到 x x x 的直接父节点就是 x + l o w b i t ( x ) x + lowbit(x) x+lowbit(x)

树状数组修改操作:

  • 如果想要让 a [ x ] + = d a[x] += d a[x]+=d (单点修改),那么就for(int i = x;i <= n;i += lowbit(i)) c[x] += d,即只需要修改子节点对应的所有父节点
  • 如果想要查询 1 1 1 ~ x x x (求前缀和)的和,那么就for(int i = x;i;i -= lowbit(i)) res += c[i];

例题

AcWing 241. 楼兰图腾 实现单点修改查询区间


在完成了分配任务之后,西部 314 来到了楼兰古城的西部。

相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V),一个部落崇拜铁锹(),他们分别用 V的形状来代表各自部落的图腾。

西部 314 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 n n n 个点,经测量发现这 n n n 个点的水平位置和竖直位置是两两不同的。

西部 314 认为这幅壁画所包含的信息与这 n n n 个点的相对位置有关,因此不妨设坐标分别为 ( 1 , y 1 ) , ( 2 , y 2 ) , … , ( n , y n ) (1,y_1),(2,y_2),…,(n,y_n) (1,y1),(2,y2),,(n,yn),其中 y 1 y_1 y1 y n y_n yn 1 1 1 n n n 的一个排列。

西部 314 打算研究这幅壁画中包含着多少个图腾。

如果三个点 ( i , y i ) , ( j , y j ) , ( k , y k ) (i,yi),(j,yj),(k,yk) (i,yi),(j,yj),(k,yk) 满足 1 ≤ i < j < k ≤ n 1≤i<j<k≤n 1i<j<kn y i > y j , y j < y k yi>yj,yj<yk yi>yj,yj<yk,则称这三个点构成 V图腾;

如果三个点 ( i , y i ) , ( j , y j ) , ( k , y k ) (i,yi),(j,yj),(k,yk) (i,yi),(j,yj),(k,yk) 满足 1 ≤ i < j < k ≤ n 1≤i<j<k≤n 1i<j<kn y i < y j , y j > y k yi<yj,yj>yk yi<yj,yj>yk,则称这三个点构成 图腾;

西部 314 想知道,这 n n n 个点中两个部落图腾的数目。

因此,你需要编写一个程序来求出 V的个数和 的个数。

输入格式
第一行一个数 n n n

第二行是 n n n 个数,分别代表 y 1 , y 2 , … , y n 。 y_1,y_2,…,y_n。 y1y2,,yn

输出格式
两个数,中间用空格隔开,依次为 V的个数和 的个数。

数据范围
对于所有数据, n ≤ 200000 n≤200000 n200000,且输出答案不会超过 i n t 64 int64 int64
y 1 y_1 y1 y n y_n yn 1 1 1 n n n 的一个排列。

输入样例:

5
1 5 3 2 4

输出样例:

3 4

假如我们要求有多少种尖端朝上的组合,那么可以找到一个点k,找到k左右两边比k大的值,然后两边相乘就是所有组合方式,有些类似于贡献法。

最影响理解此题中树状数组的一点:

  • 在本题中,树状数组存入的是每个数出现的次数,这时候如果你插入一个a[i],那么树状数组对应的tr[a[i]]++,同时他的父节点也进行了累加,注意这时候你在求右边或者左边的比当前数大或小的数的时候,树状数组维护了一个有序性,因为查询的时候依靠的是下标而并非插入的顺序,进行树状数组的询问操作时,由于我们之前在搜的时候向树状数组中加入了每次读到的一个数,那么就会实现我们可以搜在当前数前面并且还比当前数小的数。
  • 能保证搜的是当前数前面的数是因为我们才读到这个数,甚至还没有读到下一个数,能保证去查询到的是比这个数小的数的个数是因为树状数组保证了顺序性,比如我们搜样例中的第三个数之前比他小的数,去查询 3 − 1 3 - 1 31,也就是查询子节点 2 2 2 对应的数值,那么由于之前读入的只有 1 1 1 比他小,那么就会返回个数为 1 1 1

代码:

#include<iostream>
#include<cstring>
using namespace std;
const int N = 2e5 + 10;
#define ll long long

int n;
int a[N];//原数组
int tr[N];//树状数组,记录了每个数出现的次数
int bigger[N], lower[N];//greater[i]表示i前面多少数大于i,lower同理

int lowbit(int x) { return x & -x; }//lowbit模板

void add(int x, int c) {	//插入操作
	for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

int sum(int x) {			//查询(区间和)操作
	int res = 0;
	for (int i = x; i; i -= lowbit(i)) res += tr[i];
	return res;
}

int main() {
	cin >> n;

	for (int i = 1; i <= n; i++)scanf("%d",&a[i]);

	//首先从左到右扫,先求i的左边比i小的值和i的右边比i大的值
	for (int i = 1; i <= n; i++) {
		int y = a[i];

		bigger[i] = sum(n) - sum(y);
		lower[i] = sum(y - 1);

		add(y, 1);	//每次扫到一个值就把这个值在树状数组对应的下标处加1
	}

	//初始化树状数组,不然会影响之后另一部分扫描
	memset(tr, 0, sizeof tr);

	ll res1 = 0, res2 = 0;

	//再从右往左扫,求出i的右边比i大的值和i的左边比i小的值
	//并且同步加入答案
	for (int i = n; i; i--) {
		int y = a[i];

		res1 += bigger[i] * (ll)(sum(n) - sum(y));
		res2 += lower[i] * (ll)(sum(y - 1));

		add(y, 1);
	}

	cout << res1 << " " << res2 << endl;
	return 0;
}

AcWing 242. 一个简单的整数问题 实现修改区间查询单点


给定长度为 N N N 的数列 A A A,然后输入 M M M 行操作指令。

第一类指令形如 C l r d,表示把数列中第 l l l r r r 个数都加 d d d

第二类指令形如 Q x,表示询问数列中第 x x x 个数的值。

对于每个询问,输出一个整数表示答案。

输入格式
第一行包含两个整数 N N N M M M

第二行包含 N N N 个整数 A [ i ] A[i] A[i]

接下来 M M M 行表示 M M M 条指令,每条指令的格式如题目描述所示。

输出格式
对于每个询问,输出一个整数表示答案。

每个答案占一行。

数据范围
1 ≤ N , M ≤ 1 0 5 , 1≤N,M≤10^5, 1N,M105,
∣ d ∣ ≤ 10000 , |d|≤10000, d10000,
∣ A [ i ] ∣ ≤ 1 0 9 |A[i]|≤10^9 A[i]109

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
Q 4
Q 1
Q 2
C 1 6 3
Q 2

输出样例:

4
1
2
5

此题需要实现区间修改,并且要查询单点,这时候就可以将树状数组初始化为差分数组,并且将其当成差分数组进行操作,查询单点的时候也是求前缀和查询。

代码:

#include<iostream>
using namespace std;
const int N = 1e5 + 10;

int n, m;
int a[N];
int tr[N];

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

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

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

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> a[i];

	//树状数组初始化为差分数组
	for (int i = 1; i <= n; i++)add(i, a[i] - a[i - 1]);

	while (m--) {
		string op;
		int l, r, d;
		cin >> op >> l;

		if (op[0] == 'C') {
			cin >> r >> d;

			//差分模版,l处加1,r+1处减1
			add(l, d);
			add(r + 1, -d);
		}
		else {
			//就和差分一样,如果要查询就求前缀和
			cout << sum(l) << endl;
		}
	}

	return 0;
}

AcWing 243. 一个简单的整数问题2 实现修改区间查询区间

给定一个长度为 N N N 的数列 A A A,以及 M M M 条指令,每条指令可能是以下两种之一:

C l r d,表示把 A [ l ] , A [ l + 1 ] , … , A [ r ] A[l],A[l+1],…,A[r] A[l],A[l+1],,A[r] 都加上 d d d
Q l r,表示询问数列中第 l l l r r r 个数的和。对于每个询问,输出一个整数表示答案。

输入格式
第一行两个整数 N , M N,M N,M

第二行 N N N 个整数 A [ i ] A[i] A[i]

接下来 M M M 行表示 M M M 条指令,每条指令的格式如题目描述所示。

输出格式
对于每个询问,输出一个整数表示答案。

每个答案占一行。

数据范围
1 ≤ N , M ≤ 1 0 5 , 1≤N,M≤10^5, 1N,M105,
∣ d ∣ ≤ 10000 , |d|≤10000, d10000,
∣ A [ i ] ∣ ≤ 1 0 9 |A[i]|≤10^9 A[i]109
输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4

输出样例:

4
55
9
15

修改区间的部分只需要维护一个差分树状数组,而查询区间操作则需要进一步思考:
假如原数组为a[],而差分数组为b[],修改后的a[i]就是b[i]的前缀和,所以修改后的a[x]的前缀和就是 ∑ i = 1 x \sum\limits_{i = 1}^{x} i=1x ∑ j = 1 i b [ i ] \sum\limits_{j = 1}^{i} b[i] j=1ib[i],所以我们要求的就是以下的所有数的加和。
在这里插入图片描述
假如将其补全为下图
在这里插入图片描述
那么就可以得到a[x]的前缀和为 ( b 1 + b 2 + . . . . . . + b x ) ∗ ( x + 1 ) − ( b 1 − 2 b 2 − 3 b 3 − . . . . . . − x b x ) (b_1 + b_2 + ...... + b_x)*(x + 1) - (b_1 - 2b_2 - 3b_3 - ...... - xb_x) (b1+b2+......+bx)(x+1)(b12b23b3......xbx),可以表示为b[x]的前缀和乘 ( x + 1 ) (x + 1) (x+1) 减去i*b[i]的前缀和

所以我们可以维护两个树状数组,一个是b[i]a[]的差分数组,一个是i*b[i]a[]的差分数组但是每个元素都乘 i i i
当我们求 l l l ~ r r r 的和的时候,仅需要用 r r r 的前缀和减去 l − 1 l-1 l1 的前缀和

代码:

#include<iostream>
using namespace std;
const int N = 1e5 + 10;
#define ll long long

int n, m;
int a[N];
ll tr1[N];
ll tr2[N];

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

void add(ll tr[], int x, ll c) {
	for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

ll sum(ll tr[], int x) {
	ll res = 0;
	for (int i = x; i; i -= lowbit(i))res += tr[i];
	return res;
}

ll prefix(int x) {	//求前缀和(使用了推得的公式)
	return sum(tr1, x) * (x + 1) - sum(tr2, x);	
}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> a[i];

	for (int i = 1; i <= n; i++) {
		int b = a[i] - a[i - 1];

		//初始化为差分数组
		add(tr1, i, b);
		add(tr2, i, (ll)b * i);
	}

	while (m--) {
		string op;
		int l, r, d;
		cin >> op;
		cin >> l >> r;

		if (op[0] == 'Q') {

			//查找l ~ r区间的和
			cout << prefix(r) - prefix(l - 1) << endl;
		}
		else {	//修改操作
			cin >> d;

			//与差分数组操作相同
			add(tr1, l, d);
			add(tr2, l, d*l);

			add(tr1, r + 1, -d);
			add(tr2, r + 1, -d * (r + 1));
		}
	}

	return 0;
}

AcWing 244. 谜一样的牛 树状数组的应用

n n n 头奶牛,已知它们的身高为 1 1 1 n n n 且各不相同,但不知道每头奶牛的具体身高。

现在这 n n n 头奶牛站成一列,已知第 i i i 头牛前面有 A i A_i Ai 头牛比它低,求每头奶牛的身高。

输入格式
1 1 1 行:输入整数 n n n

2.. n 2..n 2..n 行:每行输入一个整数 A i A_i Ai,第 i i i 行表示第 i i i 头牛前面有 A i A_i Ai 头牛比它低。(注意:因为第 1 1 1 头牛前面没有牛,所以并没有将它列出)

输出格式
输出包含 n n n 行,每行输出一个整数表示牛的身高。

i i i 行输出第 i i i 头牛的身高。

数据范围
1 ≤ n ≤ 1 0 5 1≤n≤10^5 1n105

输入样例:

5
1
2
1
0

输出样例:

2
4
5
3
1

题目给出 2 2 2 ~ n n n 头牛的前面比它矮的牛的个数,那么很难从一开始就确定第一头牛是哪一个身高,所以可以尝试倒着推,这样就可以通过排除剩下一个数给了第一头牛。

假如给出的序列是 a [ 1 ] , a [ 2 ] , a [ 3 ] , . . . . . . , a [ i ] , . . . . . . , a [ n ] a[1] , a[2] , a[3] ,......, a[i] , ......, a[n] a[1],a[2],a[3],......,a[i],......,a[n](当然 a [ 1 ] a[1] a[1] 0 0 0) ,那么从最后一个开始看,如果第 n n n 头牛前面有 a [ n ] a[n] a[n] 头牛比它矮,所以他的高度是排名 a [ n ] + 1 a[n] + 1 a[n]+1 ,那么他的身高是没有遍历过的牛里面的第 a [ n ] + 1 a[n] + 1 a[n]+1 小的,所以就是 a [ n ] + 1 a[n] + 1 a[n]+1,到第 i i i 头牛,因为它后面遍历过的所有牛的身高已经确定,相应的高度已经被删除,所以如果前面有 a [ i ] a[i] a[i] 头牛比它矮,那么它的身高是没有遍历过的牛里面的第 a [ i ] + 1 a[i] + 1 a[i]+1 小的。假如这里不好理解的话可以去模拟一下样例。

所以该题目我们需要通过树状数组实现两个操作

  • 从剩余的数里面找出第 a [ i ] + 1 a[i] + 1 a[i]+1 小的数
  • 删除某一个数

首先可以使 a [ 1 ] a[1] a[1] ~ a [ n ] a[n] a[n] 全部初始化为 1 1 1,并且使用树状数组来维护他们的前缀和。这样可以通过修改单点来实现并判断对于某一个数的删除。

假如我们要从剩余的数里找出第 a [ i ] + 1 a[i] + 1 a[i]+1 小的数,就等价于找到一个最小的数 x x x,使得树状数组查询所得的 s u m ( x ) sum(x) sum(x) 等于 a [ i ] + 1 a[i] + 1 a[i]+1。原因是小于 a [ i ] + 1 a[i] + 1 a[i]+1 一定是不可用的,并且第一个使得 s u m ( x ) > = a [ i ] + 1 sum(x) >= a[i] + 1 sum(x)>=a[i]+1 x x x 一定就是答案,因为 i i i 前面有 a [ i ] a[i] a[i] 个比他矮的,因为是从后往前枚举,所以每次删掉一个数,剩下的数中第 a [ i ] + 1 a[i] + 1 a[i]+1 小的数一定就是答案,如果不选这个选了其他的就肯定会导致这个序列与身高冲突。

注意这里为什么要求前缀和:首先明确树状数组里面全是 1 1 1 0 , 1 0 ,1 0,1 代表了一个数还没有备选, 0 0 0 代表被选完了不能再选了,并且由于树状数组的特性,通过求它的前缀和求出来的是满足身高从低到高的能够选的数量的加和,也就是说如果 s u m ( x ) sum(x) sum(x) 满足了等于 a [ i ] + 1 a[i] + 1 a[i]+1,那么他就是第 a [ i ] + 1 a[i] + 1 a[i]+1 矮的高度。

#include<iostream>
using namespace std;
const int N = 1e5 + 10;

int n;
int a[N];	//存身高
int ans[N];	//存答案
int tr[N];	//树状数组,存某一个身高存不存在

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

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

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

int main() {
	cin >> n;
	for (int i = 2; i <= n; i++)cin >> a[i];

	//初始的时候是都存在的,所以全部设为1
	//如果对应的每个点都为1的话,那对应的第i个点区间的和就都为lowbit(i)
	for (int i = 1; i <= n; i++)tr[i] = lowbit(i);

	for (int i = n; i; i--) {
		int k = a[i] + 1;

		//二分来搜一个大于等于k的最小的身高
		//二分模板
		int l = 1, r = n;
		while (l < r) {
			int mid = l + r >> 1;
			if (sum(mid) >= k)r = mid;
			else l = mid + 1;
		}

		ans[i] = r;

		//此数已经被选了,在树状数组中删除
		add(r, -1);
	}
	for (int i = 1; i <= n; i++) {
		printf("%d\n",ans[i]);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值