• 树状数组学习


    树状数组简介

    树状数组,用于维护和查询前缀和,与线段树功能类似。树状数组代码短,常数和空间小,时间复杂度小,所以这也是一个十分优秀的算法。

    在这里插入图片描述

    a [ i ] a[i] a[i]为原数组上的点, s [ i ] s[i] s[i]为树状数组中各点,则

    s [ 1 ] = a [ 1 ] s[1]=a[1] s[1]=a[1]
    s [ 2 ] = a [ 1 ] + a [ 2 ] s[2]=a[1]+a[2] s[2]=a[1]+a[2]
    s [ 3 ] = a [ 3 ] s[3]=a[3] s[3]=a[3]
    s [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] s[4]=a[1]+a[2]+a[3]+a[4] s[4]=a[1]+a[2]+a[3]+a[4]
    s [ 5 ] = a [ 5 ] s[5]=a[5] s[5]=a[5]
    s [ 6 ] = a [ 5 ] + a [ 6 ] s[6]=a[5]+a[6] s[6]=a[5]+a[6]

    可以发现, s [ x ] = ∑ i = x − 2 k + 1 x a [ i ] s[x]=\sum\limits_{i=x-2^k+1}^x a[i] s[x]=i=x2k+1xa[i],其中 k k k表示 x x x的二进制中末尾连续的0的个数。


    树状数组的操作

    lowbit(i)

    根据上文, s [ x ] = ∑ i = x − 2 k + 1 x a [ i ] s[x]=\sum\limits_{i=x-2^k+1}^x a[i] s[x]=i=x2k+1xa[i],其中 k k k表示 x x x的二进制中末尾连续的0的个数。那应该如何求 2 k 2^k 2k呢?

    我们可以用 l o w b i t lowbit lowbit来求, l o w b i t ( n ) lowbit(n) lowbit(n)的值为 n n n的二进制中最后一个1的权值,也就是 2 k 2^k 2k。而 l o w b i t ( n ) lowbit(n) lowbit(n)可以由n&(-n)得出。

    为什么呢?我们可以得出 n − 1 n-1 n1是将 n n n的二进制从末尾到最后一个1全部取反,然后将 n − 1 n-1 n1的各位取反得 ~n+1。n与 ~n+1 在 n n n末尾最后一个1之前都为0,在末尾最后一个1之前都不相等。所以n&(~n+1)即为n的二进制中最后一个1的权值。
    因为在补码中,~ n=-n-1。所以n&(~n+1)=n&(-n)

    code

    int lowbit(int n){
    	return n&(-n);
    }
    
    • 1
    • 2
    • 3

    单点修改

    a [ i ] a[i] a[i]增加 v v v。由于 s [ x ] = ∑ i = x − 2 k + 1 x a [ i ] s[x]=\sum\limits_{i=x-2^k+1}^x a[i] s[x]=i=x2k+1xa[i],所以 s [ i ] , s [ i + 2 k ] , … s[i],s[i+2^k],\dots s[i],s[i+2k],都要修改。

    code

    void add(int i,int v){
    	while(i<=n){
    		s[i]+=v;i+=lowbit(i);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)


    区间查询

    求前 i i i个数的前缀和,可以画图理解。

    code

    int sum(int i){
    	int re=0;
    	while(i){
    		re+=s[i];i-=lowbit(i);
    	}
    	return re;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    若要求区间 [ l , r ] [l,r] [l,r]的和,则答案为 s u m ( r ) − s u m ( l − 1 ) sum(r)-sum(l-1) sum(r)sum(l1)

    时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)


    例题

    洛谷 P3374 【模板】树状数组

    单点修改+区间查询

    code

    #include
    using namespace std;
    int n,m,tp,x,y,tr[500005];
    int lb(int i){
    	return i&(-i);
    }
    void add(int i,int v){
    	while(i<=n){
    		tr[i]+=v;i+=lb(i);
    	}
    }
    int find(int i){
    	int re=0;
    	while(i){
    		re+=tr[i];i-=lb(i);
    	}
    	return re;
    }
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++){
    		scanf("%d",&x);
    		add(i,x);
    	}
    	for(int i=1;i<=m;i++){
    		scanf("%d%d%d",&tp,&x,&y);
    		if(tp==1) add(x,y);
    		else printf("%d\n",find(y)-find(x-1));
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    洛谷 P3372

    区间修改+区间查询

    我们可以用差分的思想,设 d i = a i − a i − 1 d_i=a_i-a_{i-1} di=aiai1,则 a i = ∑ j = 1 i a j a_i=\sum\limits_{j=1}^i a_j ai=j=1iaj

    对于区间修改,设将区间 [ l , r ] [l,r] [l,r]都增加 v v v,则 d l + = v , d r + 1 − = v d_l+=v,d_{r+1}-=v dl+=v,dr+1=v即可,这样就变成了单点修改。

    对于区间查询, s u m i = ∑ j = 1 i a j = ∑ j = 1 i ∑ k = 1 j d k = ∑ j = 1 i d j ∗ ( i − j + 1 ) = ( i + 1 ) ∑ j = 1 i d j − ∑ j = 1 i ( j ∗ d j ) sum_i=\sum\limits_{j=1}^ia_j=\sum\limits_{j=1}^i\sum\limits_{k=1}^jd_k=\sum\limits_{j=1}^i d_j*(i-j+1)=(i+1)\sum\limits_{j=1}^i d_j-\sum\limits_{j=1}^i(j*d_j) sumi=j=1iaj=j=1ik=1jdk=j=1idj(ij+1)=(i+1)j=1idjj=1i(jdj)

    也就是说,我们只要用两个树状数组维护 d i d_i di i ∗ d i i*d_i idi的前缀和,就能求出 a i a_i ai的和。

    code

    #include
    using namespace std;
    int n,m,tp,x,y,a[100005];
    long long k,tr[100005][2];
    int lb(int i){
    	return i&(-i);
    }
    void add(int i,long long v,int z){
    	while(i<=n){
    		tr[i][z]+=v;i+=lb(i);
    	}
    }
    long long find(int i,int z){
    	long long re=0;
    	while(i){
    		re+=tr[i][z];i-=lb(i);
    	}
    	return re;
    }
    long long sum(int i){
    	return (i+1)*find(i,0)-find(i,1);
    }
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++){
    		scanf("%d",&a[i]);
    		x=a[i]-a[i-1];
    		add(i,x,0);
    		add(i,x*i,1);
    	}
    	for(int i=1;i<=m;i++){
    		scanf("%d%d%d",&tp,&x,&y);
    		if(tp==1){
    			scanf("%lld",&k);
    			add(x,k,0);
    			add(y+1,-k,0);
    			add(x,k*x,1);
    			add(y+1,-k*(y+1),1);
    		}
    		else printf("%lld\n",sum(y)-sum(x-1));
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
  • 相关阅读:
    QT day1
    新书分享:《精装版| 基于Essential Macleod软件的光学薄膜设计技术(第二版)》
    Vue基础面试题(二)
    MySQL 高级(进阶) SQL 语句 (一)
    释放搜索潜力:基于Docker快速搭建ES语义检索系统(快速版),让信息尽在掌握
    EXPLAIN的使用
    QML 调试笔记
    DataGrip 安装教程 详细版
    硬盘接口随机
    变态跳台阶,剑指offer
  • 原文地址:https://blog.csdn.net/tanjunming2020/article/details/127985705