1. 问题重述与理解
今天我们来探讨一道有趣的位运算题目——LeetCode 3315.构造最小位运算数组 II。题目要求我们处理一个质数数组,为每个元素找到一个满足特定位运算条件的最小非负整数。
1.1 题目要求
给定一个长度为n的质数数组nums,我们需要返回一个长度相同的数组ans,其中每个元素ans[i]需要满足:
code复制ans[i] OR (ans[i] + 1) == nums[i]
并且要尽可能使ans[i]最小。如果找不到满足条件的ans[i],则返回-1。
1.2 关键概念解析
位运算OR:按位或运算,对两个数的二进制表示逐位进行或操作。例如5 OR 3:
code复制5: 101
3: 011
OR:111 (即7)
质数特性:题目特别指出nums[i]都是质数(大于1的自然数,只有1和自身两个因数)。这个条件看似简单,但实际上对解题有重要影响。
2. 解题思路分析
2.1 条件分解
我们需要找到满足ans[i] OR (ans[i]+1) == nums[i]的最小ans[i]。让我们先分析这个条件的含义:
- ans[i]和ans[i]+1是连续的两个整数,它们的二进制表示有特殊关系
- OR运算会将两个数的所有置位位合并
- 我们需要这个合并结果正好等于给定的质数
2.2 关键观察
通过分析示例,我们可以发现一些规律:
-
当nums[i]是偶数时,直接返回-1。因为:
- ans OR (ans+1)的结果总是奇数(因为ans和ans+1的末位分别是0和1,OR后末位为1)
-
对于奇数nums[i],我们需要找到满足条件的最小ans[i]
2.3 二进制模式识别
观察成功的案例,我们发现nums[i]的二进制形式有特定模式:
- 示例1中3(11),5(101),7(111)
- 示例2中11(1011),13(1101),31(11111)
这些数字的二进制表示中,1的排列有特定规律。具体来说,有效的nums[i]应该满足:在二进制表示中,所有的1必须是连续的,并且从最低位开始。
例如:
- 7(111)有效
- 5(101)有效
- 6(110)无效(因为低位的0打断了1的连续性)
3. 算法设计与实现
3.1 算法步骤
基于上述观察,我们可以设计如下算法:
- 对于每个nums[i]:
a. 如果是偶数,直接返回-1
b. 如果是奇数:
i. 找到nums[i]二进制表示中最高位的1
ii. 构造一个掩码,覆盖从最高位1开始的所有低位
iii. 计算ans[i] = nums[i] XOR (掩码 >> 1)
3.2 具体实现
让我们用C++实现这个算法:
cpp复制class Solution {
private:
int getMinAns(int n) {
if ((n & 1) == 0) {
return -1; // 偶数直接返回-1
}
int mask = 1;
while ((n & mask) != 0) {
mask <<= 1;
}
return n ^ (mask >> 1);
}
public:
vector<int> minBitwiseArray(vector<int>& nums) {
vector<int> ans(nums.size());
for (int i = 0; i < nums.size(); ++i) {
ans[i] = getMinAns(nums[i]);
}
return ans;
}
};
3.3 算法正确性验证
让我们验证几个测试用例:
-
nums = [3]
- 3(11)是奇数
- mask初始为1(01),第一次循环后变为2(10)
- ans = 3 XOR (2>>1) = 3 XOR 1 = 2
- 检查:2 OR 3 = 3 ✔
-
nums = [5]
- 5(101)是奇数
- mask从1开始,经过两次循环变为100
- ans = 5 XOR (100>>1) = 5 XOR 2 = 7
- 但7 OR 8=15≠5,看起来有问题
看来我们的初步算法有缺陷,需要重新思考。
4. 修正算法思路
4.1 重新分析问题
我们需要ans OR (ans+1) == nums[i]。让我们考虑ans和ans+1的二进制特性:
- ans和ans+1的二进制形式只在最低的0位有差异
- 例如ans=2(10), ans+1=3(11)
- OR操作会将这两个数所有的1位合并
因此,nums[i]必须是这样一个数:它的二进制表示中,所有0位在ans和ans+1中对应位都是0。
4.2 正确解法
正确的解法应该是:
- 对于奇数nums[i],找到其二进制表示中最低的0位
- 将该位及其以下所有位设置为1,这就是ans+1
- ans就是ans+1减去1
但是更简单的方法是注意到nums[i]必须满足:nums[i]+1是2的幂次方,或者nums[i]本身就是全1的形式。
4.3 最终算法
cpp复制class Solution {
private:
int getMinAns(int n) {
if ((n & 1) == 0) {
return -1;
}
// 检查n+1是否是2的幂次方
if ((n & (n + 1)) == 0) {
return (n >> 1);
}
// 否则检查n是否是全1形式
int m = n + 1;
if ((m & (m - 1)) == 0) {
return (m >> 1) - 1;
}
return -1;
}
public:
vector<int> minBitwiseArray(vector<int>& nums) {
vector<int> ans(nums.size());
for (int i = 0; i < nums.size(); ++i) {
ans[i] = getMinAns(nums[i]);
}
return ans;
}
};
5. 复杂度分析与优化
5.1 时间复杂度
对于每个nums[i],我们的算法执行固定次数的位操作,时间复杂度为O(1)。因此总体时间复杂度为O(n),其中n是数组长度。
5.2 空间复杂度
除了返回的数组外,我们只使用了常数空间,因此空间复杂度为O(1)(不考虑返回数组)。
5.3 优化方向
可以预先计算所有可能的质数对应的ans值,建立哈希表进行快速查找。但对于题目给定的约束(nums.length ≤ 100),当前算法已经足够高效。
6. 常见错误与调试技巧
6.1 常见错误
- 忽略nums[i]必须是奇数的条件
- 错误计算掩码的位置
- 混淆位运算优先级,导致逻辑错误
6.2 调试技巧
- 打印中间变量的二进制表示
cpp复制cout << bitset<8>(n) << endl; - 对小规模测试用例手动计算验证
- 特别注意边界条件,如nums[i]=1的情况(虽然题目中nums[i]≥2)
7. 位运算技巧总结
解决这个问题需要熟练掌握以下位运算技巧:
- 检查奇偶性:n & 1
- 寻找最低位1:n & -n
- 检查是否为2的幂次方:n & (n-1) == 0
- 构造掩码:~(0xFF << position)
- 位计数:__builtin_popcount(GCC内置函数)
8. 实际应用场景
这类位运算问题在实际开发中有广泛应用:
- 位图索引和压缩存储
- 网络协议中的标志位处理
- 权限控制系统中的权限掩码
- 高性能计算中的位级优化
理解这些位运算技巧可以帮助我们编写更高效、更简洁的代码。
9. 扩展思考
这个问题可以扩展到更一般的情况:
- 如果nums[i]不一定是质数,解法会有什么变化?
- 如果条件改为ans[i] AND (ans[i]+1) == nums[i],该如何解决?
- 如果允许ans[i]为负数,会影响解法吗?
这些扩展问题可以帮助我们更深入地理解位运算的特性。
10. 个人实践建议
在解决这类位运算问题时,我建议:
- 先从小的测试用例开始,手动计算验证思路
- 多打印中间结果的二进制表示,直观理解位变化
- 总结常见的位运算模式,形成自己的"工具箱"
- 注意运算符优先级,必要时使用括号明确运算顺序
通过不断练习和总结,位运算这类看似复杂的问题也能迎刃而解。