【模板篇】树状数组们(一)

以下文章逻辑混乱,请确保精神正常后再观看。

树状数组???
百度上讲的非常的多,各方面的资料也都有所涉及,大家不懂的可以去逛一圈搜一搜。
但树状数组能干的事情非常多,我看也没有很详细的总结,就来浪一波,斗胆讲一讲树状数组。。各位看官,要本蒟蒻讲的不好,你们轻喷。。
树状数组,作为一个nlogn数据结构,有着得天独厚 的优势。比如,常数小,码短好写。。
当然也是有一些缺点,比如很多事干不了。。然而在他能干的领域里面,效率能甩别的数据结构几条街。

为什么要搞树状数组???
快。
就拿树状数组最基础的例子来讲,让你维护一个数列,支持如下操作:
1)修改某个点i的值
2)查询区间[L,R]的数的总和
如果用数组来搞的话,会TLE的飞起。。。
这时候就需要一些nlogn的数据结构来救世了,比如:树状数组。
当然会有神犇@一些像线段树一类的数据结构,但树状数组比其他的要好写不少,常数也会小不少。。
然后树状数组最基本的原理是什么嘛,我真的讲不清楚(其实我不会),所以你们可以自己百度。
我只能说说大体意思。。
其实有张图会更明白是么。。(图片来自百度百科)
树状数组示意图
然后树状数组之流中有其特有的名片——lowbit~~
有了lowbit就知道是树状数组了。。。

lowbit是什么呢???
lowbit(i)指的是i在二进制表示下最低位的1及其后的所有0组成的值
比如36:在二进制下是100 1 00,最低位的1我加粗了,代表了4,所以lowbit(36)就是4。。
从图上可以看出,对于树状数组上的节点,第i个节点控制了(i-lowbit(i),i]的区间。。
比如节点1控制(0,1],节点4控制(0,4],(0是凑的),节点6控制(4,6](其实就是[5,6]啦)……
所以这么一联系就看出树状数组和lowbit密切相关。。

那如何求lowbit???
一位一位比嘛,不在乎那点效率,当然有快速的方法啦~
由百度百科(我说了你们听不懂):
设节点编号为x,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素。因为这个区间最后一个元素必然为Ax,
所以很明显:Cn = A(n – 2^k + 1) + … + An
这样算2^k易得(你要看不懂就不要管为什么了) lowbit(x)=x&(x^(x-1))①
然后计算机有个非常好的性质叫补码(就是一个二进制数相反数的表示),就是取反再加一。
所以①可简化为:lowbit(x)=x&-x !!!!真的好简便呢。。。

好吧,那么树状数组是怎么维护上面提到的两个问题的呢???
好吧,再去看图。。看图是有用的。。看到上面无比优美的性质,我们可以用C数组维护前缀和呢。。
没错,Ci可以保存1~i的前缀和,怎么求呢?
还记得大明湖畔的lowbit么?
(怎么不记得,他费了我半天劲才让我弄懂呢)
我们让一个点不断的减lowbit,然后把遇到的点的值都加上,就是前缀和了!!不信你可以找几个点蹦一下试试 ,没错就是这么神奇╮(╯_╰)╭……
所以求[L,R]的和,就用[1,R]的和减去[1,L-1]的和就行了~~
//什么玩意,我用数组预处理也能做到嘛好像还O(n)呢,你这O(nlogn)算啥嘛。。。
别急,还有修改呢。。你预处理就没法修改了吧。。。

所以如何修改呢???
单点修改嘛。。看图。。从i点开始不断的加lowbit,直到超出范围为止,单点的维护就搞定了~~ 你可以再找几个点蹦蹦试试

具体的原理基本就这样,我自己都觉得讲得不清楚。。
所以还是上代码吧,说不定你们能从代码中洞悉这般奥妙。。

#include <cstdio>

#define gc getchar

class Binary_Tree1{ //树状数组1
public:
    inline int getnum() {
        int a = 0; char c = gc(); bool f = 0;
        for (; (c<'0' || c>'9') && c != '-'; c = gc());
        if (c == '-') f = 1, c = gc();
        for (; c >= '0'&&c <= '9'; c = gc()) a = (a << 1) + (a << 3) + c - '0';
    } //读入优化

    inline void putnum(int x) { //快速输出
        int c[15], cnt = 0;
        for (; x; x /= 10)
            c[++cnt] = x % 10;
        for (; cnt; cnt--)
            putchar(c[cnt] + '0');
    }

    Binary_Tree1(int n) :n(n) {

    } //没任何卵用的构造函数╮(╯_╰)╭

    Binary_Tree1() {

    }

    void Binary_Tree1_Init() {
        for (int i = 1; i <= n; i++)
            add(i, getnum());
    } //读入的初始化

    void add(int x, int i) {      //单点x加i
        for (; x <= n; x += lb(x))
            c[x] += i;
    }

    int sum(int x) {          //查前x项的和
        int s = 0;
        for (; x; x -= lb(x))
            s += c[x];
        return s;
    }

    int query(int L, int R){     //区间查[L,R]的和
        return sum(R) - sum(L - 1);
    }
private:
    static const int MAXN = 500005;

    inline int lb(int x) { //lb就是lowbit,我嫌字母多就缩写了
        return x&(-x);
    }

    int c[MAXN], n;
};
### 关于树状数组2的模板与实现 树状数组2通常指的是支持 **区间修改** 和 **单点查询** 或者 **区间查询** 的高级版本。相比于基础版树状数组(仅支持单点修改和区间查询),它通过维护两个独立的树状数组来完成更复杂的操作。 #### 基本原理 为了处理区间加法问题,可以引入差分的思想[^3]。具体来说,定义 `ans` 表示实际的前缀和,`ans1` 是第树状数组存储的前缀和,`ans2` 是第二个树状数组用于修正偏移量,则有如下关系: \[ \text{ans}[i] = \text{ans1}[i] \times i - \text{ans2}[i]. \] 这种设计使得我们可以高效地执行以下两种操作: 1. 对某个区间的值加上个常数。 2. 查询某个位置的实际数值或者某区间的总和。 以下是具体的代码实现: ```cpp #include <bits/stdc++.h> using namespace std; // 定义最大数据范围 const int MAXN = 1e5 + 5; long long c1[MAXN], c2[MAXN]; // 两个树状数组分别记录原始值和偏移量 // lowbit函数计算二进制最低位1对应的十进制值 inline int lowbit(int x) { return x & (-x); } // 更新树状数组c1和c2的操作 void update(long long* tree, int pos, int delta, int n) { while (pos <= n) { tree[pos] += delta; pos += lowbit(pos); } } // 获取树状数组c1或c2的前缀和 long long query(long long* tree, int pos) { long long res = 0; while (pos > 0) { res += tree[pos]; pos -= lowbit(pos); } return res; } // 区间[a,b]增加val void add_range(int a, int b, int val, int n) { update(c1, a, val, n); // 修改a处的影响 update(c1, b + 1, -val, n); // 取消b+1之后的影响 update(c2, a, val * (a - 1), n); // 修改a处的偏移影响 update(c2, b + 1, -val * b, n); // 取消b+1之后的偏移影响 } // 单点查询第k个元素的真实值 long long get_value(int k) { return query(c1, k) * k - query(c2, k); } int main() { ios::sync_with_stdio(false); cin.tie(0); int n, m; cin >> n >> m; // 数组长度n,m次操作 memset(c1, 0, sizeof(c1)); memset(c2, 0, sizeof(c2)); for (int i = 1; i <= n; ++i) { // 初始化数组 int temp; cin >> temp; add_range(i, i, temp, n); // 将初始值转化为区间更新的形式 } for (int i = 0; i < m; ++i) { char op; cin >> op; if (op == 'Q') { // 查询操作 int idx; cin >> idx; cout << get_value(idx) << "\n"; } else if (op == 'C') { // 修改操作 int l, r, v; cin >> l >> r >> v; add_range(l, r, v, n); } } return 0; } ``` 上述代码实现了树状数组2的核心功能——即支持区间修改和单点查询的功能。其中的关键在于利用了两个树状数组 `c1` 和 `c2` 来模拟差分数组的效果。 --- ### 性能分析 相比线段树,树状数组2具有更高的效率和更低的空间开销,在许多场景下更适合快速解决问题[^2]。然而需要注意的是,当题目需求更加复杂时(如多维查询、动态开点等),可能仍需借助线段树或其他高级结构。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值