树状数组知识点整理一

树状数组知识点整理一

引言

原始的树状数组,是一个支持单点加区间查的结构。

要在可以接受的复杂度范围内实现区间查 \((\Theta(\log n))\) ,肯定不能遍历区间 \((\Theta(n))\) 。如果用分块暴力可以将复杂度变为 \((\Theta(\sqrt n))\) 。要想让时间复杂度更低,我们需要更高效的利用已知信息。

首先,最终规划肯定需要考虑到数组每一个元素,即所有点的信息加起来,我们能推得原始数组的信息。

其次,对于任意的区间查询,通过最终规划分解,至多分成 \(\Theta(\log n)\) 个区间,对应每次查询复杂度为 \(\Theta(1)\)

最后,最终规划要支持单点加,同时能在合理复杂度 \(\Theta(\log n)\) 时间内修改所有受到影响的点。

下面我们来看看原始的树状数组是如何通过规划满足上述条件的。

讨论

类比于前缀和的思想,我们让一个 原始数组 \(a\) 中的一些点从原来储存一位的值变成储存连续一段的和,记处理过的数组为 \(c\)

假设我们要查询 \(a_s--a_t\) 的和。

将任意查询区间划分为 \(a_1--a_{s-1}\)\(a_1--a_t\) 两部分,如果能分别得到两部分的区间和,那么相减就是查询区间的答案。

如果能保证每一段连续的长度都是 \(2\) 的幂,就可以根据下标将区间分为连续的子区间。

例如,如果要查询 \(a_1\)\(a_{111000_2} = a_{56_{10}}\) 对应的区间和,按如下区间划分下标。

\(110000_2--111000_2\)\(100000_2--110000_2\)\(0_2--100000_2\)

即查询下述区间和

\(a_{49}--a_{56}\)\(a_{33}--a_{48}\)\(a_1--a_{32}\)

相加即为所求。

这里我们考虑的区间为左开右闭。

如果按照上述要求,容易发现 \(c_{1000000_2} = c_{64_{10}}\) 中应该存储原始数组前64位的总和, \(c_{100000_2} = c_{32_{10}}\) 中应该存储原始数组前32位的总和。

那么中间的呢,例如 \(c_{110000_2} = c_{48_{10}}\) 中应该是什么?

显然,其中应该存储原始数组从32+1位到48位的总和。因为 \(110000_2-100000_2 = 10000_2\) ,即48位比32位要多出来16位,而正好16是2的幂。

同理能得到 \(c_{110100_2} = c_{52_{10}}\) 应该比48多出4位,所以 \(c_{52_{10}}\) 存储的就是从48+1位到52位的总和。

\(k\) 为数 \(x\) 在二进制下第一次出现1的位数对应的值 \((x=1100_2 \Rightarrow k=100_2)\) ,显然, \(c_x\) 存储的应该是从第 \(x-k+1\) 到第 \(x\) 位的总和。

为了快速得到 \(k\) ,于是有了函数 \(lowbit()\)

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

关于其原理,不再叙述。其结果就是数 \(x\) 对应的 \(k\)

有了这些,我们就能写出求区间1到x的和的代码。

int get_range(int x) {
  	int ans = 0;
    while(x) {
        ans += a[x];
        x -= lowbit(x);
    }
    return ans;
}

即每次查询 \(x-k+1\)\(x\) 的值,加到变量里,然后将 \(x\) 变为 \(x-k\) ,即去掉最后一个一。

显然,区间查询复杂度为 \(\Theta(\log n)\)

然后我们来考虑单点加的情况下,不妨设我们在 \(c_{11010_2} = c_{26_{10}}\) 的位置上加了一个数 。

显然,小于26的位置一定不会改变,第26位一定改变 。

考虑大于26位的情况。

容易发现,只有包含第26位也就是 \(11010_2\) 的区间会被更改。

考虑哪些区间包含 \(11010_2\)

由对于区间和的讨论可以知道任意数 \(x\) 代表的区间为 \(x-lowbit(x)+1\)\(x\)

从小到大考虑,我们可以得到如下的值:

\(11100_2,\ 100000_2,\ 1000000_2,\ ...,\ 100...00_2 \leq n\)

代码如下:

void add_point(int num, int loc) {
    while(loc <= n) {
        a[loc] += num;
        loc = loc + lowbit(loc);
    }
} 

\(X\) 满足条件即指对于修改的点 \(B\) ,我们有 \(X-lowbit(X) < B < X\) ,即它代表的区间包含修改的点

\(lowbit()\) 得到 \(k\) 的证明如下:

合理性:

假设 \(loc\) 满足条件,考虑 \(loc+lowbit(loc) = c\) 是否满足。

因为 \(loc+lowbit(loc)\) 一定会导致至少一位的进位,所以容易发现 \(lowbit(loc)*2 \leq lowbit(c)\)

显然 \(c-lowbit(c) \leq loc-lowbit(loc)\)

所以 \(c-lowbit(c) \leq loc-lowbit(loc) < B < loc < c\)

即对于任意 \(loc\) ,有\(loc+lowbit(loc)\) 满足条件。

唯一性:

首先我们证明对于一个满足条件的数 \(x\) ,下一个满足条件的数一定是 \(x+lowbit(x)\)

考虑 \(x+lowbit(y)\)\(lowbit(y)<lowbit(x)\)

显然 \(lowbit(y)\)\(x\) 的增值并不会造成进位,因为加的值小于 \(x\) 最小的1的位置对应的值。

那么我们有 \(x+lowbit(y) - lowbit(x+lowbit(y))=x\)

因为 \(x\) 满足条件,所以 \(x > B\) ,所以 \(x+lowbit(y)\) 不满足条件。

所以对于任意满足条件的数 \(x\)\(x+1\)\(x+lowbit(x)-1\) 中不存在满足条件的数。

易得,仅有按上述规则得到的数满足条件。

证毕。

代码 (以洛谷P3374为例)

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

#define ll long long
#define ull unsigned long long
#define cint const int&
#define Pi acos(-1)

const int mod = 998244353;
const int inf_int = 0x7fffffff;
const ll inf_ll = 0x7fffffffffffffff;
const double ept = 1e-9;

int n, m;
ll a[500100];
int lowbit(cint x) { return x&-x; }

void add(cint num, int loc) {
    while(loc <= n) {
        a[loc] += num;
        loc = loc + lowbit(loc);
    }
}

ll get(int x, int y) {
    ll ans = 0;
    while(y) {
        ans += a[y];
        y -= lowbit(y); 
    }
    while(x) {
        ans -= a[x];
        x -= lowbit(x);
    }
    return ans;
}

int main() {
    cin >> n >> m;
    int a;
    for(int i=1; i<=n; i++) {
        cin >> a;
        add(a,i);
    }
    int b,c;
    for(int i=1; i<=m; i++) {
        cin >> a >> b >> c;
        if(a==1) {
            add(c, b);
        } else {
            cout << get(b-1, c) << endl;
        }
    }

    return 0;
}

3.30修改,重写了一些表述不太清楚的地方

posted @ 2020-11-15 19:07  ullio  阅读(173)  评论(0编辑  收藏  举报