树状数组学习+题集

lowbit函数:x&-x

  • 思想:lowbit函数找的是二进制最右边的1。计算机机器语言中,-x实际上是x的补码,补码是x取反加1
  • 举个例子 x = 00001100,  -x = 11110011 + 1 = 11110100。通过按位取余这样就可以愉快的找到最右边的1了。思想和巧妙,看看例子应该好理解。
  • 找到最右边的1有什么用?x+lowbit(x)可以使最后一位1向前进。还是有点晕吧。看看下面的例子就明白了。

最经典的栗子:

  • C1 = A1
  • C2 = A1 + A2
  • C3 = A3
  • C4 = A1 + A2 + A3 + A4
  • C5 = A5
  • C6 = A5 + A6
  • C7 = A7
  • C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
  • 设节点编号为x,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素。

举个例子,有几个数可以通过lowbit产生0100呢?0001--lowbit-->0010-->lowbit-->0011-->lowbit-->0100,有三个数可以通过lowbit产生0100,加上它本身,总共维护了2^2个数的和。比如5(0101)没有数可以通过lowbit产生,所以维护它本身一个。6(0110)可以有5(0101)lowbit产生,所以维护两个。

大家有没有发现要通过lowbit产生X1XX(X代表未知数),只能通过X0XX+lowbit产生,而后面的XX可以是01,10,11,2^k怎么得出来该明白了吧!以下就是add(i,x)函数了

void add(int i,int x){    //第i个位置加上x
	while(i <= n){
		d[i] += x;
		i += lowbit(i);
	}
}

我们可以看到每个以Ci维护的只是一段区间和,那我们求前缀和只需找到对应的几个Ci即可。

比如我们要求前6(0110)个数的前缀和为C6 + C4。C6维护的是a[5]+a[6]。现在只需x -= lowbit(x)。0110-0010=0100。而0100永远也不可能lowbit到0110(上面说过了),所以0110和0100维护的区间是完全没有关系的。0100维护的是a[1]+a[2]+a[3]+a[4]。正好找到前缀和。

为什么这样可行呢?0110后面维护2^1个数,0100维护的是2^2个数。加起来正好是6 = 0010 + 0100。再比如11100 = 00100 + 01000 + 10000,而这些数都可以通过lowbit产生,通过维护的个数和,我们很容易能够验证这样的方法可行的。一下是求和函数。

int sum(int i){
	int s = 0;
	while(i > 0){
		s += d[i];
		i -= lowbit(i);
	}
	return s;
}

单点更新单点查询

luogu3374

代码:

#include <cstdio>
using namespace std;
typedef long long ll;
int const N = 500000 + 10;
int d[N],n,m;
int lowbit(int x){return x&-x;}
ll sum(int i){	//区间询问
	ll sum = 0;
	while(i){
		sum += d[i];
		i -= lowbit(i);
	}
	return sum;
}

void add(int i,int x){    //构造线段树,在第i个位置上加x
	while(i<=n){
		d[i] += x;
		i += lowbit(i);
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		int a;	scanf("%d",&a);
		add(i,a); 
	}
	while(m--){
		int flag,x,y;
		scanf("%d%d%d",&flag,&x,&y);
		if(flag == 1)	add(x,y);
		else printf("%lld\n",sum(y) - sum(x-1));
	}

}

区间更新单点查询

luogu3368

题解:

  • 利用差分的思想,新建一个数组c[i] = a[i] - a[i-1],那么a[i] = c[i] + a[i-1]。
  • 利用迭代得:a[i] = c[i] + a[i-1] = c[i] + c[i-1] + a[i-2] = c[i] + c[i-1] + c[i-2] + c[1] + a[0]。所以树状数组维护c[i]即可
  • 如何更新区间[x,y]。直接add(x,val),add(y+1,-val)。
  • 可以这样理解:a[n]=c[1]+c[2]+……+c[n] = (a[1]-a[0])+(a[2]-a[1])+……+(a[n]-a[n-1]+val) = a[n]+val。只要在x处加val,区间[x,y]内的每一个数都加了val。而y+1处减val,那么y后面的又恢复正常。 

代码:

#include <bits/stdc++.h>
using namespace std;
int const N = 500000 + 10;
int n,m,d[N],a[N];
int lowbit(int x){return x&-x;}
void add(int i,int x){
	while(i <= n){
		d[i] += x;
		i += lowbit(i);
	}
}
int sum(int i){
	int s = 0;
	while(i > 0){
		s += d[i];
		i -= lowbit(i);
	}
	return s;
}
int main(){
	scanf("%d%d",&n,&m);
	int flag,k,x,y;
	for(int i=1;i<=n;i++)	scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)	add(i,a[i]-a[i-1]);
	for(int i=1;i<=m;i++){
		scanf("%d",&flag);
		if(flag == 1){
			scanf("%d%d%d",&x,&y,&k);
			add(x,k);		add(y+1,-k);
		}else{
			scanf("%d",&x);
			printf("%d\n",sum(x));
		}
	}
	return 0;
}

区间修改区间查询

Code1082

题解:

 

  • 区间查询,所以维护a[1]+a[2]+……+a[n]的前缀和

  • a[1]+a[2]+……+a[n] = c[1]+(c[1]+c[2])+(c[1]+c[2]+c[3])+……+(c[1]+c[2]+……+c[n]) = n*(c[1]+c[2]+……+c[n])-(0*c[1]+1*c[2]+2*c[3]+……+(n-1)*c[n])

  • 前面c[1]+c[2]+……+c[n]这一段的区间和可以维护,为了维护后面这一段,我们新建数组c2[i] = (i-1)*c[i]即可。

  • 每次c[i]修改量为val,那么c2[i]=(i-1)(c[i]+val) = (i-1)*c[i]+(i-1)*val。所以修改偏移量为(i-1)*val

代码:

#include <bits/stdc++.h>
using namespace std;
int const N = 200000 + 10;
typedef long long ll;
ll c1[N],c2[N],a[N];
int n,m,k;
ll lowbit(ll x){return x&-x;}
void add(ll c[N],ll i,ll x){
	while(i <= n){
		c[i] += x;
		i += lowbit(i);
	}
}
ll getsum(ll c[N],ll i){
	ll s = 0;
	while(i > 0){
		s += c[i];
		i -= lowbit(i);
	}
	return s;
}
int main(){
	ll x,y,v;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)	scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++)	add(c1,i,a[i]-a[i-1]),	add(c2,i,(i-1)*(a[i]-a[i-1]));
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		scanf("%d%lld%lld",&k,&x,&y);
		if(k == 1)	scanf("%lld",&v),		add(c1,x,v),	add(c1,y+1,-v),		add(c2,x,v*(x-1)),	add(c2,y+1,-y*v);
		else	printf("%lld\n",(y*getsum(c1,y)-getsum(c2,y)) - ((x-1)*getsum(c1,x-1)-getsum(c2,x-1)));
	}
}

loj10114

题解:

左右括号的方法。在一个区间内种树,相当于加一对括号。用树状数组维护从起始到这个位置的左右括号的数量。区间内有左括号那么一定有这一种类型的树,只有离开了对应的右括号这种树才没有了。所以为了统计区间[x,y]内的树种类,只需把y左边左括号的个数-(x-1)左边右括号的个数即可。

代码:

#include <bits/stdc++.h>
using namespace std;
int const N = 50000 + 10;
int c1[N],c2[N];
int n,m;
int lowbit(int x){return x&-x;}
void add(int c[N],int i){
	while(i <= n){
		c[i]++;
		i += lowbit(i);
	}
}
int getsum(int c[N],int i){
	int s = 0;
	while(i > 0){
		s += c[i];
		i -= lowbit(i);
	}
	return s;
}
int main(){
	int k,x,y;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&k,&x,&y);
		if(k == 1){
			add(c1,x),	add(c2,y);   //c1记录左括号的个数,c2记录右括号的个数
		}else{
			printf("%d\n",getsum(c1,y) - getsum(c2,x-1));
		}
	}
}

POJ2155

题解:

  • 这是一道二维树状数组模板题。二维和一维套路是一样的。看看代码就明白了。
  • 翻转一个区间相当于区间的每个数加1,最后是0还是1就模2即可。
  • 树状数组维护区间和,(x1,y1)(x2+1,y2+1)(x1,y2+1) (x2+1,y1)都要加1。画一个矩形框框选一下就明白了。懒得画图

代码:

#include <cstring>
#include <algorithm>
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
int const N = 1000 + 10;
int n,m,c[N][N];
int lowbit(int x){return x&-x;}
void add(int x,int y){
	for(int i=x;i<=n;i+=lowbit(i))
		for(int j=y;j<=n;j+=lowbit(j))
			c[i][j]++;
}
int sum(int x,int y){
	int s = 0;
	for(int i=x;i>0;i-=lowbit(i))
		for(int j=y;j>0;j-=lowbit(j))
			s += c[i][j];
	return s;
}
int main(){
	int T;
	scanf("%d",&T);
	bool first = true;;
	while(T--){
		if(first)	first = false;
		else	printf("\n");
		memset(c,0,sizeof(c));
		scanf("%d%d",&n,&m);
		for(int i=1;i<=m;i++){
			char p;
			scanf(" %c",&p);
			if(p == 'C'){
				int x1,y1,x2,y2;
				scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
				add(x1,y1);
				add(x2+1,y1);
				add(x1,y2+1);
				add(x2+1,y2+1);
			}else{
				int x,y;
				scanf("%d%d",&x,&y);
				printf("%d\n",sum(x,y)%2);
			}
		}
	}
	return 0;
}

POJ2299

题解:

这是一道树状数组求逆序数的经典题目。注意要离散化,最后要开long long。

代码

#include <cstring>
#include <algorithm>
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
typedef long long ll;
int const N = 500000 + 10;
int a[N],n,d[N];
ll ans;
vector<int>v;
int lowbit(int x){return x&-x;}
void add(int i,int x){
	while(i <= n){
		d[i] += x;
		i += lowbit(i);
	}
}
int sum(int i){
	int s = 0;
	while(i){
		s += d[i];
		i -=lowbit(i);
	}
	return s;
}
int main(){
	while(cin>>n&&n){
		v.clear();
		for(int i=1;i<=n;i++){
			scanf("%d",&a[i]);
			v.push_back(a[i]);
		}
		sort(v.begin(),v.end());
		v.erase(unique(v.begin(),v.end()),v.end());
		memset(d,0,sizeof(d));
		ans = 0;
		for(int i=1;i<=n;i++){
			int k = lower_bound(v.begin(),v.end(),a[i]) - v.begin();
			ans += (i-1-sum(++k));
			add(k,1);
		}
		printf("%lld\n",ans);
	}
	return 0;
}

POJ3067

题解:

又是一道求逆序数的题目。先排序,x升序,x相同y升序。注意要long long,数据范围不准确。

代码:

#include <cstring>
#include <algorithm>
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
int const N = 1000 + 10;
typedef long long ll;
int n,m,k,d[N];
ll ans;
struct Road
{
	int x,y;
	bool operator < (const Road &e) const{
		return e.x == x ? y < e.y : x < e.x;
	}
}road[N*N];
int lowbit(int x){return x&-x;}
void add(int i,int x){
	while(i <= m){
		d[i] += x;
		i += lowbit(i);
	}
}
int sum(int i){
	int s = 0;
	while(i){
		s += d[i];
		i -= lowbit(i);
	}
	return s;
}
int main(){
	int T;
	scanf("%d",&T);
	int caser = 0;
	while(T--){
		scanf("%d%d%d",&n,&m,&k);
		for(int i=0;i<k;i++)	scanf("%d%d",&road[i].x,&road[i].y);
		sort(road,road+k);
		memset(d,0,sizeof(d));
		ans = 0;
		for(int i=0;i<k;i++){
			ans += (i - sum(road[i].y));
			add(road[i].y,1);
		}
		printf("Test case %d: %lld\n",++caser,ans);
	}
	return 0;
}

POJ2352

题解:

又是一道求逆序数的题目。题目顺序都处理好了。注意向右平移一个单位,因为0号位置也有star

代码:

#include <bits/stdc++.h>
using namespace std;
int const N = 15000 + 10;
int const M = 32000 + 10;
int n,d[M],num[N];
int lowbit(int x){return x&-x;}
void add(int i,int x){
	while(i <= M){
		d[i] += x;
		i += lowbit(i);
	}
}
int sum(int i){
	int s = 0;
	while(i > 0){
		s += d[i];
		i -= lowbit(i);
	}
	return s;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){    //0好位置有
		int x,y;
		scanf("%d%d",&x,&y);
		++x;
		num[sum(x)]++;
		add(x,1);
	}
	for(int i=0;i<=n-1;i++)	printf("%d\n",num[i]);
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值