小R的子数组权值
问题描述
小R有一个长度为 n
的数组 a
,她定义每个子区间 [l, r]
的权值为 a[l] | a[l+1] | ... | a[r]
,即该区间内所有元素的按位或运算结果。小R非常好奇,在这 n × (n + 1) / 2
个子区间中,究竟有多少种不同的权值。
她希望你能帮她计算一下,所有子区间中的不同权值总共有多少种。
测试样例
样例1:
输入:
a = [1, 2, 4]
输出:6
样例2:
输入:
a = [5, 3, 8, 1]
输出:8
样例3:
输入:
a = [1, 1]
输出:1
样例4:
输入:
a = [7, 8, 9, 10, 11]
输出:6
题目解析
这道题的意思就是问:在一个数组中,计算所有子区间(连续的元素)通过“按位或(|)”操作得到的结果中,不同的结果有多少种。
解决这个问题的核心在于如何高效地计算所有子区间的按位或结果,并统计其中不同的值的总数,而且最好不用用时间复杂度特别大的方法。
对于一个长度为 n 的数组,子区间的总数为 n×(n+1)/2,如果直接枚举每个子区间并计算其按位或值,这种暴力解法会导致时间复杂度达到O(n^3),难以适应较大的数据规模。因此,我们需要找到一种优化的方法,既能够准确地计算结果,又能降低复杂度。
按位或操作具有重要的性质:它是一种单调递增的操作。具体来说,如果我们有一个区间 [l, r] 的按位或值,将区间的右边界扩展为 r+1 时,新区间 [l,r+1] 的按位或值只可能大于或等于原来的值。这种性质为我们优化计算提供了一个重要思路:可以通过滑动窗口的方法,从左到右遍历数组,动态维护当前子区间的按位或值,而不需要重新计算之前的所有值。
按位或操作单调递增的性质:假设有一个子区间[l,r],其按位或值为 res=a[l]∣a[l+1]∣...∣a[r]。如果将区间的右边界扩展为 r+1,即新的区间为[l,r+1],那么新的按位或值必然满足:
res′=res ∣ a[r+1]
由于按位或操作不会清除已有的二进制位,因此结果只可能增加或保持不变。这一特性为我们优化算法提供了重要依据:可以通过动态维护当前区间的按位或值,避免重复计算。
具体实现过程中,我们引入两个集合来帮助计算:一个是 current
集合,用于存储当前以某个位置结尾的所有子区间的按位或结果;另一个是 all_or_values
集合,用于存储全局的按位或结果,也就是从数组中所有子区间中可能出现的不同按位或值。在遍历数组时,每遇到一个新的元素,就可以将它与 current
集合中的每个值进行按位或操作,并将结果加入到 current
中,同时将当前元素本身也作为一个新的起点加入 current
集合中。这样,current
集合始终包含的是以当前元素结尾的所有可能按位或值,而全局集合 all_or_values
则通过 update
方法将每次计算得到的 current
集合合并进来,从而记录所有可能的值。
举例来说,对于数组 a=[1,2,4],初始时 current
和 all_or_values
都为空。在处理第一个元素 1 时,current
集合会更新为 {1},同时将其加入 all_or_values
,结果为 {1}。接下来处理第二个元素2 时,将其与当前 current
集合的每个值进行按位或操作,得到的结果为 {2,3},加上自身值 {2},更新后的 current
集合为 {2,3},并将其并入全局集合,最终 all_or_values
变为 {1,2,3}。类似地,处理第三个元素 444 时,current
会扩展为 {4,6,7},加入到 all_or_values
中,最终结果为 {1,2,3,4,6,7}。最终集合的大小为 666,即为所有子区间中不同按位或值的总数。
通过这样的处理方法,时间复杂度得到了显著优化。从表面上看,每个元素最多会与当前 current
集合中的所有值进行按位或操作,而 current
集合的大小在最坏情况下为 O(n),因此单次更新的复杂度为 O(n),整个算法的总时间复杂度为 O(n^2)。
下面是python版本代码实现
def solution(a):
all_or_values = set()
current = set()
for num in a:
current = {num | x for x in current} | {num}
all_or_values.update(current)
return len(all_or_values)
if __name__ == '__main__':
print(solution([1, 2, 4]) == 6)
print(solution([5, 3, 8, 1]) == 8)
print(solution([1, 1]) == 1)
print(solution([7, 8, 9, 10, 11]) == 6)
java版本的代码实现如下:无法直接使用类似推导式的语法,需要通过显式的 for
循环遍历当前集合,并将结果逐一加入新的集合中。
这使得 Java 实现的代码量非常复杂——但逻辑是相同的。
import java.util.HashSet;
import java.util.Set;
public class Main {
public static int solution(int[] a) {
Set<Integer> allOrValues = new HashSet<>();
Set<Integer> current = new HashSet<>(); // 以当前元素结尾的所有按位或值
for (int num : a) {
Set<Integer> next = new HashSet<>();
next.add(num);
for (int x : current) {
next.add(x | num);
}
current = next;
allOrValues.addAll(current);
}
return allOrValues.size();
}
public static void main(String[] args) {
System.out.println(solution(new int[]{1, 2, 4}) == 6);
System.out.println(solution(new int[]{5, 3, 8, 1}) == 8);
System.out.println(solution(new int[]{1, 1}) == 1);
System.out.println(solution(new int[]{7, 8, 9, 10, 11}) == 6);
}
}