2411-按位或最大的最小子数组长度
给你一个长度为 n
下标从 0 开始的数组 nums
,数组中所有数字均为非负整数。对于 0
到 n - 1
之间的每一个下标i
,你需要找出 nums
中一个 最小 非空子数组,它的起始位置为 i
(包含这个位置),同时有 最大 的 按位或
运算值 。
- 换言之,令
Bij
表示子数组nums[i...j]
的按位或运算的结果,你需要找到一个起始位置为i
的最小子数组,这个子数组的按位或运算的结果等于max(Bik)
,其中i <= k <= n - 1
。
一个数组的按位或运算值是这个数组里所有数字按位或运算的结果。
请你返回一个大小为 n
的整数数组 _ _answer
,其中 _ _answer[i]
是开始位置为 i
,按位或运算结果最大,且
最短 子数组的长度。
子数组 是数组里一段连续非空元素组成的序列。
示例 1:
**输入:** nums = [1,0,2,1,3]
**输出:** [3,3,2,2,1]
**解释:**
任何位置开始,最大按位或运算的结果都是 3 。
- 下标 0 处,能得到结果 3 的最短子数组是 [1,0,2] 。
- 下标 1 处,能得到结果 3 的最短子数组是 [0,2,1] 。
- 下标 2 处,能得到结果 3 的最短子数组是 [2,1] 。
- 下标 3 处,能得到结果 3 的最短子数组是 [1,3] 。
- 下标 4 处,能得到结果 3 的最短子数组是 [3] 。
所以我们返回 [3,3,2,2,1] 。
示例 2:
**输入:** nums = [1,2]
**输出:** [2,1]
**解释:** 下标 0 处,能得到最大按位或运算值的最短子数组长度为 2 。
下标 1 处,能得到最大按位或运算值的最短子数组长度为 1 。
所以我们返回 [2,1] 。
提示:
n == nums.length
1 <= n <= 105
0 <= nums[i] <= 109
视频讲解 已出炉,包括本题的末尾列出的部分题目,欢迎点赞三连,在评论区分享你对这场双周赛的看法~
方法一:利用或运算的性质
首先,我们有如下 O(n^2) 的暴力算法:
从左到右正向遍历 nums,对于 x=\textit{nums}[i],从 i-1 开始倒着遍历 nums}[j],更新 nums}[j]=\textit{nums}[j]\ |\ x,如果 nums}[j] 变大,则更新 ans}[j]=i-j+1。
下面来优化该算法。
我们可以把二进制数看成集合,二进制数第 i 位为 1 表示 i 在集合中。两个二进制数的或,就可以看成是两个集合的并集。
对于两个二进制数 a 和 b,如果 a\ |\ b=a,从集合的角度上看,b 对应的集合是 a 对应的集合的子集。
据此我们可以提出如下改进后的算法:
从左到右正向遍历 nums,对于 x=\textit{nums}[i],从 i-1 开始倒着遍历 nums}[j]:
- 如果 nums}[j]\ |\ x\ne\textit{nums}[j],说明 nums}[j] 可以变大(集合元素增多),更新 nums}[j]=\textit{nums}[j]\ |\ x;
- 如果 nums}[j]\ |\ x=\textit{nums}[j],从集合的角度看,此时 x 不仅是 nums}[j] 的子集,同时也是 nums}[k]\ (k<j) 的子集(因为循环保证了每个集合都是其左侧相邻集合的子集),那么后续的循环都无法让元素变大,退出循环;
- 在循环中,如果 nums}[j] 可以变大,则更新 ans}[j]=i-j+1。
1 | class Solution: |
1 | class Solution { |
1 | class Solution { |
1 | func smallestSubarrays(nums []int) []int { |
复杂度分析
- 时间复杂度:O(n\log U),其中 n 为 nums 的长度,U=max(\textit{nums})。由于 2^{29}-1<10^9<2^{30}-1,二进制数对应集合的大小不会超过 29,因此在或运算下,每个数字至多可以增大 29 次。总体上看,二重循环的次数等于每个数字可以增大的次数之和,即 O(n\log U)。
- 空间复杂度:O(1)。返回值不计入。
方法二:更加通用的模板
该模板可以做到
- 求出所有子数组的按位或的结果,以及值等于该结果的子数组的个数。
- 求按位或结果等于任意给定数字的子数组的最短长度/最长长度。
末尾列出了一些题目,均可以用该模板秒杀。
思考:对于起始位置为 i 的子数组的按位或,至多有多少种不同的结果?
根据或运算的性质,我们可以从 x=\textit{nums}[i] 开始,不断往右扩展子数组,按位或的结果要么使 x 不变,要么让 x 的某些比特位的值由 0 变 1。最坏情况下从 x=0 出发,每次改变一个比特位,最终得到 2^{29}-1<10^9,因此至多有 30 种不同的结果。这意味着我们可以递推计算所有按位或的结果。
另一个结论是,相同的按位或对应的子数组右端点会形成一个连续的区间,从而保证下面去重逻辑的正确性(这一性质还可以用来统计按位或结果及其对应的子数组的个数)。
据此,我们可以倒着遍历 nums,在遍历的同时,用一个数组 ors 维护以 i 为左端点的子数组的按位或的结果,及其对应的子数组右端点的最小值。继续遍历到 nums}[i-1] 时,我们可以把 nums}[i-1] 和 ors 中的每个值按位或,合并值相同的结果。
这样在遍历时,ors 中值最大的元素对应的子数组右端点的最小值,就是要求的最短子数组的右端点。
注:下面代码用到了原地去重的技巧,如果你对此并不熟悉,可以先做做 26. 删除有序数组中的重复项 。
1 | class Solution: |
1 | class Solution { |
1 | class Solution { |
1 | func smallestSubarrays(nums []int) []int { |
复杂度分析
- 时间复杂度:O(n\log U),其中 n 为 nums 的长度,U=max(\textit{nums})。
- 空间复杂度:O(\log U)。返回值不计入。
可以用模板秒杀的题目
按位或:
按位与:
最大公因数(GCD):
乘法:
思考题
如果是异或要怎么做?
依然是倒序遍历,求后缀异或和,然后可以用 421. 数组中两个数的最大异或值 的字典树方法,需要额外存后缀异或和对应的下标,如果有多个相同的,存下标最小的。