树状数组

目录

树状数组

前缀和 引入

什么是树状数组 单点修改 区间查询

lowbit

lowbit有何用

逆序对与顺序对 提升

什么是差分 区间修改 单点查询

差分法的作用

区间修改 区间查询

多维树状数组


树状数组

前缀和 引入

我们有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,这里自己要注意一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值