树状数组知识点整理一
树状数组知识点整理一
引言
原始的树状数组,是一个支持单点加区间查的结构。
要在可以接受的复杂度范围内实现区间查 \((\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修改,重写了一些表述不太清楚的地方