树状数组题集

书本配套OJ
我校OJ
树状数组知识点:

  • 单点修改,区间求和,O(logN)。
  • 区间修改,区间求和,O(logN)。
  • 二维树状数组单点修改,子矩阵求和,时间复杂度O(logN * logN),空间O(N^2)。

与线段树相比功能比较单一,不够灵活,但胜在简短易写,可以用来打辅助。

注意事项

  • 树状数组修改的下标不能为0,因为 lowbit(0) = 0,这就死循环了。
  • 注意树状数组的空间,这是由值域来决定的,而不是定义域。

数列操作

题目描述
给定n个数列,规定有两种操作,一是修改某个元素,二是求子数列[a,b]的连续和。数列元素个数最多10万个,询问操作最多10万次。
解题思路
模板题。
代码示例

#include
using namespace std;
const int N = 1e5+10;
typedef long long ll;
int n,m; 
ll A[N],c[N];
void Add(int x,int y){
	/*把第x位置上的数+y*/
	for(;x <= n;x += x&-x) c[x] += y;
}
ll ask(int x){
	/*返回[1,x]元素和*/
	ll res = 0;
	for(;x;x -= x&-x) res += c[x];
	return res;
}
int main(){
	scanf("%d%d",&n,&m);
	int k,a,b;
	for(int i = 1;i <= n;i++) scanf("%lld",A+i);
	for(int i = 1;i <= n;i++) Add(i,A[i]);
	for(int i = 1;i <= m;i++){
		scanf("%d%d%d",&k,&a,&b);
		if(k) Add(a,b);
		else printf("%lld\n",ask(b)-ask(a-1));
	}
	return 0;
}

数星星 Stars

题意简述
给定 n 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。
(详细描述见原题面)

解题思路
由于y是升序的,我们只需要记录比当前x小的star有多少个即可得知其等级 x ,然后用num[x]表示等级为x的星星个数,最终答案就是num数组。
CDQ分治也可以写,都是先将一维给变的有序,再统计另一维度。
代码示例

#include
using namespace std;
const int N = 4e4+10;
int c[N],n;
int ans[N];
void Add(int x,int y){ //若x=0则死循环 
	for(;x < N;x += x&-x) c[x] += y;
}
int ask(int x){
	int res = 0;
	for(;x;x -= x&-x) res += c[x];
	return res;
}
int main(){
//	freopen("123.txt","r",stdin);
	scanf("%d",&n);
	for(int i = 1,x,y;i <= n;i++){
		scanf("%d%d",&x,&y);
		ans[ask(x+1)]++;//要注意x = 0这种情况 
		Add(x+1,1);
	}
	for(int i = 0;i < n;i++) printf("%d\n",ans[i]);
	return 0;
}

校门外的树

题意简述

原题来自:Vijos P1448
校门外有很多树,学校决定在某个时刻在某一段种上一种树,保证任一时刻不会出现两段相同种类的树,现有两种操作:

  • K=1,读入 l,r 表示在 l 到 r 之间种上一种树,每次操作种的树的种类都不同;
  • K=2,读入 l,r 表示询问 l 到 r 之间有多少种树。

解题思路
刚开始以为是区间修改的题,后来发现不是。这题利用了差分数组的思想。如果我们假设c0[ x ] 存放 [1, x] 的左端点数量,c1[]存放右端点数量;如果所求区间为[l , r],那么一共只可能有 6 种不同的线段:

  • 第一种是全在r右侧;
  • 第二种是左端点在 [l ,r] 内,右端点在r右侧;
  • 第三种是左右端点都在[l ,r] 内;
  • 第四种是右端点在[l ,r]内,左端点在 l 左侧;
  • 第五种是左右端点全在 l 左侧;
  • 第六种是左端点在 l 左侧,右端点在 r 右侧。

那么我们用 l 右侧所有的右端点的数量,减去 r 右侧左端点的数量,可以画图理解,简单。

代码示例

#include
using namespace std;
const int N = 1e5+10;
typedef long long ll;
int n,m; 
ll A[N],c[2][N];
void Add(int ty,int x,int y){
	/*把第x位置上的数+y*/
	for(;x < N;x += x&-x) c[ty][x] += y;
}
ll ask(int ty,int x){
	/*返回[1,x]元素和*/
	ll res = 0;
	for(;x;x -= x&-x) res += c[ty][x];
	return res;
}
int main(){
	scanf("%d%d",&n,&m);
	int k,a,b;
	for(int i = 1;i <= m;i++){
		scanf("%d%d%d",&k,&a,&b);
		if(k == 1){
			Add(0,a,1); Add(1,b,1);
		}else{
			/*分类讨论,所有线段共6种情况 */ 
			int t1 = ask(1,N-1) - ask(1,a-1); 
			int t2 = ask(0,N-1) - ask(0,b);
			printf("%d\n",t1-t2);
		} 
	}
	return 0;
}

清点人数

题意简述
NK 中学组织同学们去五云山寨参加社会实践活动,按惯例要乘坐火车去。由于 NK 中学的学生很多,在火车开之前必须清点好人数。
初始时,火车上没有学生。当同学们开始上火车时,年级主任从第一节车厢出发走到最后一节车厢,每节车厢随时都有可能有同学上下。年级主任走到第 m 节车厢时,他想知道前 m 节车厢上一共有多少学生,但是他没有调头往回走的习惯。也就是说每次当他提问时,m 总会比前一次大。
解题思路
也是模板题。
代码示例

#include
using namespace std;
const int N = 5e5+10;
int c[N] ,n,k;
void Add(int x,int y){
	for(;x < N;x += x&-x) c[x] += y;
}
int ask(int x){
	int res = 0;
	for(;x;x -= x&-x) res += c[x];
	return res;
}
int main(){
	//freopen("123.txt","r",stdin);
	char op[5];
	scanf("%d%d",&n,&k);
	for(int i = 1,x,y;i <= k;i++){
		scanf("%s",op);
		if(op[0] == 'A'){
			scanf("%d",&x); printf("%d\n",ask(x));
		}else if(op[0] == 'B'){
			scanf("%d%d",&x,&y); Add(x,y);
		}else{
			scanf("%d%d",&x,&y); Add(x,-y);
		}
	}
	return 0;
} 

简单题

题意简述
题目来源:CQOI 2006
有一个 n 个元素的数组,每个元素初始均为 0。有 m 条指令,要么让其中一段连续序列数字反转——0 变 1,1 变 0(操作 1),要么询问某个元素的值(操作 2)。

解题思路
一个位置如果反转偶数次,那么它还是0,否则是1,因此我们只需要统计每个位置被反转了几次即可,用到了树状数组区间修改单点查询。树状数组区间修改的推导就不写了,书上有。

代码示例

#include
using namespace std;
const int N = 1e5+10;
int c[2][N],n,m ,num[N];
int getInt() {
	int ans = 0;
	bool neg = false;
	char c = getchar();
	while (c!='-' && (c<'0' || c>'9')) c = getchar();
	if (c == '-') neg = true, c = getchar();
	while (c>='0' && c<='9')
		ans = ans*10 + c-'0', c = getchar();
	return neg ? -ans : ans; 
}
void Add(int ty,int x,int y){
	for(;x <= n;x += x&-x) c[ty][x] += y;
}
int ask(int ty,int x){
	int res = 0;
	for(;x;x -= x&-x) res += c[ty][x];
	return res;
}
int main(){
	//freopen("123.txt","r",stdin); 
	n = getInt(); m = getInt();
	for(int i = 1,x,y,z;i <= m;i++){
		z = getInt();
		if(z == 1){
			x = getInt(); y = getInt();
			Add(0,x,1); Add(0,y+1,-1);
			Add(1,x,x); Add(1,y+1,-y-1);
		}else {
			y = getInt(); x = y;
			int res = (y+1)*ask(0,y) - ask(1,y) - x*ask(0,x-1) + ask(1,x-1);
			printf("%d\n",res&1);
		}
	}
	return 0;
}

打鼹鼠

题意简述
这是一道模板题。
给出一个 n×m 的零矩阵 A,你需要完成如下操作:
1 x y k:表示元素 Ax,y自增 k;
2 a b c d:表示询问左上角为 (a,b),右下角为 (c,d) 的子矩阵内所有数的和。

解题思路
是二维树状数组模板题,但是空间卡的很紧,开到N = 5000就险过,开到N = (1<<12)+10就好了一点。
和vijos那个原题不同,数据不一样,范围也不一样,这题不会出现下标为0的情况。就注意一下二维更新和查询时,每次的y需要恢复原值即可,因此需要用个变量存放一下y初值。

代码示例

#include
using namespace std;
const int N = (1<<12)+10;
typedef long long ll;
ll c[N][N]; 
int n ,m;
int getInt() {
	int ans = 0;
	bool neg = false;
	char c = getchar();
	while (c!='-' && (c<'0' || c>'9')) c = getchar();
	if (c == '-') neg = true, c = getchar();
	while (c>='0' && c<='9')
		ans = ans*10 + c-'0', c = getchar();
	return neg ? -ans : ans; 
}
void Add(int x,int y,int z){
	//printf("%d %d %d\n",x,y,z);
	for(;x <= n;x += x&-x)
		for(int ty = y;ty <= m;ty += ty&-ty) c[x][ty] += z;
}
ll ask(int x,int y){
	ll res = 0;
	for(;x;x -= x&-x)
		for(int ty = y;ty;ty -= ty&-ty) res += c[x][ty];
	return res;
}
int main(){
	int a,b,c,d,x,y,k,op;
	//freopen("123.txt","r",stdin);
	n = getInt(); m = getInt();
	while(scanf("%d",&op) != EOF){
		if(op == 1){
			x = getInt(); y = getInt(); k = getInt();
			Add(x,y,k);
		}else{
			a = getInt(); b = getInt(); c = getInt();d = getInt();
			printf("%lld\n",ask(c,d)-ask(c,b-1)-ask(a-1,d)+ask(a-1,b-1));
		}
	}
	return 0;
}

你可能感兴趣的:(数据结构)