算法02 二进制与位运算
二进制作为计算机底层数据的核心表示方式,其独特的位结构和运算规则在算法设计中有着广泛且关键的应用。以下从基础操作、算法技巧、数据结构、经典问题等多个维度,全面梳理二进制在算法中的应用:
一、基础位运算:算法的“原子操作”
二进制的核心优势在于位运算的高效性(时间复杂度通常为O(1)),常见位运算包括:
与(&):两个位都为1则为1,否则为0。常用于“保留指定位”(如 x & mask 保留mask中1对应的位)。
或(|):两个位有一个为1则为1,常用于“设置指定位为1”(如 x | mask 将mask中1对应的位设为1)。
异或(^):两个位不同则为1,相同则为0。特性: x ^ x = 0 , x ^ 0 = x ,可用于“交换变量”“找唯一出现奇数次的数”等。
左移(<<): x << k 等价于 x * 2^k (无溢出时),快速放大。
右移(>>): x >> k 等价于 x /2^k (整数除法),快速缩小。
取反(~):位反转(0变1,1变0),常用于构造掩码(如 ~0 是全1的二进制)。
这些操作是二进制应用的基础,几乎所有二进制相关算法都依赖它们。
二、状态压缩:用二进制表示“集合状态”
当问题需要表示“元素是否被选择”“状态是否激活”等集合信息时,二进制可将集合状态压缩为一个整数,大幅节省空间并简化操作。
典型场景:
1. 子集枚举
若有 n 个元素,每个子集可表示为一个 n 位二进制数(第 i 位为1表示元素 i 在子集中)。例如, n=3 时,二进制 101 表示子集 {0, 2} 。
枚举所有子集的代码可简化为:
python
for mask in range(0, 1 << n): # 1<<n 即2^n,覆盖所有子集
# mask的二进制表示即为一个子集
2. 动态规划中的状态压缩
例如旅行商问题(TSP):用 dp[mask][u] 表示“已访问的城市集合为 mask (二进制),当前在城市 u 时的最小距离”。其中 mask 的第 i 位为1表示城市 i 已访问。
状态转移依赖位运算: if not (mask & (1 << v)) (判断城市 v 是否未访问)。
3. 位掩码优化
例如“N皇后问题”中,用三个整数(列、主对角线、副对角线)的二进制表示当前禁止放置的位置,通过位运算快速判断合法性。
三、二进制特性:解决数学与计数问题
二进制的表示本身蕴含特殊规律,可用于优化数学问题的求解:
1. 判断2的幂
若 n 是2的幂(如2、4、8),则二进制表示为 100...0 ,满足 n & (n-1) == 0 (如 8(1000) & 7(0111) = 0 )。
2. 计算二进制中1的个数(位计数)
方法1:循环检查每一位( count += n & 1; n >>= 1 )。
方法2:Brian Kernighan算法(高效): while n: n &= n-1; count +=1 (每次清除最后一个1)。
应用:汉明距离(两数二进制不同位的数量,即 bin(x^y).count('1') )。
3. 二进制分解(拆分为2的幂之和)
任何整数 n 都可分解为 n = 2^a + 2^b + ... (如 5=4+1=2^2+2^0 )。
应用:贪心算法中,用最少的2的幂之和表示 n (直接取二进制中1的位置)。
4. 高低位分离与合并
例如将32位整数的高16位和低16位分离: high = (x >> 16) & 0xffff; low = x & 0xffff ,常用于哈希、加密等场景。
四、数据结构:基于二进制的高效设计
部分经典数据结构利用二进制的“分解性”(将问题拆分为2的幂次规模)实现高效操作:
1. 二进制索引树(Fenwick Tree,树状数组)
核心思想:将数组索引分解为二进制表示(如 index=5(101) 可分解为 4+1=2^2+2^0 ),通过树状结构存储前缀和,支持O(log n)的单点更新和前缀和查询。
应用:区间和、逆序对计数等。
2. 线段树的二进制优化
线段树的区间划分基于“二分”,本质是二进制分解(将区间拆分为2的幂次长度的子区间),其查询和更新效率(O(log n))依赖于二进制的快速拆分。
3. 二进制堆
堆的结构是完全二叉树,其父子节点索引关系(左子节点 2*i+1 ,右子节点 2*i+2 )本质是二进制的左移操作( i << 1 等价于 2*i ),因此堆的插入、删除操作可通过位运算快速定位节点。
五、算法优化:用位运算替代传统操作
二进制运算的高效性(硬件级支持)可优化算法的时间复杂度:
1. 替代算术运算
- 乘以/除以2的幂: x * 8 = x << 3 , x // 16 = x >> 4 (比乘法/除法快得多)。
- 判断奇偶: x & 1 == 1 (奇数),比 x % 2 更高效。
- 取模2的幂: x % 8 = x & 7 (因为 8=2^3 ,掩码 7=111 )。
2. 变量交换(无需临时变量)
利用异或特性: a ^= b; b ^= a; a ^= b (原理: a ^ b ^ a = b )。
3. 区间状态快速更新
例如用二进制表示“开关状态”,通过 mask ^ (1 << i) 快速翻转第 i 个开关的状态,比数组操作更简洁。
六、经典问题中的二进制应用
1. 子集和问题
当元素数量 n <= 20 时,可用二进制枚举所有子集( 2^20 ≈ 1e6 ),判断是否存在和为目标值的子集。
2. 最大异或对
给定数组,找一对数使其异或结果最大。解法:按二进制位从高到低构建前缀树(Trie),对每个数在树中找能使异或最大的数(每一位尽量取相反值)。
3. 位运算dp
例如“统计[0, n]中数字二进制不含连续1的个数”,用dp记录每一位的状态(是否为1),通过位转移避免连续1。
4. 格雷码生成
格雷码是相邻数二进制仅差1位的编码,生成公式: gray(n) = n ^ (n >> 1) ,利用二进制异或特性直接构造。
总结
二进制在算法中的应用核心是利用位运算的高效性和二进制表示的结构性,实现从基础操作到复杂问题的优化。无论是状态压缩、数据结构设计,还是数学问题求解,二进制都提供了简洁且高效的思路,是算法工程师必须掌握的基础工具。
七、实用的二进制运算技巧
n&(n-1)
将最低位的1置零
6&5 = 4 (110 101 ->100)
n&(-n)
保留最低位的1,其余位清零
6&-6 = 2 (110)
n|(n-1)
将最低位的1及其右边的位都置为1
6|5 =7 (110|101->111)