算法竞赛中的Lowbit
“Lowbit”在程序设计里是一个很常用的位运算概念,尤其在二叉索引树(Fenwick Tree)等数据结构中非常重要。下面我会从基础原理、计算方法、常见应用三个方面,用通俗易懂的方式给你详细讲解。
一、Lowbit 的含义与原理
-
概念定义
对任意一个正整数x,我们称它的 lowbit(也叫“最低位的 1”)为它二进制表示中最低位的那个 1 对应的值。
换句话说,如果把x写成二进制,最低位的 1 出现在第k个位置(从右往左数,第 1 位是最低位),那么 lowbit(x) 就是 \(2^{k-1}\) (即该位置对应的权值)。 -
位运算公式
最常见的计算方法是:lowbit(x) = x & (-x)这里的
-x指的是对x取二进制补码(即先按位取反再加 1)。- 在二进制补码表示中,
−x就相当于把x的所有位取反再加 1。 - 当你把
x与-x做按位与 (&) 时,结果正好是“x最低位的那个 1” 保留,其它所有位都变为 0。
举例说明:
-
设
x = 12,二进制是1100。-
-x = -12的补码表示是先将12的二进制0000 1100取反变成1111 0011,再加 1 得到1111 0100(即补码形式的 -12)。 -
再做
1100 & 1111 0100,相当于:0000 1100 (12) &1111 0100 (-12 补码) ---------- 0000 0100 (结果 4) -
结果是
0100,也就是十进制的 4。 -
这正是 12(二进制 1100)最低位的那个 1 所代表的值:2²=4。
-
-
再看
x = 20(二进制10100)。-20的补码过程:20取反01011,再加1 得到01100(补码形式)。20 & (-20)即10100 & 01100 = 00100,也就是 4。- 对应原数 20(10100)中从右往左第 3 位的那个 1(权值 2²=4)。
- 在二进制补码表示中,
-
为什么能“挑出”最低位的 1?
-
二进制补码
-x会把x的最低位 1 及其右边所有位“保留”最后一个 1,在它右边位置全部变为 0。左边高位则因取反产生不相同。 -
具体推演一下:
- 假设
x从右往左遇到第一个 1 的位置为第 k 位,那么它右边(第 1~k-1 位)全是 0。 -x在加 1 时,会造成第 1~k-1 位由 0 变为 0(因为低位全为 0,加 1 会进到第 k 位),第 k 位本来是 1,取反后是 0,加 1 又变为 1。此时第 k 位在-x中是 1,而在x中是 1。x & (-x)对于第 k 位,两个都是 1,就保留了 1;对于比 k 位更低的位置,x为 0,与任何位做&都是 0;对于第 k 位以上的位置,由于取反后两者必然至少有一个 0,与运算都得 0。
- 假设
-
所以结果就是一个仅在第 k 位上为 1,而其它位全 0 的数,也即 lowbit。
-
二、Lowbit 的具体计算与常见代码示例
1. 单纯计算 lowbit
最经典的 C/C++ 写法:
int lowbit(int x) {
return x & -x;
}
- 输入: 任意正整数
x。 - 输出:
x在二进制表示中最低位那个 1 所对应的值。
你可以在调试时多做几个测试,印证一下结果:
#include <iostream>
using namespace std;
int lowbit(int x) {
return x & -x;
}
int main() {
int a[] = {1, 2, 3, 4, 5, 6, 12, 20, 37};
for (int v : a) {
printf("lowbit(%d) = %d\n", v, lowbit(v));
}
return 0;
}
期望输出:
lowbit(1) = 1 // 1 的二进制 0001,最低位是第 1 位
lowbit(2) = 2 // 2 的二进制 0010,最低位是第 2 位
lowbit(3) = 1 // 3 的二进制 0011,最低位是第 1 位
lowbit(4) = 4 // 4 的二进制 0100,最低位是第 3 位
lowbit(5) = 1 // 5 的二进制 0101,最低位是第 1 位
lowbit(6) = 2 // 6 的二进制 0110,最低位是第 2 位
lowbit(12) = 4 // 12(1100) 最低位 1 在第 3 位 => 2^2 = 4
lowbit(20) = 4 // 20(10100) 最低位 1 在第 3 位 => 4
lowbit(37) = 1 // 37(100101) 最高到第 6 位,最低 1 在第 1 位 => 1
2. 在 Python 里模拟
如果你在 Python 中想要写,等价地可以用:
def lowbit(x):
return x & -x
# 测试
for v in [1,2,3,4,5,6,12,20,37]:
print(f"lowbit({v}) = {lowbit(v)}")
原理都是一样的,Python 里也是用补码来表示负数。
三、Lowbit 的典型应用场景
1. Fenwick Tree(二叉索引树)
Fenwick Tree(也叫 BIT,Binary Indexed Tree),是一种支持“前缀和查询”和“单点更新”都能在 \(O(\log n)\) 时间内完成的数据结构。而 lowbit 在 Fenwick Tree 的实现里至关重要,几乎贯穿于所有操作。
(1)Fenwick Tree 原理简述
-
给定一个长度为
n的数组A[1..n],我们想要频繁地:- 查询前缀和
sum(1..i) = A[1] + A[2] + ... + A[i]; - 更新某个位置
A[i] += Δ。
- 查询前缀和
-
直接用线段树也能做到 \(O(\log n)\),但 Fenwick Tree 实现更简洁,常见于竞赛/培训场景。
Fenwick Tree 内部维护一个辅助数组 C[1..n],其中:
-
C[i]存储了一段对应区间的部分和,具体来说:\[ C[i] = A[i - lowbit(i) + 1] + A[i - lowbit(i) + 2] + \dots + A[i] \]换句话说,
C[i]记录从位置i - lowbit(i) + 1到i这一段区间的和。
(2)更新操作:add(i, Δ)
如果想把原数组中下标 i 处加上 Δ,那么我们需要:
- 先让
C[i]加上 Δ; - 然后让
i往“下一个”要负责包含i位置的节点位置移动。
那么“下一个”节点位置用到的就是i += lowbit(i)。
这是因为在 Fenwick Tree 的设计里,所有「包含 i 的 C[]」的下标,都满足“下标 j 的那段区间能覆盖 i”,而 j 的遍历就是从 i 开始,不断 + lowbit(j),直到 j 超出 n 为止。
更新伪码:
void add(int i, int Δ) {
while (i <= n) {
C[i] += Δ;
i += lowbit(i);
}
}
- 说明:一开始先给
C[i]加上 Δ;然后把 i 移动到 “下一个”要负责的节点,直到越界。
(3)查询前缀和:prefix_sum(i)
想要查询 A[1] + A[2] + ... + A[i],可以把答案拆成若干段,就是从 i 开始,取 C[i],然后往 i 减去 lowbit(i) 的方向移动,一直减到 0 即可。
伪码如下:
int prefix_sum(int i) {
int s = 0;
while (i > 0) {
s += C[i];
i -= lowbit(i);
}
return s;
}
- 这里的思路类似贪心:
C[i]存的正好是从i - lowbit(i) + 1到i的那段和,减去这段后把 i 跳到i - lowbit(i),再继续拆更低位,直到 i 变到 0。
(4)Fenwick Tree 整体代码
以下给出一个完整版(C++)示例,帮助你理清思路:
#include <bits/stdc++.h>
using namespace std;
struct FenwickTree {
int n;
vector<long long> C; // 用 long long 以防累加时溢出
FenwickTree(int _n) : n(_n), C(_n+1, 0) {}
// lowbit 函数
int lowbit(int x) {
return x & -x;
}
// 单点更新:把 A[i] += delta
void add(int i, long long delta) {
// i 从 1 开始到 n
while (i <= n) {
C[i] += delta;
i += lowbit(i);
}
}
// 查询前缀和:sum A[1..i]
long long prefix_sum(int i) {
long long s = 0;
while (i > 0) {
s += C[i];
i -= lowbit(i);
}
return s;
}
// 查询区间和:sum A[l..r]
long long range_sum(int l, int r) {
return prefix_sum(r) - prefix_sum(l-1);
}
};
int main() {
int n = 10; // 假设长度为 10
FenwickTree tree(n);
// 初始化数组 A[1..n],先全部为 0
// 如果要一开始就批量赋值,可以用 add 操作逐个加
// 举例:给 A[3] 加上 5
tree.add(3, 5);
// 给 A[7] 加上 2
tree.add(7, 2);
// 查询 A[1..8] 的前缀和
cout << "sum(1..8) = " << tree.prefix_sum(8) << endl;
// 查询 A[3..7] 的区间和
cout << "sum(3..7) = " << tree.range_sum(3, 7) << endl;
return 0;
}
-
运行思路:
tree.add(3,5):会依次修改C[3],C[4],C[8]等位置;tree.add(7,2):修改C[7],C[8]等位置;prefix_sum(8):相当于取C[8] + C[0],其中C[8]已经包涵了之前对 3、7 的更新,正好符合前缀和逻辑。
2. 其他位运算场景
除了 Fenwick Tree 之外,lowbit(即 x & -x)在竞赛/位运算中也常用于多种场景,下面罗列几个典型用途:
-
快速判断一个数是否为 2 的幂
- 如果
x是 2 的幂,那么它的二进制表示只有一位是 1,其余全是 0。那么lowbit(x)就等于x本身。 - 因此,可以用
lowbit(x) == x来判断 “x 是否为 2 的幂”。
bool isPowerOfTwo(int x) { if (x <= 0) return false; return (x & -x) == x; } - 如果
-
把二进制表示中最低位的 1 “去掉”
-
如果你想把一个数
x的二进制表示里的最低位那个 1 变成 0,常见写法是:x = x - lowbit(x);或更“位运算”风格地:
x = x & (x - 1); -
这样做在统计二进制中 1 的个数时非常高效(每次把最右侧的 1 “消去”),时间复杂度是 \(O(\text{popcount}(x))\),一般比逐位左移判断快。
-
例如,要统计
x中一共有多少个 1,可以写:int countOnes(int x) { int cnt = 0; while (x) { x &= (x - 1); // 消去最低位的 1 cnt++; } return cnt; }
-
-
枚举子集 / 组合数学
-
在需要对二进制掩码、子集中 “从小到大枚举” 的场合,常常会用到 lowbit。例如,给定一个掩码
mask,想要找到它所有非空子集,可以用如下模板:for (int sub = mask; sub; sub = (sub - 1) & mask) { // 这里 sub 会遍历 mask 的所有子集(从 mask 本身到 0 依次减) } -
这里用到的是
(sub - 1) & mask,而sub - 1的操作本身也会自动消去最低位的 1,配合 &mask,就能保证子集不越界。 -
如果只是想快速取出子集中 “当前子集中末尾最低位那一位”,也可写
low = sub & -sub来快速定位。
-
-
某些 DP 状态压缩技巧
-
在状态压缩 DP 中,如果状态用“位掩码”来表示,比如有
n个开关,每个状态用一个mask(0~(1<<n)-1)表示开关的开/关情况。我们想要快速从某个状态mask中取出最低位开的那个开关位置pos,可以做:int lb = mask & -mask; // 得到最低位的值,例如 001000 表示第 4 位 int pos = __builtin_ctz(lb); // GCC/Clang 提供的内建函数,返回 lb 最低位 1 的索引(从 0 开始) -
这样就不用写循环去从最低位往上找了,效率更高。
-
四、一步步图示理解(以 12 为例)
假设我们要理解 lowbit(12) 的整个计算过程,按二进制画出来会更清晰:
x = 12 (十进制)
= 1100 (二进制)
-
先写出
x的二进制:0000 1100(我们假设用 8 位表示,左边补 0)。 -
计算
-x(补码表示):x的原码(8 位) =0000 1100- 取反 (
~x) =1111 0011 - 加 1 =
1111 0100(这就是-12的补码)
-
x & (-x)即:
0000 1100 (12)
AND 1111 0100 (-12)
= 0000 0100 (= 4)
- 结果只有第 3 位是 1,其它全为 0。也就是你在原数
12(1100)中最低那位 1 对应的值。
五、为什么要学习 Lowbit?
-
理解底层位运算的奥妙
x & -x之所以能“挑出最低那位 1”,是因为二进制补码里负数的表示原理。掌握它能够让你对位运算的本质更明晰,也能在需要高性能、常见竞赛里迅速写出常见技巧。
-
Fenwick Tree 及其相关数据结构
- 在竞赛或需要高效处理区间前缀和/区间更新时,Fenwick Tree 是一个非常简洁且高效的方案。lowbit 是 Fenwick Tree 的灵魂,一旦理解了 lowbit,就能轻松驾驭这类题目。
-
位运算技巧在算法竞赛中非常常见
- 比如要高效枚举二进制子集、统计二进制中 1 的个数、判断 2 的幂、甚至在数论/组合数学题中,lowbit 能派上大用场。
- 有时候面试题会考察你对“低位掩码”技巧的熟悉度,比如“快速删除二进制最低位的 1”,“计算某数最低位 1 的位置”等等。
六、举例题目:Fenwick Tree 基础操作
题目示例:
你有一个长度为 10 的数组A[1..10],初始都为 0。
- 对第 3 个元素加 5;
- 对第 7 个元素加 2;
- 求前缀和
sum(1..8);- 求区间和
sum(3..7)。请写出 Fenwick Tree 的实现,并回答上述查询结果是多少。
解:
-
初始
A[i] = 0,辅助数组C[i]也全 0。 -
操作 1:
add(3,5)-
从
i=3开始:- 更新
C[3] += 5; - 然后
3 + lowbit(3) = 3 + 1 = 4,更新C[4] += 5; 4 + lowbit(4) = 4 + 4 = 8,更新C[8] += 5;8 + lowbit(8) = 8 + 8 = 16,超过 10,停止。
- 更新
-
此时
C数组变化(只列第 1~10 位):C = [0, 0, 0, 5, 5, 0, 0, 5, 0, 0] i:1 2 3 4 5 6 7 8 9 10
-
-
操作 2:
add(7,2)-
从
i=7开始:C[7] += 2;7 + lowbit(7) = 7 + 1 = 8,C[8] += 2;8 + lowbit(8) = 8 + 8 = 16,越界结束。
-
更新完毕后
C变成:C = [0, 0, 0, 5, 5, 0, 0, 2, 7, 0, 0] i:1 2 3 4 5 6 7 8 9 10其中
C[8]原先是 5,现在加 2 变成 7。
-
-
操作 3:查询
prefix_sum(8)-
从
i = 8开始:res += C[8] = 7;- 然后
i = 8 - lowbit(8) = 8 - 8 = 0,终止。
-
所以前缀和
sum(1..8) = 7。
-
-
操作 4:查询
range_sum(3,7) = prefix_sum(7) - prefix_sum(2)-
先算
prefix_sum(7):i=7:res = C[7] = 2;i = 7 - lowbit(7) = 7 - 1 = 6;i=6:res += C[6] = 0(因为 C[6] 一直没变)i = 6 - lowbit(6) = 6 - 2 = 4;i=4:res += C[4] = 5,此时res = 2 + 5 = 7;i = 4 - lowbit(4) = 4 - 4 = 0,终止。- 所以
prefix_sum(7) = 7。
-
再算
prefix_sum(2):i=2:res = C[2] = 0;i = 2 - lowbit(2) = 2 - 2 = 0,终止。- 所以
prefix_sum(2) = 0。
-
因此
range_sum(3,7) = 7 - 0 = 7。
-
总结:
sum(1..8) = 7sum(3..7) = 7
—— 与我们直觉一致,只有对索引 3 和 7 做了更新,分别加了 5 和 2,因此 A = [0,0,5,0,0,0,2,0,0,0],前 8 项和正好是 7;第 3~7 项也只包含了 A[3]=5 和 A[7]=2,共 7。
七、小结
-
lowbit(x) = x & (-x),它可以“挑出” x 二进制表示中最右边的那个 1 对应的值。
-
lowbit 的原理源自“二进制补码”对负数的表示方式。
-
在 Fenwick Tree(Binary Indexed Tree)中,用 lowbit 轻松实现「单点更新」和「前缀和查询」,使得两种操作都可以 \(O(\log n)\) 完成。
-
其他场景中,lowbit 常用于:
- 判断是否为 2 的幂;
- 从一个数中“去掉”最低位的 1;
- 快速枚举二进制掩码的子集;
- 状态压缩 DP 中快速定位最低位 1 的位置;
- 以及各种需要对二进制尾部 1 进行操作的算法技巧。
掌握 lowbit,不仅能让你写出简洁高效的 Fenwick Tree,还能在竞赛中处理各类位运算题目时更加得心应手。希望这番讲解能帮你彻底搞懂 “lowbit 能干什么”,从原理到代码到应用都有所了解。如果你还有什么进一步的疑问,或者想看更多例题演示,随时欢迎继续交流!

浙公网安备 33010602011771号