目录
树状数组
前缀和 引入
我们有N个数,求这个数列A[L]到A[R]的和
1.我们可以循环暴力,简单好理解!
但我们如果有M次求和,那么这个时间复杂度为O(N*M)
一但数据大一点,这个暴力求解就会爆掉!
2.前缀和
我们可以在输入时预处理,用一个B数组来求前缀和,即B[i]为1到 i 这一串数字的和
那么我们求和时就直接B[R]-B[L-1]就可以拿到和了,只用O(1)的时间查询,时间复杂度为O(M)
但是,如果我们有K次修改,将其中一个数修改,那么之后的B数组都要修改,这样的话时间复杂度就变成了O(M*K)了
一旦数据大时,就又会爆掉!
3.树状数组
那什么是树状数组?
什么是树状数组 单点修改 区间查询
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值。
上面就是一个树状数组了!
为什么长这个鬼样?我们发现,C[3]只存了A[3]的值,C[6]只存了A[5]+A[6],而C[4]却存了A[1]+A[2]+A[3]+A[4],即C[2]+C[3]+A[4]
怎么建造和求出和呢?我们就要知道一个东西,lowbit!
lowbit
lowbit就是将一个数转化为2进制数,最低为的1表示的10进制就是这个数的lowbit
例如 lowbit(22),我们将22转化为2进制,10110
我们发现最后面的1在从左往右第4个,lowbit就是把最低为1前面的都砍掉,变成10
10的10进制就是2,所以lowbit(22)就是2了!
如何求lowbit?你说循环就太浪费时间了
有两种写法
1. x-(x&(x-1))
什么意思 &是按位与,将x转化为2进制后,一 一对应按位与,法则为 0&0=0 0&1=0 1&0=0 1&1=1
那为什么这样就可以了
就拿22为例子吧,10110,我们把最低为1前看作A,后面看作B,10110就看作A1B(注意这里的B全是0,因为在最低为1的后面)
那么B后面肯定全部是0
x-1后B是不是全部变成了1 ,x-1的2进制就是A0B(注意这里的B全是1)了,我们把这个B看作C吧(好区分一点),A0C
A1B&A0C=(A&A)(1&0)(B&C)=A0B
A1B-A0B=最低为的1,对不对?
2. x & -x
-x的2进制是什么,其实这里要涉及到补码了(啊,什么是补码啊!)
22的2进制是10110 我们可以在前面加一个0 变成010110 那这个0有什么用啊,其实计算机把这个0当作判断符号的标准,0就是正数,1就是负数
那是不是 110110就是-22了呢,不是的,我们除了要把前面的符号0改成1,还要把后面的取反(啊,取反又是什么鬼)
就是0变成1,1变成0啦,(注意,取反符号为不变)
1 1 0 1 1 0
1 0 1 0 0 1
恩,那么101001是不是就是-22的二进制了呢,不是的,还要+1,就是101001+1=101010了 101010才是-22的2进制
也就是说,负数的二进制就是以正数前面符号0变成1,再把后面取反,再+1就行了
那 x & -x 怎么就可以求到lowbit了?
我们想一想,求负数的时候我们将它取反了,那么这个时候肯定&上全是0,而我们+1时,最后一位1后面的0全部变成1后,再+1是不是就进一位,恰好进到lowbit的位置,后面的1也会变成0,是不是就可以了?
lowbit有何用
其实lowbit就是这个数组的子节点
大家可以仔细对比一下,看一看是不是这样的,你看6的二进制110,lowbit为2,是不是6只有两个儿子(A[5]+A[6])
8的lowbit就是8,是不是就是A[1]+A[2]+......A[7]+A[8]?,那我们不可能用循环吧,我们是想要C[8]连着C[4],C[6],C[7],A[8]啊
那你要知道,不是C[8]去连,也就是说不是父亲去主动连儿子,而是儿子来主动连父亲。
那么儿子与父亲之间有什么关系?其实我们会发现,加上自己的lowbit,就是自己的父亲了
比如3 11+lowbit(3)=11+1=100,而100就是4,100+lowbit(4)=100+4(100)=1000(8)
这个可能需要大家自己去推理,儿子加自己的lowbit,就是自己父亲的位置了
那怎么求和,我们减去自己的lowbit得到的累积和就是了
比如求1-6的和,ANS+=C[6]+C[6-lowbit(6)] (C[4]) + C[4-lowbit(4)] (C[0]) (到了0就结束)=C[6]+C[4]
int lowbit(int x){
return x & -x;
}
void updata (int k,int x){//k是下标,x是修改后与原数的差,如果要小,那么x应是负数
for(int i = k; i <= n; i+=lowbit(i)){
c[i] += x;
}
}
int Sum(int l,int r){
int suml = 0, sumr = 0;
for(int i = r; i > 0; i -= lowbit(i)){
sumr += c[i];
}
for(int i = l; i > 0; i -= lowbit(i)){
suml += c[i];
}
return sumr - suml ;
}
逆序对与顺序对 提升
求逆序对(顺序对)
1.2路归并(用归并排序)(时间复杂度和树状数组一样)
2.树状数组
怎么求?
当前有N个数,前面有M个数比他小,那是不是有N-M个是比他大的,就是逆序对了?M就是顺序对
我们每次当然只加1了,来表示他前面比他小的个数
for(int i = 1; i <= n; i++){
scanf("%d",&a[i]);
updata(a[i], 1);
ans += i - sum(i);
}
大家不懂得话,可以再去理解理解的。
那顺序对就是Sum[i]了啊!对吧?ans+=Sum[i]
什么是差分 区间修改 单点查询
什么是差分?
就是每两个数之间的差嘛!
A 1 5 9 4 5
B 1 4 4 -5 1
B[i]=a[i]-a[i-1] 其中特殊的,A[0]=0
我们很容易发现 A[i]= ∑ j=1 i b[j]
你会说,直接拿A[i]不就行了嘛,但是我们要区间修改啊,你不可能去循环吧!
我们上面的树状数组解决的是单点修改,区间查询
而这里是区间修改,单点查询,所以我们的C数组维护的不再是A数组了,而是差分数组B
差分法的作用
我们按照这个例子
A 1 5 9 4 5
B 1 4 4 -5 1
如果我们要将1-4的区域都加上3
A 4 8 12 7 5
B 4 4 4 -5 -2
你发现没有,差分B数组只有B[1]和B[5]改变了,也就是说,只有l和r+1改变了,B[l]加x 而我们的B[r+1]要减去-x
中间不需要变化,最后差分数组的前缀和就是A[i]
好,我们亮一下代码吧!
lowbit就不用打了吧!
void updata(int k, int x){
for(int i = k; i <= n; i+=lowbit(i)){
c[i] += x;
}
}
int Sum(int x){//由于是单点查询,所以就不用两个数了
int tot = 0;
for(int i = x; i > 0; i-=lowbit(i)){
tot += c[i];
}
return tot;
}
void solve(){
for(int i = 1; i <= n; i++){
b[i] = a[i] - a[i-1];
updata(i, b[i]);
}
for(int i = 1; i <= m; i++){
scanf("%d%d%d",&l,&r,&x);
updata(l, x);
updata(r+1, -x);
}
}
区间修改 区间查询
这个可就要复杂一点了
我们不难想到 按照上面的思路
位置p的前缀和 =
∑i=1 p a[i] =( ∑ i=1 p )∑ j=1 i d[j]
哎呀,你说你看不懂,那就这样吧
for(int i = 1; i <= m; i++){
scanf("%d%d",&x,&y);//表示查询的区间
int tot = 0;
for(int j = x; j <= y; j++){
tot += Sum(j);
}
}
这个代码是在上面的基础上啊!
那你会说,这不太浪费时间了啊!
恩,我也觉得,但是我们可以变形
我们想想,我们在双重循环时,d[x]只用了y-x+1,而d[x+1]用了y-x次,d[x+2]用了y-x-1次,对不对,
哎呀,你不懂,就把x举例子看做1嘛,想过来没有?
我们就可以知道
那我们的树状数组就要维护两个前缀和
void updata(int k, ll x){
for(int i = k; i <= n; i +=lowbit(i))
sum1[i] += x, sum2[i] += x * p;
}
void range_add(ll l, ll r, ll x){
updata(l, x), updata(r + 1, -x);
}
int Sum(int x){
int tot = 0;
for(int i = x; i > 0; i -=lowbit(i))
tot += (x + 1) * sum1[i] - sum2[i];
return tot;
}
int range_Sum(int l, int r){
return Sum(r) - Sum(l - 1);
}
多维树状数组
什么啊!不要想得怎么难!
我们就直接来一个二维的代码
void updata(int x,int y,int z){
for(int i = x; i <= n; i+=lowbit(i)){
for(int j = y; j <= n; j+=lowbit(j)){
c[i][j] += z;
}
}
}
int Sum(int x,int y){
int tot = 0;
for(int i = x; i > 0; i -= lowbit(i)){
for(int j = y; j > 0; j -= lowbit(j)){
tot += c[i][j];
}
}
}
什么嘛,就是多了个循环嘛,其实也就是这样的,我可能说明的不好,所以你们就自己思考思考吧
当然,什么多维啊,就是多了个循环,区间查询,区间修改,单点查询,搬过来就行了。
上面的代码也许有错,因为没有返回tot,这里自己要注意一下。