课前知识
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……) ak−1ak−2ak−3…a2a1a0,(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}。
可得:
- 长度为 2 i 1 2^{i_1} 2i1的区间为[ 1 , 2 i 1 1,2^{i_1} 1,2i1]。
- 长度为 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]。
- 长度为
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(l−1)
单点更新
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。
输出
输出序列中逆序对的数目。
题目解析
- 在 a a a的数值范围上建立树状数组 t t t;
- 由后向前倒序扫描,对于
a
[
i
]
a[i]
a[i]:
- 从树状数组 t t t中查询前缀和 [ 1 , a [ i ] − 1 ] [1,a[i]-1] [1,a[i]−1],累加至 a n s ans ans中
- 倒序同时进行 a d d add add操作,即 t [ a [ i ] ] t[a[i]] t[a[i]]++
- 输出 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;
}
总结与反思:
- 十年OI一场空,不开 L o n g L o n g LongLong LongLong见祖宗(数据加强版)
- 始终注意Add函数的格式
- 记得在所建树状数组中添加的元素是什么
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 y1∼yn是 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 1≤i<j<k≤n且 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 1≤i<j<k≤n且 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 的个数和 ∧ ∧ ∧的个数
题目解析
依据题目描述:
- 倒叙扫描序列 a a a,如上题求出 a [ i ] a[i] a[i]后大于其的数,记为 r i g h t [ i ] right[i] right[i]。
- 接着正序扫描序列 a a a,如上题求出 a [ i ] a[i] a[i]前大于其的数,记为 l e f t [ i ] left[i] left[i]。
- 依次枚举各个点为中间点,则左侧 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"字图案的个数
- 依据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;
}
总结与反思:
- l o n g l o n g long long longlong的转化尽量避免。
- ‘ v ’ ‘v’ ‘v’与‘^’的计数转换要搞清楚。