题目解析 小R的子数组权值 | 豆包MarsCode AI 刷题

4 阅读5分钟

小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],初始时 currentall_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);
    }
}