树状数组

简介

树状数组支持 \(O(\log n)\)的区间查询以及单点修改,对于区间修改是劣于线段树的,\(O(n\log n)\),但是他码量比线段树少,常数小,并且可以应对题目卡常等问题。

基本思想与实现

区间维护

树状数组就是维护区间和,如图。

上图 \(c[i]\) 维护一定范围的区间和,那么如何知道维护哪一段的区间和呢?
我们打个比方比如说 \(4\) 将其转化成二进制得 \(100\),维护长度为 \(4\) 的区间, \(6\) 转化成二进制得 \(110\) 维护长度为 \(2\) 的区间。
我们发现,维护的区间长度就是将其转化成二进制的最低位的 \(1\) 的权值,对于任何数均满足。我们将其记作 \(lowbit(i)\) ,那么整个 \(c[i]\) 的维护范围就是 \([i-lowbit(i)+1,i]\)
求这个函数是 \(O(1)\) 的,运用一些简单的位运算即可,完整函数如下:

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

我们来推导一下:
首先我们知道, \(-x\) 一定是负数,负数的二进制等于原码取反加一,我们令 \(k\)\(a\) 的最低位,首先,在 \(k\) 的前面包括 \(k\) 的都会取反那么 \(k\) 前面的&运算都为0,\(k\)为0,此时后边的所有\(0\)都变成了\(1\),那么再加一,会导致一系列的进位,最后让 \(k\) 又变成了1,而其后边的数本身就为0,所以&后结果也为0,只有\(k\)为1。
举个例子:
有一个二进制序列: \(1011000\)
取反后得: \(0100111\)
加一后得: \(0101000\)
前后与得: \(0001000\)

建树

我们知道, \(c[i]\) 维护的范围是 \([i-lowbit(i)+1,i]\) 因此我们可以通过前缀和建树。

int t[N];
void init(){
    for(int i=1;i<=n;i++)
        t[i]=sum[i]-sum[i-lowbit(i)];
}

区间查询

如果要查询 \([l,r]\) 我们可以求出前缀和 \([1,r]\)\([1,l-1]\) ,将其相减,就能得到。
所以我们可以将区间问题变为前缀和问题。
即我们需要计算 \([1,x]\) 的和。
我们知道, \(t[i]\) 维护的右端点一定是等于 \(i\) 的,那么在跳到其维护左端点以下的 \(t[i-lowbit(i)]\) 直到跳到头。
其复杂度是 \(O(\log n)\) ,即最需要跳 \(\log n\)\(i\)

get_sum(int x){
    int cnt=0;
    while(x){
        cnt+=t[x];
        x-=lowbit(x);
    }
    return cnt;
}

单点修改

修改一个点只需要修改其所有父节点,将本身加上 \(lowbit\) ,得到父节点。
复杂度 \(O(\log n)\)

void add(int x,int k){
    while(x<n)
        t[x]+=k,x+=lowbit(x);
}

区间修改

一个个修就行,复杂度 \(O(n\log n)\)

posted @ 2023-10-08 21:24  xyh0528  阅读(9)  评论(0)    收藏  举报