算法笔记——树状数组

一、一维树状数组

树状数组是一种存储前缀和的数据结构

树状数组通过二进制来划分区间存储存储部分的区间和,得到一个管理数组c

 若i的二进制表示末尾有k个连续的0,则c[i]存储的区间长度为2^{k},即c[i]=a[i-2^{k}+1]+a[i-2^{k}+2]+...+a[i]

c数组的修改

修改一个a中的值,只需要修改与其有关的祖先节点,例如修改a[5]只需改动c[6]和c[8]

如何获取区间长度?

c[i]表示的区间长度实际上就是i末尾1和后面所有零表示的二进制数字。

对i取反+1,再使其和i进行 与(&)操作即可得到区间长度

因为计算机以补码存储数字,-i 的补码为i取反加一

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

从而得到c[i]表示的区间长度。

直接前驱:c[i]的直接前驱为c[i-lowbit(i)]

直接后继:c[i]的直接后继为c[i+lowbit(i)]

前驱:c[i]的直接前驱、其直接前驱的直接前驱等,即c[i]左侧所有子树的根

后继:c[i]的直接后继、其直接后继的直接后继等,即c[i]的所有祖先

因为区间长是按二进制划分的,所以c[i+lowbit(i)]得到的一定是其父节点

c[i-lowbit(i)]得到的一定是左侧紧邻的子树的根(区间长度大一倍)

因此对一个点进行更新,只需要更改其所有祖先,即所有后继

void add(int i,int w)
{
    for(;i<=n;i+=lowbit(i))    c[i]+=w;
}

前缀和的查询

因为每个数都可以表示为若干个2的幂次之和,那么按照其划分前缀和区间计算c的和即可做到O(logn)查询前缀和。即只需要查询其全部的前驱。

例:7D=111B,则sum[7]=c[7]+c[6]+c[4]  (找前驱的过程实际上就是抹末尾1的过程)

int sum(int i)
{
    int s=0;
    for(;i>0;i-=lowbit(i))
        s+=c[i];
    return s;
}

树状数组每次更改和查询的时间复杂度均为O(logn)

二、多维树状数组

加套一层循环即可

void add(int x,int y,int w)
{
    for(int i=x;i<=n;i+=lowbit(i))
        for(int j=y;j<=n;j+=lowbit(j))
            c[i][j]+=w;
}

int sum(int x,int y)
{
    int s=0;
    for(int i=x;i>0;i-=lowbit(i))
        for(int j=y;j>0;j-=lowbit(j))
            s+=c[i][j];
    return s;
}

int subsum(int x1,int y1,int x2,int y2)
{
    return sum(x2,y2)-sum(x1-1,y2)-sum(x2,y1-1)+sum(x1-1,y1-1);
}

树状数组的局限性

树状数组主要用于查询前缀和、区间和及点更新,对点查询、区间修改效率较低

点查询:普通数组需要O(1),树状数组需要O(logn)

区间查询:普通数组需要O(n),树状数组需要O(nlogn)

减法规则:树状数组只适用于满足减法规则的区间维护,当问题不满足减法规则,如最大最小值时,则不能采用树状数组维护。

注意点:树状数组只能从1开始存,否则更新0时会导致死循环

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值