树状数组

课前知识

lowbit

l o w b i t lowbit lowbit运算即计算数字在二进制情况下,从最小位到最高位,找到的第一个1时的数。(即取出最高位的1)

负整数的补码,将其原码除符号位外的所有位取反(0变1,1变0,符号位为1不变)后加1

C O D E : CODE: CODE

int lowbit(int x){
	return x&(-x);
}
/*
while(x>0){//输出分区间的始末
		printf("[%d,%d]\n",x-(x&-x)+1,x);
		x-=x&-x;
}
*/

因为其中二进制以补码的形式储存,所以 x x x& ( − x ) (-x) (x)所得即为连续0的个数。

树状数组

基本概念

对一个较大的线性区间,将其按照 2 2 2的次幂的形式分解成若干个小块,进行预处理或计算。
依据 任意正整数关于 2 2 2 的唯一分解性质 可得:

若设整数 x x x
则用二进制可表示为 a k − 1 a k − 2 a k − 3 … a 2 a 1 a 0 , ( 111000111 … … ) a_{k-1} a_{k-2}a_{k-3}…a_{2}a_{1}a_{0},(111000111……) ak1ak2ak3a2a1a0(111000111)
其中设为 1 1 1的位为{ a i 1 , a i 2 , a i 3 , … , a i m a_{i_1},a_{i_2},a_{i_3},…,a{i_m} ai1,ai2,ai3,,aim}。
可得:

  1. 长度为 2 i 1 2^{i_1} 2i1的区间为[ 1 , 2 i 1 1,2^{i_1} 1,2i1]。
  2. 长度为 2 i 2 2^{i_2} 2i2的区间为[ 2 i 1 + 1 , 2 i 1 + 2 i 2 2^{i_1}+1,2^{i_1}+2^{i_2} 2i1+1,2i1+2i2]。
  3. 长度为 2 i 3 2^{i_3} 2i3的区间为[ 2 i 1 + 2 i 2 + 1 , 2 i 1 + 2 i 2 + 2 i 3 2^{i_1}+2^{i_2}+1,2^{i_1}+2^{i_2}+2^{i_3} 2i1+2i2+1,2i1+2i2+2i3]。
    ……

由此可知:
若所需区间结尾为 R R R,则区间长度为”二进制分解“下最小的2的次幂即 l o w b i t ( R ) lowbit(R) lowbit(R)
E g : R = 7 = 2 2 + 2 1 + 2 0 Eg: R=7=2^2+2^1+2^0 Eg:R=7=22+21+20即区间 [ 1 , 7 ] [1,7] [1,7]可分解为 [ 1 , 4 ] , [ 5 , 6 ] , [ 7 , 7 ] [1,4],[5,6],[7,7] [1,4],[5,6],[7,7]

树状数组基于以上概念建立。

如图:在这里插入图片描述

  • 由上图所示树状数组只需开到N

于是乎主要的两种操作如下:

区间查询

int ASK(int x){
	int ans=0;
	for(;x;x-=(x&-x))
		ans+=c[x];
	return ans;
}

[ l , r ] [l,r] [l,r]之间的内容则只需 A S K ( r ) − A S K ( l − 1 ) ASK(r)-ASK(l-1) ASK(r)ASK(l1)

单点更新

void ADD(int x,int y){
	for(;x<=n;x+=(x&-x)){
		c[x]+=y;// 由x点依次向上传递至c[n]。
	}
}

类似于从每个“叶结点"到“根结点”依次累加的前缀和。

例题

1.逆序对[洛谷]

[PS:本题可以用其他多种算法解决]
题目描述

逆序对是这样定义的:对于给定的一段正整数序列,逆序对就是列就是序列中 a i > a j ai>aj ai>aj i < j i<j i<j的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。

输入

第一行,一个数 n n n,表示序列中有 n n n个数。 第二行, n n n个数,表示给定的序列。序列中每个数字不超过 1 0 9 10^9 109

输出

输出序列中逆序对的数目。

题目解析

  1. a a a的数值范围上建立树状数组 t t t
  2. 由后向前倒序扫描,对于 a [ i ] a[i] a[i]:
    1. 从树状数组 t t t中查询前缀和 [ 1 , a [ i ] − 1 ] [1,a[i]-1] [1,a[i]1],累加至 a n s ans ans
    2. 倒序同时进行 a d d add add操作,即 t [ a [ i ] ] t[a[i]] t[a[i]]++
  3. 输出 a n s ans ans

针对题目设定树状数组 t [ v a l ] t[val] t[val]保存数值 v a l val val在集合中出现的次数。于是 t t t上的区间和即为(已读入的)集合 a a a中范围在 [ l , r ] [l,r] [l,r]内的数的数量 ( [ 1 , a [ i ] ] [1,a[i]] [1,a[i]])

CODE:

#include<iostream>
#include<cstdio>
#include<algorithm> 
#include<cmath>
#define ll long long 
using namespace std;
const int maxn=500050;
ll lsh[maxn],cnt,a[maxn],n,t[maxn];
const int INF=1e9+1;
ll ans=0;
inline ll wr(){
	int x=0,f=1;char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
	return f*x;
}
void Add(ll x,int y){
	for(;x<=n;x+=(x&-x)){
		t[x]+=y;
	}
}
ll Ask(ll x){
	ll ans1=0;
	for(;x;x-=(x&-x))
		ans1+=t[x];
	return ans1;
}
int main(){
	n=wr();
//离散化***************************************************** 
	for(int i=1;i<=n;++i){
		a[i]=wr();
		lsh[i]=a[i];
	}
	sort(lsh+1,lsh+n+1);
	ll cnt=unique(lsh+1,lsh+n+1)-(lsh+1);
	for(int i=1;i<=n;++i)
		a[i]=lower_bound(lsh+1,lsh+cnt+1,a[i])-lsh;
//*********************************************************** 
	for(int i=n;i;i--){
		ans+=Ask(a[i]-1);	//查询数量 
		Add(a[i],1);		//在a[i]数值上定义的t上累加 
	}
	printf("%lld\n",ans);
	return 0;	
}

总结与反思:

  1. 十年OI一场空,不开 L o n g L o n g LongLong LongLong见祖宗(数据加强版)
  2. 始终注意Add函数的格式
  3. 记得在所建树状数组中添加的元素是什么

2.楼兰图腾[ACwing]

题目描述

传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V),一个部落崇拜铁锹(∧),他们分别用 V 和 ∧的形状来代表各自部落的图腾。西部 314 314 314在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 n n n个点,经测量发现这 n n n个点的水平位置和竖直位置是两两不同的。西部 314 314 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 n y_1∼y_n y1yn 1 1 1 n n n的一个排列。西部 314 314 314打算研究这幅壁画中包含着多少个图腾。 如果三个点 ( i , y i ) , ( j , y j ) , ( k , y k ) (i,y_i),(j,y_j),(k,y_k) (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 y_i>y_j,y_j<y_k yi>yj,yj<yk,则称这三个点构成V图腾;如果三个点 ( i , y i ) , ( j , y j ) , ( k , y k ) (i,y_i),(j,y_j),(k,y_k) (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 y_i<y_j,y_j>y_k yi<yj,yj>yk,则称这三个点构成 ∧ ∧ 图腾;西部 314 314 314想知道,这 n n n个点中两个部落图腾的数目。因此,你需要编写一个程序来求出 V 的个数和 ∧ ∧ 的个数。

输入格式

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

输出格式

两个数,中间用空格隔开,依次为 V 的个数和 ∧ ∧ 的个数

题目解析
依据题目描述:

  1. 倒叙扫描序列 a a a,如上题求出 a [ i ] a[i] a[i]后大于其的数,记为 r i g h t [ i ] right[i] right[i]
  2. 接着正序扫描序列 a a a,如上题求出 a [ i ] a[i] a[i]前大于其的数,记为 l e f t [ i ] left[i] left[i]
  3. 依次枚举各个点为中间点,则左侧 l e f t [ i ] ( a < a [ i ] ) ∗ r i g h t [ i ] ( a [ i ] < a ) left[i](a<a[i])*right[i](a[i]<a) left[i](a<a[i])right[i](a[i]<a)即为" V V V"字图案的个数
  4. 依据3的方法可得"^"字图案的个数。

CODE:

#include<iostream>
#include<cstdio>
#include<algorithm> 
#include<cmath>
#include<cstring> 
#define maxn 200010
#define INF 0x7ffffffff
#define ll long long 
using namespace std;
ll a[maxn],left1[maxn],right1[maxn],t[maxn];
ll ansv=0,ansx=0,n;
inline ll wr(){
	int x=0,f=1;char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
	return f*x;
}
void Add(ll x,int y){
	for(;x<=n;x+=(x&-x))
		t[x]+=y;
}
ll ask(ll x){
	int ans=0;
	for(;x;x-=(x&-x))
		ans+=t[x];
	return ans;
}
int main(){
	n=wr();
	for(int i=1;i<=n;++i)	a[i]=wr();
	for(int i=1; i<=n;++i){
        	ll y=a[i];
		left1[i]=ask(y-1);
		right1[i]=ask(n)-ask(y);
		Add(y,1);
   	}
   	memset(t,0,sizeof(t));
	for(int i=n;i>=1;--i){
        	ll y=a[i];
		ansv+=right1[i]*(ask(n)-ask(y));
        	ansx+=left1[i]*ask(y-1);
        	Add(y,1);
    	}
	printf("%lld %lld\n",ansv,ansx);
	return 0;	
}

总结与反思:

  1. l o n g l o n g long long longlong的转化尽量避免。
  2. ‘ v ’ ‘v’ v与‘^’的计数转换要搞清楚。

树状数组(下)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值