关于二进制的一些基础知识
关于二进制的基础知识:从“拆解 2 的幂”到位运算的本质在算法竞赛与底层编程中,二进制无处不在。无论是状态压缩、树状数组,还是快速幂,其底层逻辑都绕不开一个核心命题:任何正整数都可以唯一表示为若干个互不相同的 2 的幂之和。这句话听起来很数学,但只要放到二进制视角下,其本质一目了然:拆解出的 2 的幂的项数,就是该数二进制表示中 1 的个数;每个幂次,对应着每一个 1 所在的具体位(Bit)。一、 为什么任何正整数都能拆成“互不相同的 2 的幂”?1. 直观对应:位权体系我们熟悉的十进制采用位权累加:$$13 = 1 \times 10^1 + 3 \times 10^0$$同理,二进制的每一位权重都是 \(2^k\)(\(k \ge 0\)),且每一位上的取值只有两种:0(不选)或 1(选)。以正整数 \(13\) 为例:转换为二进制:\((13)_{10} = (1101)_2\)按权展开:$$(1101)_2 = 1 \times 2^3 + 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 = 8 + 4 + 1$$你会发现:幂次互不相同:展开式中的项分别是 \(2^3, 2^2, 2^0\)(即 \(8, 4, 1\)),绝不会出现重复项(因为二进制每位逢 2 进 1,单权系数最大只能是 1)。拆解项数 = 1 的数量:\((1101)_2\) 中有 3 个 1,对应拆解出 3 个互不相同的项(\(8, 4, 1\))。唯一性:正整数的二进制表示是唯一的,因此这种互不相同的幂和表示形式也是唯一的。二、 核心性质与算法直觉这一基础性质直接衍生出算法中最常用的几类设计思想:算法思想 / 技巧底层映射逻辑快速幂 (Binary Exponentiation)将指数 \(b\) 拆解为 \(\sum 2^i\),把 \(a^b\) 拆成 \(a^{2^i}\) 相乘,将 \(O(b)\) 降为 \(O(\log b)\)倍增法 (Binary Lifting / LCA / ST表)任意步数跳跃均可拆解为若干次 \(2^k\) 步的组合多重背包二进制拆分将数量 \(s\) 拆解为 \(1, 2, 4, \dots\) 组装出 \([0, s]\) 内任意整数树状数组 (Fenwick Tree)利用 lowbit(x) 提取最低位的 \(2^k\),实现高效前缀划分状态压缩 (State Compression)一个整数的每一位对应一个集合元素,“选/不选”对应“1/0”三、 代码必备:提取与统计二进制中的 1在实际编码中,针对“拆解 2 的幂”和“统计 1 的个数”,有几组经典操作必须熟练掌握。1. lowbit 操作:提取最低位的那个 \(2^k\)树状数组的核心操作,用来快速取出某个数二进制表示中最低位的 1 以及它代表的权值:inline int lowbit(int x) {
return x & -x;
}
原理:计算机中使用补码存储负数,\(-x = \sim x + 1\)。效果:若 \(x = 12 = (1100)_2\),则 lowbit(12) 得到 \((0100)_2 = 4\)(即提取出了最低位的 \(2^2\))。2. 逐项拆解:把数还原为 2 的幂序列利用 lowbit 或 x & (x - 1),可以高效遍历该数包含的所有 2 的幂:
#include <vector>
std::vector<int> decompose(int n) {
std::vector<int> powers;
while (n > 0) {
int p = n & -n; // 取出当前最低位的 2^k
powers.push_back(p);
n -= p; // 消除该位,也可以写成 n &= (n - 1);
}
return powers;
}
int main() {
int n = 13;
auto res = decompose(n);
std::cout << n << " 拆解为: ";
for (int v : res) std::cout << v << " "; // 输出: 1 4 8
std::cout << "\n项数(1的个数): " << res.size() << std::endl;
return 0;
}
3. 高效统计 1 的个数(popcount)方法 A:消除最低位 1(Brian Kernighan 算法)n &= (n - 1) 会抹掉二进制中最后一个 1,循环执行的次数恰好等于 1 的个数,时间复杂度 $O(\text{popcount}(n))$:int count_ones(int n) {
int cnt = 0;
while (n) {
n &= (n - 1);
cnt++;
}
return cnt;
}
方法 B:编译器内置指令(竞赛首选)GCC/Clang 提供了 CPU 指令级支持的内置函数,单次调用 $O(1)$:int cnt32 = __builtin_popcount(x); // 针对 32 位 unsigned int
int cnt64 = __builtin_popcountll(x); // 针对 64 位 unsigned long long
(C++20 标准已将其正式加入标准库头文件 <bit>,可直接使用 std::popcount(x))四、 常见位运算速查掌握基础位运算是操作这些“2 的幂”的前提:// 1. 判断第 k 位是否为 1 (从第 0 位开始)
bool is_set = (n >> k) & 1;
// 2. 将第 k 位置为 1
n |= (1 << k);
// 3. 将第 k 位置为 0
n &= ~(1 << k);
// 4. 将第 k 位取反
n ^= (1 << k);
// 5. 判断一个数是否刚好是 2 的幂 (大于0且只有1个1)
bool is_power_of_two = (n > 0) && ((n & (n - 1)) == 0);
结语“将任意正整数表示为互不相同的 2 的幂之和”不是一个抽象的数论定理,它就是二进制本身的定义。理解了这一点,再去推导倍增的跳跃步长、快速幂的指数折半、以及状压 DP 的状态转移时,思维模型就会变得极为直观。

浙公网安备 33010602011771号