1. 题目描述
给你一个数组
nums
和一个整数k
。你需要找到nums
的一个子数组
,满足子数组中所有元素按位或运算
OR
的值与k
的绝对差
尽可能小
。换言之,你需要选择一个子数组nums[l..r]
满足|k-(nums[l] OR nums[l + 1] ... OR nums[r])|
最小
。
请你返回最小
的绝对差值。
子数组
是数组中连续的非空
元素序列。
示例 1:
输入:nums = [1,2,4,5], k = 3
输出:0
解释:子数组 nums[0…1] 的按位 OR 运算值为 3 ,得到最小差值 |3 - 3| = 0 。示例 2:
输入:nums = [1,3,1,3], k = 2
输出:1
解释:子数组 nums[1…1] 的按位 OR 运算值为 3 ,得到最小差值 |3 - 2| = 1 。示例 3:
输入:nums = [1], k = 10
输出:9
解释:只有一个子数组,按位 OR 运算值为 1 ,得到最小差值 |10 - 1| = 9 。提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
1 <= k <= 109题目链接:https://leetcode.cn/problems/find-subarray-with-bitwise-or-closest-to-k/description/?envType=daily-question&envId=2024-10-07
2. 解题思路
- 返回最小的差值,即寻找一个
子数组
,首先想到滑动窗口
,思考下是否具有滑动窗口的条件;- 或运算,运算的结果不变或增加,不会减小,所以随着元素的增加,结果具有单调性;
- 求与k的最小绝对差值,即距离k最近,当小于k时,越大越接近k,当大于k时,越大距离k越远;即刚大于k时,如果在这个范围内没有最小的绝对差值,那值继续增大,也没有最小的绝对差值;
- 思考下,即得到滑入和滑出窗口的条件,即当或的值小于k时,继续向窗口内添加元素,当或值不小于k时停止,然后滑出最左边的一个元素,继续判断或值是否小于k;
3. 实现
3.1 code
class Solution {
public int minimumDifference(int[] nums, int k) {
int res = Integer.MAX_VALUE;
// 31位数组,记录子数组内元素每个位置1的个数
int[] dp = new int[31];
// 右窗口边界,包含r
int r = 0;
// 窗口内所有元素或的值
int tmp = 0;
for (int l = 0; l < nums.length; l++) {
// 终止条件,tmp 大于等于 k,或者r出界
while (tmp < k && r < nums.length) {
// 加上nump[r]每个位置上1的个数
sumBit(nums[r++], dp);
// 计算或的值
tmp = countNum(dp);
// 计算最小值
res = Math.min(res, Math.abs(k - tmp));
}
// 加速点,当tmp == k时,此时最小,返回0
if (tmp == k) {
return 0;
}
// 最左边的元素滑出窗口,减去num[l]每个位置上1的个数
subtractionBit(nums[l], dp);
tmp = countNum(dp);
// 注意,存在 l==r的情况,此时窗口内没有元素,不满足非空元素序列条件
if (tmp > 0) {
res = Math.min(res, Math.abs(k - tmp));
}
}
return res;
}
// 把num每个位上的1累加到数组dp中
private void sumBit(int num, int[] dp) {
for (int i = 0; i < dp.length; i++) {
dp[i] += (num >>> i) & 1;
}
}
// 在dp中去除num每个位上1的个数
private void subtractionBit(int num, int[] dp) {
for (int i = 0; i < dp.length; i++) {
dp[i] -= (num >>> i) & 1;
}
}
// 统计dp中或的值
private int countNum(int[] dp) {
int res = 0;
for (int i = 0; i < dp.length; i++) {
if (dp[i] > 0) {
res |= 1 << i;
}
}
return res;
}
}
3.2 说明
- 使用一个长度位31的int数组,记录每个位1的个数,这样可以快速得到窗口内或的值,不需要遍历窗口内的所有元素求或的值;
- 窗口内没进入一个元素和出去一个元素,就要比较一次当前值是否是答案;
- 无回退的情况,时间复杂度为: O ( n ) O(n) O(n),但是常数时间比较大
3.3 运行结果
4. 优秀答案
阅读一下优秀的示例代码,揣摩一下大神的思路、增长一下见识、学习一下技巧;
4.1 code
public int minimumDifference(int[] nums, int k) {
int l = 0, r = 1;
int ans = Math.abs(nums[0] - k);
// 以右边界为基础,遍历数组
for (r = 1; r < nums.length; r++) {
// 如果和前一个元素相同,前一种已经统计过了,跳过、不再重复统计
if (nums[r] == nums[r - 1]) continue;
// 如果等于k,此时最小,返回结果
if (nums[r] == k) return 0;
// 计算只有num[r]一个元素时,是否是最小值
ans = Math.min(ans, Math.abs(nums[r] - k));
// 如果nums[r] 大于 k,继续往前追加元素,或值只会不变或变大,最小值不会变小,所以跳过
if (nums[r] > k) continue;
// 往左追加元素,当或值 等于 nums[l]时,终止循环(加速项),nums[l]之前已经计算过了,如果或值没有变化,说明不会增加了,退出循环
for (l = r - 1; l >= 0 && ((nums[l] | nums[r]) != nums[l]); l--) {
// 加速项,前缀或值,把后面的值或到前一个值上
nums[l] |= nums[r];
// 计算当前值是否是最小值
ans = Math.min(ans, Math.abs(nums[l] - k));
// 或值大于k时,退出循环,
if (nums[l] > k) break;
// 或值大于k时,获得最优解
if (nums[l] == k) return 0;
}
}
return ans;
}
4.2 说明
- 时间复杂度是 O ( n 2 ) O(n^2) O(n2),最坏情况下无法减枝,l一直遍历到l=0时结束向前的循环;
- 通过巧妙的减枝技巧,使得时间非常的块,非常值得学习;
5. 总结
- 笔记记录
6. 参考资料
- https://leetcode.cn/problems/find-subarray-with-bitwise-or-closest-to-k/description/?envType=daily-question&envId=2024-10-07