题目链接:
https://nanti.jisuanke.com/t/A1613
题目大意:
n的数组,q次询问,L-R区间内所有子段的异或和之和。
题目思路:
考虑每一位算贡献。每个数位单独拿出来的话,对于一个区间的贡献就是
ans += 【1-R】的贡献 - 【1 - L-1】的贡献 - 左端点在【1,L-1】范围内的,右端点在【L -R 】范围内的。
先考虑前缀贡献怎么算,假如有了前i-1个数的贡献,那么第i的前缀贡献就是,包含第i个位置的所有子段的贡献,那么怎么计算这个东西呢,这个东西出来了,前缀贡献就出来了。
我们可以处理一个前缀异或和,如果到第i位的前缀异或和为0,那么前边【0,i-1】有多少个前缀异或和为1,这个 i 这一位就贡献了几次(一定要包含第0个位置,因为这是类似于相减的东西,如果不算0的话就会漏 1 - (i -1)的情况)
通过上边我们还发现需要处理一个前缀异或和上0有多少个,1有多少个的数组就是代码中的one 和 zero。
接下来就是怎么减去左端点在【1,L-1】范围内的,右端点在【L -R 】范围内的贡献了,这个又需要一个减法的思想。
如果右端点在【L,R】中,并且前缀异或和为1,为了使得某个区间异或和为1,就必须减去【1,L-2】中前缀异或和为0的。
同理,如果前缀异或和为0,为了使得某个区间异或和为1,就必须减去【1,L-2】中前缀异或和为1的。
(为啥是L-2呢,如果是L-1的话,用大区间一减这个小区间,那么就完全包含在【L-R】里头了)
数组含义:
perX【j】【i】:从第一个数字 到 第i个数字 第j位 的前缀异或和 。 (非0即1)
one【j】【i】: 从第一个数字 到 第i个数字 第j位 的前缀异或和中有多少个1。(【1 , i】前缀异或和1个数)
zero【j】【i】:从第一个数字 到 第i个数字 第j位 的前缀异或和中有多少个0。(【1 , i】前缀异或和0个数)
sum【j】【i】:从第一个数字 到 第i个数字 第j位 的所有子段异或和贡献。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN = 1e5+5;
const int mod = 1e9+7;
int preX[25][MAXN],a[MAXN];
ll sum[25][MAXN],zero[25][MAXN],one[25][MAXN];
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
int n,q;
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i=0;i<20;i++){
preX[i][0] = 0;one[i][0] = 0;zero[i][0] = 1;
for(int j=1;j<=n;j++){
if(a[j] & (1<<i))
preX[i][j] = preX[i][j-1] ^ 1;
else
preX[i][j] = preX[i][j-1];
one[i][j] = one[i][j-1];zero[i][j] = zero[i][j-1];
if(preX[i][j])
one[i][j] ++ ;
else zero[i][j] ++;
}
}
for(int i=0;i<20;i++){
sum[i][0] = 0;
for(int j=1;j<=n;j++){
sum[i][j] = sum[i][j-1];
if(preX[i][j])
sum[i][j] = (sum[i][j] + zero[i][j-1]*(1<<i))%mod;
else
sum[i][j] = (sum[i][j] + one[i][j-1] *(1<<i))%mod;
}
}
while(q--)
{
int L,R;
scanf("%d%d",&L,&R);
ll ans = 0;
for(int i=0;i<20;i++){
ans += sum[i][R] - sum[i][L-1];
ans %= mod;
if(L>=2){
ans -= (1<<i) * zero[i][L-2]*(one[i][R]-one[i][L-1])%mod;
ans -= (1<<i) * one[i][L-2] * (zero[i][R]-zero[i][L-1])%mod;
ans %= mod;
}
}
printf("%lld\n",(ans+mod)%mod);
}
}
}