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

本文介绍了树状数组这一高效数据结构,详细阐述了其原理及应用,包括如何利用lowbit进行区间查询与单点修改,适合对算法有一定了解的读者。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

树状数组???
百度上讲的非常的多,各方面的资料也都有所涉及,大家不懂的可以去逛一圈搜一搜。
但树状数组能干的事情非常多,我看也没有很详细的总结,就来浪一波,斗胆讲一讲树状数组。。各位看官,要本蒟蒻讲的不好,你们轻喷。。
树状数组,作为一个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;
};
<think>我们正在讨论树状数组的倍增技术。树状数组本身是基于二进制的索引结构,而倍增是种利用树状数组特性进行高效查询(如前缀和)的技术。倍增在树状数组中的个典型应用是:在单调序列(如前缀和数组是单调递增的)上进行二分查找。例如,我们可以使用倍增来查找前缀和达到某个值的最小位置(类似于lower_bound)。根据引用[2]中提到的“树状数组二分”,它实际上是种倍增方法。其基本思想是从大到小枚举幂次(即从高位到低位),判断当前指针加上该幂次后是否满足条件,如果满足则移动指针,并更新累加值。下面是树状数组上倍增的模板,用于查找最小的位置pos,使得1到pos的前缀和大于等于某个给定值s(即求前缀和的lower_bound):注意:树状数组的索引从1开始。实现步骤:1.初始化个指针pos=0,个累加器sum=0。2.枚举幂次(从大到小,即从log(n)到0),每步尝试将指针移动2的幂次(即step=1<<i),如果移动后不超过数组长度n,且累加当前树状数组的值后仍小于s,则移动指针并累加。3.最后,如果我们需要的是前缀和大于等于s的最小位置,那么返回pos+1(因为循环结束时,sum是小于s的最大前缀和,所以下个位置就是所求位置)。注意:树状数组的每个节点存储的是特定区间的和,倍增的过程就是利用树状数组的层次结构来快速逼近目标位置。代码实现如下(假设树状数组已经建好,数组名为c,大小为n+1):```cpp#include<iostream>#include<cmath>usingnamespacestd;constintmaxn=100000;intn;//数组大小intc[maxn];//树状数组//树状数组基本操作intlowbit(intx){returnx&-x;}voidadd(inti,intx){while(i<=n){c[i]+=x;i+=lowbit(i);}}intquery(inti){//查询前缀和[1,i]intsum=0;while(i){sum+=c[i];i-=lowbit(i);}returnsum;}//树状数组上倍增:查找最小的位置pos,使得前缀和>=sintfind(ints){intpos=0;intsum=0;//计算最大的幂次,2^k<=nintk=0;while((1<<(k+1))<=n)k++;//求出最大的k使得2^k<=nfor(inti=k;i>=0;i--){intnext_pos=pos+(1<<i);if(next_pos<=n&&sum+c[next_pos]<s){//如果加上这个区间后仍然小于s,则移动指针并累加pos=next_pos;sum+=c[next_pos];}}//此时,pos位置的前缀和是小于s的最大前缀和,所以pos+1位置的前缀和>=s(如果存在的话)returnpos+1;}intmain(){//初始化树状数组等操作//...return0;}```注意:上面的倍增函数find中,我们直接使用了树状数组的c数组。这是因为树状数组的每个节点c[i]存储的是区间[i-lowbit(i)+1,i]的和。在倍增过程中,我们直接使用c数组的值,而不需要调用query函数(因为query函数是O(logn)的,而倍增过程本身也是O(logn),如果调用query就会变成O(log^2n))。所以倍增过程中直接使用c数组的值是关键。但是,上面的代码中,我们在倍增过程中直接使用了c数组,这要求我们在树状数组更新时,已经维护好了c数组。因此,在更新时我们使用add函数,它会更新所有相关的c数组元素。另外,注意倍增过程中,我们枚举的步长是2的幂次,从大到小枚举。每次尝试移动步长,如果移动后位置不超过n且累加后仍然小于目标值s,则移动并累加。这样,最后我们得到的pos是满足前缀和小于s的最大位置,因此pos+1就是满足条件的最小位置。如果整个数组的前缀和都小于s,那么函数将返回n+1。引用[2]中提到的“树状数组二分”正是这种倍增方法。它比线段树二分更简洁,代码更短。相关问题:1.树状数组倍增的应用场景有哪些?2.树状数组倍增和线段树二分有什么区别?3.如何理解树状数组倍增的时间复杂度?</think>### 树状数组倍增模板及实现 树状数组的倍增是种高效查询技术,利用二进制位运算快速定位前缀和的分界点。核心思想是通过**高位到低位的二进制枚举**(类似倍增)实现 $O(\log n)$ 复杂度的单次查询,常用于解决前缀和单调性问题(如求第 $k$ 小元素)。 #### 倍增查询模板代码 ```cpp #include <iostream> using namespace std; const int MAXN = 100010; int tree[MAXN], n; // tree: 树状数组, n: 数据规模 int lowbit(int x) { return x & -x; // 获取最低位的1 } void add(int pos, int val) { // 单点更新 for (; pos <= n; pos += lowbit(pos)) tree[pos] += val; } int find_kth(int k) { // 倍增查询:找到前缀和≥k的最小位置 int pos = 0, sum = 0; for (int i = 20; i >= 0; i--) { // 枚举二进制位 (2^20 > 1e6) int next_pos = pos + (1 << i); // 满足三条件:不越界、不超k、区间和有效 if (next_pos <= n && sum + tree[next_pos] < k) { sum += tree[next_pos]; pos = next_pos; } } return pos + 1; // 返回目标位置 } int main() { cin >> n; // 初始化树状数组 (需先调用add初始化) // 查询示例: cout << find_kth(5); // 找前缀和≥5的首位置 } ``` #### 关键逻辑说明 1. **二进制枚举** 从高位向低位枚举幂次(`i=20` 到 `0`),相当于按 $2^{20}, 2^{19}, ..., 1$ 的步长试探移动。 2. **安全移动条件** `next_pos = pos + (1 << i)` 需满足: - 位置不越界:`next_pos <= n` - 累加和仍小于目标:`sum + tree[next_pos] < k` - 此时移动指针并累加区间和 3. **定位原理** 循环结束时: - `sum` 是小于 `k` 的最大前缀和 - `pos+1` 是首个满足前缀和 $≥k$ 的位置 #### 应用场景 1. 动态求解第 $k$ 小元素(配合桶计数) 2. 有序序列的快速二分查找 3. 离线查询的优化处理(如区间统计问题) > **复杂度优势**:相比线段树二分节省约 50% 常数时间,代码更简洁[^2]。 --- ### 相关问题 1. 树状数组倍增和普通二分的性能差异具体体现在哪些场景? 2. 如何利用倍增技术动态维护区间第 $k$ 大元素? 3. 倍增过程中如果遇到非单调前缀和会有什么问题?如何解决? [^1]: 树状数组的基础结构参考自树状数组模板 [^2]: 倍增技术实现参考树状数组二分模板
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值