树状数组 & lowbit()

本文深入讲解了树状数组的工作原理及实现方式,包括其在处理数组区间查询与更新操作中的高效应用。通过二进制视角解析树状数组如何简化计算过程,并通过具体实例介绍了lowbit函数的使用方法。

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

看了很多大佬的博客,每看一篇博客懂一部分,总算是大概理解了树状数组这个神奇又强大的东西;

在这里我做个整合,把我认为好的部分摘录下来;

 

参考博客1:https://blog.youkuaiyun.com/flushhip/article/details/79165701

参考博客2:https://blog.youkuaiyun.com/int64ago/article/details/7429868

下面是这两位大佬写的乐章合奏篇~~~~~~~~


 

 首先,我们需要通篇以二进制的视角来学习树状数组,树状数组就是应用二进制的特点来简化不必要的计算过程,利用位运算以实现高效的增删改查;

 说到树状数组,顾名思义,这两幅图完全体现了它的核心思想;

   

树状数组的作用

  首先我们搞明白树状数组是用来干嘛的,现在有一个这样的问题:有一个数组a,下标从0n-1,现在给你w次修改,q次查询,修改的话是修改数组中某一个元素的值;查询的话是查询数组中任意一个区间的和;

  这个问题很常见,首先分析下朴素做法的时间复杂度,修改是O(1)O(1)的时间复杂度,而查询的话是O(n)O(n)的复杂度,总体时间复杂度为O(qn)O(qn);可能你会想到前缀和来优化这个查询,我们也来分析下,查询的话是O(1)O(1)的复杂度,而修改的时候修改一个点,那么在之后的所有前缀和都要更新,所以修改的时间复杂度是O(n)O(n),总体时间复杂度还是O(qn)O(qn)。

  可以发现,两种做法中,要么查询是O(1)O(1),修改是O(n)O(n);要么修改是O(1)O(1),查询是O(n)O(n)。那么就有没有一种做法可以综合一下这两种朴素做法,然后整体时间复杂度可以降一个数量级呢?有的,对,就是树状数组。

 

树状数组的思想

  假设数组a是我们增删改查的对象,但树状数组的思想维护的是 c 数组, 从上面的图我们可以看到,c[i]不是通常意义上的1~i 元素的和;

  由图来看看c数组的规则,其中c8 = c4+c6+c7+a8,c6 = c5+a6……先不必纠结怎么做到的,我们只要知道c数组的大致规则即可,很容易知道c8表示a1~a8的和,但是c6却是表示a5~a6的和,为什么会产生这样的区别的呢?或者说发明她的人为什么这样区别对待呢?答案是,这样会使操作更简单!看到这相信有些人就有些感觉了,为什么复杂度被lg了呢?可以看到,c8可以看作a1~a8的左半边和+右半边和,而其中左半边和是确定的c4,右半边其实也是同样的规则把a5~a8一分为二……继续下去都是一分为二直到不能分,可以看看B图。说白了树状数组就是巧妙的利用了二分!

  具体如何一分为二的规则通过 lowbit() 函数实现;

lowbit函数

lowbit这个函数的功能就是求某一个数的二进制表示中最低的一位1,举个例子,x = 6,它的二进制为110,那么lowbit(x)就返回2

那么怎么求lowbit呢?

  • 先消掉最后一位1,然后再用原数减去消掉最后一位1后的数,答案就是lowbit(x)的结果;

  • 第二种方法就是计算机组成原理课上老师教过我们求负数的补码的简便方法:把这个数的二进制写出来,然后从右向左找到第一个1(这个1就是我们要求的结果,但是现在表示不出来,后来的操作就是让这个1能表示出来),这个1不要动和这个1右边的二进制不变,左边的二进制依次取反,这样就求出的一个数的补码,说这个方法主要是让我们理解一个负数的补码在二进制上的特征,然后我们把这个负数对应的正数与该负数与运算一下,由于这个1的左边的二进制与正数的原码对应的部分是相反的,所以相与一定都为0,;由于这个1和这个1右边的二进制都是不变的,因此,相与后还是原来的样子,故,这样搞出来的结果就是lowbit(x)的结果。

两种方法对应的代码依次如下:

1 int lowbit(x) 
2 {   
3     return x - (x & (x - 1));
4 }
int lowbit(x) 
{   
    return x & -x;
}

树状数组的实现

更新操作:只要更新修改这个点会影响到的c数组,假设现在修改6(110)这个点,依据树状数组的性质三,它影响的直系父层就是c[6(110) + lowbit(6(110))] = c[8(1000)],但是它肯定不是只影响直系父层,上面所有包含这一层和的层都要更新,但是我们把这个更新传递给直系父层c[8],8这个点的直系父层是c[16],依次类推地更新就行了;

查询前缀和:

1 int sum(int x, ArrayInt c, int n)
2 {
3     int ret = 0;
4     for ( ; x > 0; ret += c[x], x -= lowbit(x));
5     return ret;
6 }

更新操作:

1 void update(int x, int val, ArrayInt c, int n)
2 {
3     for ( ; x <= n; c[x] += val, x += lowbit(x));
4 }

 

转载于:https://www.cnblogs.com/liubilan/p/9457083.html

&lt;think&gt;首先,用户的问题是:&ldquo;我想理解树状数组lowBit运算的作用请问树状数组lowBit运算的功能和实现原理&rdquo;。这是关于树状数组中的lowBit运算的。根据系统级指令,我需要:-所有行内数学表达式使用$...$格式。-独立公式使用$$...$$格式并单独成段。-LaTeX语法正确。-使用中文回答。-生成相关问题。-在回答中引用的段落末尾自然地添加引用标识。-回答结构清晰,帮助用户逐步解决问题,尽量保证回答真实可靠。参考提供的引用:-引用[1]:解释了lowbit函数:intlowbit(intx)returnx&amp;(-x);[^1]-引用[2]:讨论了lowbit运算和树状数组的二进制原理[^2]-引用[3]:描述了树状数组如何利用lowbit实现单点修改和前缀和查询[^3]-引用[4]:给出了lowbit运算的例子和更新函数[^4]-引用[5]:提供了树状数组的C++实现,包括lowbit宏定义和函数[^5]lowbit运算在树状数组中的作用是获取一个整数的最低有效位(LeastSignificantBit),具体来说,是获取二进制表示中最低位的1所对应的值。例如,对于x=6(二进制110),lowbit(6)=2(二进制10),因为最低位的1在第二位。实现原理:lowbit(x)=x&amp;(-x)。这是因为在补码表示中,-x是x的每一位取反后加1。所以,x&amp;(-x)会保留x中最低位的1,其他位都变为0。在树状数组中,lowbit用于导航数组的索引。树状数组(BinaryIndexedTree,BIT)是一种用于高效计算前缀和的数据结构,支持单点更新和前缀查询。-更新操作:当更新索引i处的值时,需要更新所有包含i的区间。使用lowbit,我们可以从i开始,逐步增加lowbit(i),直到覆盖整个数组。-查询操作:当查询前缀和到索引i时,从i开始,逐步减少lowbit(i),累计值。结构清晰的回答:1.**介绍lowbit的功能**:解释它在树状数组中的角色。2.**实现原理**:详细说明lowbit(x)=x&amp;(-x)的二进制原理。3.**在树状数组中的应用**:通过例子展示如何在更新和查询中使用lowbit。4.**生成相关问题**:在末尾添加相关问题。使用中文回答。添加引用标识:在引用引用内容时自然添加。最终回答结构:-先回答核心问题。-逐步解释。-使用数学表达式。-结尾添加相关问题。&lt;/think&gt;### 树状数组lowBit 运算的功能与实现原理 树状数组(Binary Indexed Tree, BIT)是一种高效的数据结构,用于动态维护序列的前缀和,支持单点更新和前缀查询操作。其中,`lowBit` 运算在树状数组中扮演着核心角色,它基于二进制位运算实现,用于导航数组索引并优化操作效率。下面我将逐步解释其功能和实现原理,确保内容清晰易懂。 #### 1. **lowBit 的功能** - **核心作用**:`lowBit` 运算用于获取一个整数在二进制表示中最低有效位的值(即最低位的 1 所对应的数值)。例如: - 对于整数 $x = 6$(二进制 $110$),$lowBit(6) = 2$(二进制 $10$),因为最低位的 1 在第二位。 - 对于 $x = 7$(二进制 $111$),$lowBit(7) = 1$(二进制 $1$)。 - **在树状数组中的应用**: - **更新操作(单点修改)**:当更新索引 $i$ 处的值时,树状数组需要更新所有包含 $i$ 的父节点。`lowBit` 用于计算下一个需要更新的索引:从 $i$ 开始,通过 $i + lowBit(i)$ 跳跃到父节点,直到覆盖整个数组范围。这确保了更新操作的时间复杂度为 $O(\log n)$。 - **查询操作(前缀和)**:当查询前缀和到索引 $i$ 时,`lowBit` 用于从 $i$ 开始,通过 $i - lowBit(i)$ 跳跃到前一个子区间,累计求和。时间复杂度同样为 $O(\log n)$。 - 这种机制使树状数组比朴素方法(如遍历数组)更高效,特别适合处理动态数据[^3][^5]。 #### 2. **lowBit 的实现原理** - **数学表达式**:`lowBit` 通过位运算实现,其定义为: $$ lowBit(x) = x \&amp; (-x) $$ 其中: - $x$ 是输入整数(通常为非负整数)。 - $\&amp;$ 表示按位与运算。 - $-x$ 是 $x$ 的二进制补码表示(在计算机中,负数的补码等于原码取反后加 1)。 - **二进制原理解释**: - 在补码系统中,$-x$ 的计算相当于对 $x$ 的每一位取反(按位非运算),然后加 1。例如: - 设 $x = 6$(二进制 $0110$),则 $-x$ 的补码为:取反得 $1001$,加 1 得 $1010$(即 $-6$)。 - 执行 $x \&amp; (-x)$:$0110 \&amp; 1010 = 0010$(十进制 $2$),这正是 $lowBit(6)$。 - 为什么这样工作?因为 $-x$ 的补码保留了 $x$ 最低位 1 的位置(该位在取反加 1 后变为 1),而其他位变为 0。按位与操作后,仅最低位 1 被保留,其他位清零[^2][^4]。 - **代码实现**:在编程中,`lowBit` 通常定义为宏或内联函数(如 C++ 示例): ```c #define lowbit(x) ((x) &amp; (-x)) // 宏定义实现 ``` 或作为函数: ```c int lowbit(int x) { return x &amp; (-x); // 直接返回位运算结果 } ``` 这个实现简洁高效,依赖于硬件的位运算支持[^1][^5]。 #### 3. **实际应用示例** 以树状数组的更新和查询操作为例(基于引用[5]的代码): - **更新操作**:当修改索引 $x$ 处的值时,循环更新所有相关节点: ```c void update(int x, int v) { for (int i = x; i &lt;= MAXV; i += lowbit(i)) { // 使用 lowbit 跳到父节点 c[i] += v; // 更新当前节点 } } ``` - 例如,$x = 3$(二进制 $011$),$lowBit(3) = 1$,则更新序列为 $3 \rightarrow 4$(因为 $3 + 1 = 4$),再跳到 $8$($lowBit(4) = 4$)等。 - **查询操作**:计算前缀和到索引 $x$: ```c int getSum(int x) { int sum = 0; for (int i = x; i &gt; 0; i -= lowbit(i)) { // 使用 lowbit 跳到前一个区间 sum += c[i]; // 累计和 } return sum; } ``` - 例如,$x = 7$(二进制 $111$),$lowBit(7) = 1$,则查询序列为 $7 \rightarrow 6$($7 - 1 = 6$),再跳到 $4$($lowBit(6) = 2$)等[^3][^5]。 #### 4. **为什么 lowBit 关键** - **效率优势**:`lowBit` 利用二进制特性,将树状数组的树形结构扁平化,避免了复杂的指针操作,使代码简洁且缓存友好。 - **适用场景**:适用于需要频繁更新和查询前缀和的问题,如动态统计序列中元素排名(例如,&ldquo;统计左边比它小的元素个数&rdquo;)[^4][^5]。 - **局限性**:树状数组主要针对前缀和优化;对于区间修改或复杂查询,线段树可能更灵活,但树状数组在代码简洁性上优势明显[^3]。 通过以上解释,您应该对 `lowBit` 的功能和原理有了清晰理解。如果还有疑问,欢迎继续探讨!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值