小 K 的区间与值和 | 豆包MarsCode AI刷题

105 阅读2分钟

小K的区间与值和

问题描述

小K有一个数组,她定义数组的权值为数组中任选两个数的按位与的值之和。具体来说,对于数组中的每个连续子数组,我们可以计算所有可能的两个元素的按位与值之和,并将这些值相加。小K想知道该数组中所有可能的连续子数组的权值和是多少,最后结果对109+710^9+7取模。

测试样例

样例1: 输入:n=4,a=[2,3,1,2] 输出:16

样例2: 输入:n=3,a=[5,6,7] 输出:25

样例3: 输入:n=2,a=[1,10] 输出:0

样例4: 输入:n=5,a=[1,2,4,8,16] 输出:0

题目分析

一、暴力做法 O(n4)O(n^4)

我们可以枚举所有存在的子数组,然后对于每一个子数组,都去选择两个数,相与并将答案计入。

const long long mod = 1e9+7;

int solution(int n, vector<int> a) {
    vector<long long> res(n+3,0);
    long long ans = 0;
    for(int i=0;i<n;i++) {
        for(int j=0;j<i;j++) {
            for(int r=j;r<=i;r++) {
                for(int l=j;l<r;l++) {
                    ans = (ans + (a[l] & a[r])) % mod;
                }
            }
        }
    }
    return ans;
}

这虽然也能过,但我们还是要去追求更快更好的解决方法

二、枚举每一对数字相与的贡献 O(n2)O(n^2)

对于一对数字 a[i]a[j](其中 i < j),我们去计算共有多少个子数组包含这对数字,可以发现包含这两个数的子数组的数量等于(i+1) * (n-j)。

const long long mod = 1e9+7;

int solution(int n, vector<int> a) {
    vector<long long> res(n+3,0);
    long long ans = 0;
    for(int i=0;i<n;i++) {
        for(int j=0;j<i;j++) {
            ans = (ans + (j+1) * (n-i) * (a[i] & a[j])) % mod;
        }
    }
    return ans;
}

但还是不够快,我们还能更进一步!

三、计算每一个二进制位的贡献 O(nlogV)O(nlogV)

题目中有一个很重要的信息我们并没有使用到:两个元素的按位与值之和

对于位运算题目,我们完全可以独立考虑每一个二进制位对答案的影响,不同位之间并无关系。这样我们就将问题转化为了01序列中所有子序列里的1的贡献。

我们从左往右扫描,计算第i个1作为区间右端点时的贡献,也就是把前面所有1出现的位置再+1的和。

const ll mod = 1e9+7;

int solution(int n, vector<int> a) {
    vector<ll> res(n+3,0);
    ll ans = 0;
    for(int base=0;base<=30;base++) {
        ll t = 1ll << base;
        ll cnt = 0;
        for(int i=1;i<=n;i++) {
            res[i] = a[i-1] >> base & 1;
            if(res[i] == 1) {
                ans = (ans + cnt * (n-i+1) % mod * t) % mod;
                cnt += i;
            }
        }
    }
    return ans;
}

得到了一个O(nlogV)O(nlogV)的算法,这里的V是指数组a值域上限。