树状数组【原理及模板】

本文深入解析树状数组的工作原理,包括如何通过二进制操作消除最后一个位1,以及如何利用lowbit函数进行高效区间查询和单点更新。通过一个实战题目,详细展示了树状数组在处理动态数组更新与查询问题上的应用。
摘要由CSDN通过智能技术生成

消除二进制中最后一个位1,利用n &= (n - 1)就可以做到。然后看一下消了几次n变成0,次数就是答案。

n &= (n - 1)具体的操作是把最后一位1变成0,后面的0变成1,与运算一下,最后一位1就消掉了。

{ n &= (n - 1)  :即 n = n & ( n - 1 );  也就是说 只有当n与(n-1) 都为1时 n为1;

[&:与运算] }

 

以下稍微变动自大佬博客https://www.cnblogs.com/hsd-/p/6139376.html

树状数组,看名字就知道是树状的数组;(如下图二叉树所示)

叶子结点代表数组A[1]~A[8];

通过变形,可将图形转化为:

现在定义每一列的顶端节点C[ ]数组;

即为下图所示:

C[i]代表子树的叶子结点的权值之(和或差或积等其他),//这里以求和举例;

如图可以知道:

C[1] = A[1];

C[2] = A[1]+A[2];

C[3] = A[3];

C[4] = A[1]+A[2]+A[3]+A[4];

C[5] = A[5];

C[6] = A[5]+A[6];

C[7] = A[7];

C[8] =A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

下面观察如下图

将C[ ]数组的结点号转化为二进制

1 = (001)   C[1] = A[1];

2 = (010)   C[2] = A[1]+A[2];

3 = (011)   C[3] = A[3];

4 = (100)   C[4] = A[1]+A[2]+A[3]+A[4];

5 = (101)   C[5] = A[5];

6 = (110)   C[6] = A[5]+A[6];

7 = (111)   C[7] = A[7];

8 = (1000)  C[8] = A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

对比式子可以发现 C[i] = A[i-2^k+1]+A[i-2^k+2]+......+A[i];  (K为i的二进制中从最低位到高位连续零的长度)

当i = 8时,k = 3;带入验证正确;

现在引入lowbit (x)

lowbit (x) 就是取出x的最低位1    即为:lowbit(x) = 2^k ; 

例:假设 x = 6;  lowbit(6) = 2^1=2;  (因为6的二进制数110从最低位到高位连续零的长度为1)

2化为二进制数就是数字6的二进制数的最低位1的位置。

lowbit  代码:

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

//-t 代表t的负数, 计算机中负数使用对应正数的b补码来表示

//例如:

//t = 6 = (0110)  此时 k = 1

//-t = -6 = (1001+1) = (1010)

//t&(-t) = (0010) = 2 = 2^1

C[i]=A[i-2^k+1]+A[i-2^k+2]+......A[i];

C[i]=A[i-lowbit(i)+1]+A[i-lowbit(i)+2]+......A[i];



区间查询(从上往下):

利用C[i]数组,求A数组中前i项的和

例如,

i= 7;

sum[7] = A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7];前i项和

C[4] = A[1]+A[2]+A[3]+A[4];

C[6] = A[5]+A[6];

C[7] = A[7];

所以可以推出:sum[7] = C[4]+C[6]+C[7];

序号写为二进制为:sum[(111)] = C[(100)]+C[(110)]+C[(111)];

i = 5;

sum = A[1]+A[2]+A[3]+A[4]+A[5]; 前i项和;

C[4] = A[1]+A[2]+A[3]+A[4];

C[5] = A[5];

所以可以推出:sum[5] = C[4]+C[5];

序号写为二进制为:sum[(101)] = C[(100)]+C[(101)];

区间查询代码:

int getsum(int x)
{
    int ans=0;
    for(int i=x;i>0;i-=lowbit(i))
        ans+=C[i];
    return ans;
}

对于i=7 进行演示 

7(111)                                               ans+=C[7]

lowbit(7)=001  7-lowbit(7)=6(110)    ans+=C[6]

lowbit(6)=010  6-lowbit(6)=4(100)    ans+=C[4]

lowbit(4)=100  4-lowbit(4)=0(000)    

对于i=5 进行演示 

5(101)                                               ans+=C[5]

lowbit(5)=001  5-lowbit(5)=4(100)    ans+=C[4]

lowbit(4)=100  4-lowbit(4)=0(000)   



单点更新(从下往上);

当我们修改A[ ]数组中的某一个值时,应该如何更新C[ ] 数组

回想区间查询的过程,结合代码分析:

(更新过程是查询过程的逆向)

void add(int x, int y)
{
    for(int i=x; i<=n; i=i+lowbit(i))
        tree[i]=tree[i]+y;

}

如图:更新A[1]时,需要向上更新C[1], C[2], C[4], C[8]

C[1],            C[2],           C[4],         C[8]         写为二进制分别为:

C[(001)],   C[(010)],   C[(100)],   C[(1000)]

1(001)   C[1]=C[1]+A[1]

lowbit(1)=001  1+lowbit(1)=2(010)    C[1]=C[1]+A[1]

lowbit(2)=010  2+lowbit(2)=4(100)    C[2]=C[2]+A[1]

lowbit(4)=100  4+lowbit(4)=8(1000)  C[8]=C[8]+A[1]
 


----------------------------------------------------------------------------我是分割线----------------------------------------------------------------------------- 


下面 写个简单题的 题解帮助更好理解和应用


首先 题意

就是  N个营地里的人 在随时变化(增加或减少),

Add   A  B 命令:第A个营地里增加了B个人

Sub   A  B 命令:第A个营地里减少了B个人

Query A  B 命令:计算从第A个到第B个营地里的总人数


#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
int tree[50001];
int N;

//提取最低位的1
int lowbit(int t)
{
    return t&(-t);
}

//单点更新(第i个营地开始更新,更新数为y)
void add(int x, int y)
{
    for(int i=x; i<=N; i+=lowbit(i))//从i=x开始 通过+lowbit  直到i不满足<=N 即把树更新完。
        tree[i]+=y;
}

//区间查询
int getsum(int x)
{
    int ans = 0;
    for(int i=x; i>0; i-=lowbit(i))
        ans+=tree[i];
    return ans;
}
int main()
{
    int T, x, y;
    int cases=1;
    char s[10];
    scanf("%d",&T);//输入案例数;
    while(T--)
    {
        scanf("%d",&N);//输入营地数
        memset(tree, 0, sizeof(tree));//数组初始化;
        printf("Case %d:\n",cases++);
        for(int i=1; i<=N; i++)
        {
            scanf("%d",&x);//输入每个营地开始时的人数;
            add(i, x);//并且将一开始时的人数放入树状数组中;
        }
        while(scanf("%s",s)&&s[0]!='E')//当输入字符串第一个不为E即操作命令并没有结束时;
        {
            scanf("%d %d",&x, &y);//输入对第x个营地,做出的s操作,改变y的人数;

            //如果输入字符串为Add  即第x个营地 增加y个人
            if(s[0]=='A')
            {
                add(x,y);
            }

            //如果输入字符串为Sub  即第x个营地  减少y个人
            else if(s[0]=='S')
            {
                add(x,-y);
            }

            //如果输入字符串为Query 即计算从第x到第y个营地的总人数
            else if(s[0]=='Q')
            {
                int ans = getsum(y)-getsum(x-1);
                printf("%d\n",ans);
            }
        }
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值