树状数组

任务1:区查单改

模板:洛谷P3374

已知一个数列,你需要进行下面两种操作:

1.将某一个数加上x

2.求出某区间每一个数的和

初看此题,我们很容易想到暴力和前缀和。但暴力询问时间太长,前缀和修改时间太长,两者时间复杂度均为$O(nm)$,不能满足需求。

于是,一个$O(mlogn)$的算法横空出世...

树状数组的详解网上一大堆,我只谈谈我对树状数组的理解。

1.lowbit(x)的证明

$lowbit(x)$=$x$&$(-x)$

e.g:$lowbit(148) = 148$ & $(-148) = $10010100 & 01101100 $ = $ 00000100 $= 4$

$lowbit(x)$的值即一个二进制数中从低位到高位数的第一个1所表示的十进制值。

但是,为什么呢?

网上题解很少提及这个函数的证明,因为它确实特别玄学。

我的证明如下:

(下文中的“原来”或“之前”指原数,“现在”指取反~后)

我们知道,一个负数表示为二进制就是它的补码,补码就是 该负数的绝对值 表示为二进制后 按位取反 +1 的值 (~|x|+1)

e.g:$-148 = -($10010100$) = $~10010100$ + $1$ = $01101011 $+$ 1 $=$  01101100

生成补码后

一种情况是不发生任何进位,即 原来为1,现在为0 的最低位 +1 后变成了1,其他位只有取反操作。

再对原数和负数的补码 按位与&,

由于取反,除最低位的所有位运算的情况 不是0&1=0,就是1&0=0,

只有最低位原来是1,取反后+1还是1,1&1=1

所以这种情况下$lowbit(x)$恒为00000001,也就是1

e.g:

$lowbit(149)$

$= 149$ & $(-149)$

$= $10010101 & 01101011

$= $ 00000001

$= 1$

另一种情况则进位,而且可能连锁反应进很多次位。

最低位 原来为0,现在为1 ,+1后变为2,需向上进位,然后自身变回0,

如果上一位 之前为0,现在为1 ,收到了前一位的进位 +1 后变为2,又需要向上进位,然后自身变为0...

如此反复,直到遇到 之前为1,现在为0 的位,收到前一位进位后+1变成1,停止连锁反应。

我们把这一个位设为a,则补码出现了这样的情况:

比x高的位因为没有受到进位的影响,是原数取反的结果;

x由于取反后为0,又收到上一位进位,所以一定为1;

比x低的位由于取反后为1,又收到上一位进位,所以一定为0。

然后我们把原数和负数补码 按位与&,

比x高的位的情况不是0&1=0,就是1&0=0;

x则为1&1=1;

比x低的位则全为0,所以结果一定为0。

所以答案为0...010...0 ,1所在的位置便是x位,也是原数中从低位数第一个1。

e.g:

$lowbit(148)$
$=148$ & $(-148)$
$=$ 10010100 & $-($10010100$)$
$=$ 10010100 & $(~$10010100$ + $1$)$
$=$ 10010100 & 01101100
$=$ 00000100

综上所述,$lowbit(x)$后只剩下一位有1,而它所在的位即x,也是就从低位数到高位的第一个1。

(我自己都感觉写的太复杂了)╮(╯▽╰)╭

2.lowbit(x)的运用

既然$lowbit(x)$的值即一个二进制数中从低位数到高位的第一个1所表示的十进制值,也是节点x所管辖的节点个数。那么拿$x+lowbit(x)$必定发生一次最小的进位,$x-lowbit(x)$必定让从低位数到高位的第一个1变成0。而观察树状数组,发现一旦进位就多出一层包含关系,一旦变0就会退出一层所在的包含关系,那么就可以方便的通过$x+lowbit(x)$找到当前节点的父节点,通过$x-lowbit(x)$找到前面未包含的节点

 上代码:

一维:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#include<ctime>
using namespace std;
const int MXN=1000000,INF=999999999;

struct BIT{
    int lowbit(int x){return x&(-x);}
    int tr[MXN],n;
    void Init(int myn){n=myn;for(int i=0;i<=n;i++) tr[i]=0;}
    void Modify(int x,int val){
        for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=val;
    }
    int GetSum(int x){
        int ans=0;if(x==0) return 0;
        for(int i=x;i>=1;i-=lowbit(i)) ans+=tr[i];
        return ans;
    }
    int Query(int l,int r){
        return GetSum(r)-GetSum(l-1);
    }
}bit;
//这里以单点加值,区间查询为例
int n,qn;
int main(){
    cin>>n>>qn;
    bit.Init(n);
    for(int i=1;i<=n;i++){
        int t;scanf("%d",&t);
        bit.Modify(i,t);
    }
    for(int i=1;i<=qn;i++){
        int type,x,y;
        scanf("%d%d%d",&type,&x,&y);
        switch(type){
            case 1:
                bit.Modify(x,y);
                break;
            case 2:
                printf("%d
",bit.Query(x,y));
                break;
        }
    }
    return 0;
}
View Code(new)

二维:

其实就是多了一层循环...然后变更和查询的方式也变了,稍微想想就能够明白

没找到模板,就直接上代码了

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<ctime>
#include<cstdlib>
#include<queue>
//#include<windows.h>
using namespace std;

long long lowbit(long long val){
    return val&(-val);
}
struct BIT{
    long long sizex,sizey;
    long long tree[2005][2005];
    
    void init(long long mysx,long long mysy){
        sizex=mysx;
        sizey=mysy;
        for(long long i=0;i<2005;i++) 
            for(long long j=0;j<2005;j++)
                tree[i][j]=0;
    }
    void change(long long idx,long long idy,long long val){
        for(long long i=idx;i<=sizex;i+=lowbit(i))
            for(long long j=idy;j<=sizey;j+=lowbit(j))
                tree[i][j]+=val;
    }
    long long getsum(long long idx,long long idy){
        if(idx==0||idy==0) return 0;
        long long ans=0;
        for(long long i=idx;i>=1;i-=lowbit(i))
            for(long long j=idy;j>=1;j-=lowbit(j))
                ans+=tree[i][j];
        return ans;
    }
    long long ask(long long x1,long long y1,long long x2,long long y2){
        return getsum(x2,y2)-getsum(x1-1,y2)-getsum(x2,y1-1)+getsum(x1-1,y1-1);
    }
}bit;

int main(){
    return 0;
}
View Code

任务2:区改单查

模板:洛谷P3368

已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数数加上x

2.输出某一位置上的数

用到了差分的思想。

比如原数列a[]={1,5,4,2,3}

加一个数组b[],使b[i]=a[i]-a[i-1],即{1,4,-1,-2,1}

然后进行区间加值操作,如把下标2到下标5的数+3。

a[]变为{1,8,7,5,3},b[]变为{1,7,-1,-2,-2}

有没有发现只有b[2]和b[5]变了?

b[]只需一头一尾变化,因为区间内所有的数都加上了同样的值,差值不变。

所以把b[]这个差分数组用树状数组维护前缀和。

为什么维护前缀和?因为a[i]就等于b[1]+b[2]+...+b[i]

完美解决

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;

long long n,con;
long long e[500005]= {0};

long long lowbit(long long num) {
    return (-num)&num;
}

void change(int pos,long long val) {
    int now=pos;
    for(; now<=n; now+=lowbit(now)) {
        e[now]+=val;
    }
}
long long getans(int pos) {
    if(pos==0) {
        return 0;
    }
    long long sum=0;
    int now=pos;
    for(; now>=1; now-=lowbit(now)) {
        sum+=e[now];
    }
    return sum;
}

int main() {
    cin>>n>>con;
    int t1,t2=0;
    for(int i=1; i<=n; i++) {
        scanf("%lld",&t1);
        change(i,t1-t2);
        t2=t1;
    }
    for(int i=1; i<=con; i++) {
        long long type,x,y,z;
        scanf("%lld",&type);
        if(type==1) { //更改
            scanf("%lld%lld%lld",&x,&y,&z);
            change(x,z);
            change(y+1,-z);
        } else if(type==2) { //询问
            scanf("%lld",&x);
            printf("%lld
",getans(x));//差分数组前缀和即为答案 
        }
    }
    return 0;
}
View Code

任务3:区查区改

模板:洛谷P3372 线段树1

除非题目卡常,不推荐使用。好好的线段树不写为什么要用这个奇怪的玩意儿,(虽然常数小那么一点)

这用到了一些数学的推导

接着任务2的进度,我们把a[]差分到b[]后,只需要维护b[]的前缀和就可以搞到a[]中对应数的值

现在我们想求$a[1]$~$a[x]$的和,转化一下,就变成了

$a[1]+a[2]+...+a[x]$

$=b[1]$

$+b[1]+b[2]$

$+b[1]+b[2]+b[3]$

$+...$

$+b[1]+b[2]+...+b[x]$

再转化,

原式$=b[1]*x+b[2]*(x-1)+b[3]*(x-2)+...+b[x-1]*2+b[x]$

也就是

$large ∑(b[i]*(x+1-i))$

提取其中的不变量,得

 $large ∑(b[i]*(x+1-i))=(x+1)*∑b[i] - ∑(b[i]*i)$

好了,我们开2个树状数组,一个维护$b[i]$,另一个维护$b[i]*i$,完事

注意:两个维护b[i]的树状数组并不是差分维护的,它们每次只是修改了b[]里的2个值,b[]是a[]的差分数组

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<ctime>
#include<cstdlib>
#include<queue>
//#include<windows.h>
using namespace std;

long long lowbit(long long val){
    return val&(-val);
}
struct BIT{
    long long size;
    long long tree[1000005];
    
    void init(long long mysize){
        size=mysize;
        for(long long i=0;i<1000005;i++) tree[i]=0;
    }
    void change(long long id,long long val){
        for(long long i=id;i<=size;i+=lowbit(i))
            tree[i]+=val;
    }
    long long getsum(long long id){
        if(id==0) return 0;
        long long ans=0;
        for(long long i=id;i>=1;i-=lowbit(i))
            ans+=tree[i];
        return ans;
    }
    long long ask(long long l,long long r){
        return getsum(r)-getsum(l-1);
    }
}bitC,bitD;

long long n,qn;

void Change(long long l,long long r,long long val){
    bitC.change(l,val);
    bitC.change(r+1,-val);
    bitD.change(l,val*l);
    bitD.change(r+1,-val*(r+1));
}
long long Getsum(long long id){
    return (id+1)*bitC.getsum(id)-bitD.getsum(id);
}
long long Ask(long long l,long long r){
    return Getsum(r)-Getsum(l-1);
}

int main(){
    cin>>n>>qn;
    bitC.init(n);bitD.init(n);
    for(long long i=1;i<=n;i++){
        long long t;scanf("%lld",&t);
        Change(i,i,t);
    }
    for(long long i=1;i<=qn;i++){
        char type[2];
        long long l,r,val;
        scanf("%s",type);
        switch(type[0]){
            case '2':{
                scanf("%lld%lld",&l,&r);
                printf("%lld
",Ask(l,r));
                break;
            }
            case '1':{
                scanf("%lld%lld%lld",&l,&r,&val);
                Change(l,r,val);
                break;
            }
        }
    }
    return 0;
}
View Code

任务4:

模版:洛谷P1531 (I Hate It)

(树状数组求最值)

已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数数变为x

2.询问某一区间的最大值

这个和普通的求和又不一样了。随便举几个例子我们发现,存储区间最值的tree[i]不能直接$O(1)$修改。所以更新的话需要把tree[i]的所有直接子节点全部遍历一道(其实某种意义上,求和是一种不需要遍历的特殊用法)。这里有一个玄学的规律,假设我们需要更新tree[i],那么它的直接子节点就是tree[i-1],tree[i-2],tree[i-4],...也就是tree[i-2^k],直到编号超出tree[i]所管辖的范围。又因为tree[i]的管辖点的个数为$lowbit(i)$,所以只需要判断$2^k<lowbit(i)$就好了。处理完本个需要更新的节点后,i+=lowbit(i)更新下一个节点。时间复杂度$O((logn)^2)$

更新没问题了,但显然区间最值没办法用前缀和那样的思想求解。那么如何处理询问呢?可以把要求的区间拆分成众多小区间来计算。首先还是像求和那样不停的减lowbit(i),如果发现这个这个区间超出了要求的区间(比较要求的左端点和本区间左端点),显然我们不能把这个区间纳入计算范围。所以我们把i减1,让i进入更小的区间去搜寻。最后,当i==l时,结束搜寻,输出结果。时间复杂度O(logn)

(代码里询问部分用r替代了i)

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<ctime>
#include<cstdlib>
#include<queue>
//#include<windows.h>
using namespace std;

int lowbit(int val){
    return val&(-val);
}

struct BIT{
    int size;
    int data[500005];
    int tree[500005];
    
    void init(int mysize){
        size=mysize;
        for(int i=0;i<500005;i++){
            tree[i]=0;data[i]=0;
        }
    }
    void change(int id,int val){
        data[id]=val;
        for(int i=id;i<=size;i+=lowbit(i)){
            tree[i]=data[i];
            for(int j=1;j<lowbit(i);j<<=1){
                tree[i]=max(tree[i],tree[i-j]);
            }
        }
    }
    int ask(int l,int r){
        int ans=0;
        for(;l<=r;){
            ans=max(ans,data[r]);
            r--;
            for(;r-lowbit(r)>=l;r-=lowbit(r)){
                ans=max(ans,tree[r]);
            }
        }
        return ans;
    }
}a;

int n,m;

int main(){//单点修改,区间查询最大值 
    cin>>n>>m;
    a.init(n);
    for(int i=1;i<=n;i++){
        int t1;
        scanf("%d",&t1);
        a.change(i,t1);
    }
    
    for(int i=1;i<=m;i++){
        int t1,t2,t3;
        scanf("%d",&t1);
        if(t1==1){
            scanf("%d%d",&t2,&t3);
            a.change(t2,t3);
        }else if(t1==2){
            scanf("%d%d",&t2,&t3);
            printf("%d
",a.ask(t2,t3));
        }
    }
    return 0;
}
View Code

参考:

(lowbit证明)http://www.mamicode.com/info-detail-1270028.html
(任务1题解)https://pks-loving.blog.luogu.org/senior-data-structure-gao-ji-shuo-ju-jie-gou-chu-bu-yang-xie-fen-kuai

(任务1二维题解)https://blog.csdn.net/xf_zhen/article/details/52439808

(任务2简单思路)https://www.luogu.org/blog/user33857/solution-p3368

(任务3)https://www.cnblogs.com/RabbitHu/p/BIT.html

(任务4)https://www.cnblogs.com/mypride/p/5002556.html

(任务4)https://blog.csdn.net/yu121380/article/details/81431480

原文地址:https://www.cnblogs.com/sun123zxy/p/bit.html