树状数组详解
简介
树状数组和下面的线段树可是亲兄弟了,但他俩毕竟还有一些区别:
树状数组能有的操作,线段树一定有;
线段树有的操作,树状数组不一定有。
这么看来选择 线段树 不就 「得天下了」 ?
事实上,树状数组的代码要比线段树短得多,思维也更清晰,在解决一些单点修改的问题时,树状数组是不二之选。
原理
如果要具体了解树状数组的工作原理,请看下面这张图:
这个结构的思想和线段树有些类似:用一个大节点表示一些小节点的信息,进行查询的时候只需要查询一些大节点而不是更多的小节点。
最下面的八个方块 (标有数字的方块) 就代表存入
他们上面的参差不齐的剩下的方块就代表
很显然看出:
所以,如果你要算区间和的话,比如说要算
那么这种类似于跳一跳的连续跳到中心点而分值不断变大的原理是一样的(倍增)。
你从
用法及操作
那么问题来了,你是怎么知道
这时,我们引入一个函数—— lowbit
:
int lowbit(int x) {
//算出x二进制的从右往左出现第一个1以及这个1之后的那些0组成数的二进制对应的十进制的数
return x & -x;
}
lowbit
的意思注释说明了,咱们就用这个说法来证明一下
发现第一个
这就是 lowbit
的用处,仅此而已(但也相当有用)。
你可能又问了:x & -x 是什么意思啊?
在一般情况下,对于
int
型的正数,最高位是 0,接下来是其二进制表示;而对于负数 (-x),表示方法是把 x 按位取反之后再加上 1 (补码知识)。
例如 :
那么对于 单点修改 就更轻松了:
void add(int x, int k) {
while (x <= n) { //不能越界
c[x] = c[x] + k;
x = x + lowbit(x);
}
}
每次只要在他的上级那里更新就行,自己就可以不用管了。
int getsum(int x) { // a[1]……a[x]的和
int ans = 0;
while (x >= 1) {
ans = ans + c[x];
x = x - lowbit(x);
}
return ans;
}
区间加 & 区间求和
若维护序列
进行推导
区间和可以用两个前缀和相减得到,因此只需要用两个树状数组分别维护
代码如下
int t1[MAXN], t2[MAXN], n;
inline int lowbit(int x) { return x & (-x); }
void add(int k, int v) {
int v1 = k * v;
while (k <= n) {
t1[k] += v, t2[k] += v;
k += lowbit(k);
}
}
int getsum(int* t, int k) {
int ret = 0;
while (k) {
ret += t[k];
k -= lowbit(k);
}
return ret;
}
void add1(int l, int r, int v) {
add(l, v), add(r + 1, -v); //将区间加差分为两个前缀加 ①
}
long long getsum1(int l, int r) {//1ll :代表长整型的 1
return (r + 1ll) * getsum(t1, r) - 1ll * l * getsum(t1, l - 1) -
(getsum(t2, r) - getsum(t2, l - 1));
}
/* ------------另一种写法 ------------*/
//树状数组 2:区间修改,单点查询 模板AC代码
#include <bits/stdc++.h>
using namespace std;
#define lowbit(x) (x & -x)
typedef long long ll;
const int maxn = 1e6 + 10;
ll n, q, tr[maxn], a, pre;
void add(int i, int v) {
for (; i <= n; i += lowbit(i)) tr[i] += v;
}
ll getsum(int i) {
ll sum = 0;
for (; i; i -= lowbit(i)) sum += tr[i];
return sum;
}
int main() {
// freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) cin >> a, add(i, a - pre), pre = a;
ll opt, u, v;
while (q--) {
cin >> opt >> u;
if (opt == 1) {
cin >> v >> a;
add(u, a), add(v + 1, -a);//维护差分数组
} else
cout << getsum(u) << endl;
}
}
注释 ①:因为维护的是差分数组。
区间 [l,r] 加 v 就相当于在差分数组的 l 位置加 v ,在 r + 1 位置 -v
维护的是差分数组的前缀信息
Tricks
每一个节点的值是由所有与自己直接相连的儿子的值求和得到的。因此可以倒着考虑贡献,即每次确定完儿子的值后,用自己的值更新自己的直接父亲。
// O(n)建树
void init() {
for (int i = 1; i <= n; ++i) {
t[i] += a[i];
int j = i + lowbit(i);
if (j <= n) t[j] += t[i];
}
}
参考 "可持久化线段树" 章节中,关于求区间第
因此可以想到算法:如果已经找到
在树状数组中,节点是根据 2 的幂划分的,每次可以扩大 2 的幂的长度。令
- 求出
- 计算
- 如果
,则此时扩展成功,将 累加到 上;否则扩展失败,对 不进行操作 - 将
减 1,回到步骤 2,直至 为 0
//权值树状数组查询第k小
int kth(int k) {
int cnt = 0, ret = 0;
for (int i = log2(n); ~i; --i) { // i与上文depth含义相同
ret += 1 << i; //尝试扩展
if (ret >= n || cnt + t[ret] >= k) //如果扩展失败
ret -= 1 << i;
else
cnt += t[ret]; //扩展成功后 要更新之前求和的值
}
return ret + 1;
}
时间戳优化:
对付多组数据很常见的技巧。如果每次输入新数据时,都暴力清空树状数组,就可能会造成超时。因此使用
//权值树状数组查询第k小
int kth(int k) {
int cnt = 0, ret = 0;
for (int i = log2(n); ~i; --i) { // i与上文depth含义相同
ret += 1 << i; //尝试扩展
if (ret >= n || cnt + t[ret] >= k) //如果扩展失败
ret -= 1 << i;
else
cnt += t[ret]; //扩展成功后 要更新之前求和的值
}
return ret + 1;
}
例题
- 树状数组 1:单点修改,区间查询 / 提交记录
- 树状数组 2:区间修改,单点查询 / 提交记录
- 树状数组 3:区间修改,区间查询 / 提交记录
- 二维树状数组 1:单点修改,区间查询 / 提交记录
- 二维树状数组 3:区间修改,区间查询 / 提交记录
其它
文章开源在 Github - blog-articles,点击 Watch 即可订阅本博客。 若文章有错误,请在 Issues 中提出,我会及时回复,谢谢。
如果您觉得文章不错,或者在生活和工作中帮助到了您,不妨给个 Star,谢谢。
(文章完)
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库
· 5. Nginx 负载均衡配置案例(附有详细截图说明++)