下面用一道例题来引出我们今天要求解的问题:
不难看出,这道题的题意十分简单,就是给你一个长度为1e6的数组a,然后给你q个询问,对于每个询问,给你一个l,r,让你打印出数组a在区间[l,r]的和、按位异或、按位或、按位与的值。
作为一个小白,当时看到这道题第一眼是想到这种关于二进制运算的区间问题肯定一些性质,只不过但是未能发现。所以用线段树来解决这个问题,不出意外地TLE了,因为线段树常数太大了。
然后开始躺平,睡了一觉之后,又去搜了一下博客,猛然间发现一个新的名词:二进制拆位(OS:当时不知道什么是二进制拆位)。
其主要内容说的是,可以将每个元素按照其二进制表示法拆分之后,然后看对应每一位对于最后整体答案的贡献即可。
不太明白?举个例子:
就拿题目中的数组[1,2,3,4,5]来说,首先第一个区间和问题我们就不谈了,我把它单拎出来用前缀和实现的。我们主要来探讨后面三个问题的解决方案。
第一步:我们首先将这5个元素分解成二进制的形式,为了方便观察,我补全了4位,如下图:
第一个问题:求区间异或
区间异或:在编程中用符号^表示,其含义就是在二进制中的不进位加法,比如:1^1=0,1^0=1。(如果不理解的话,可以先去其他地方学习好了再过来)
回到问题,既然是不进位加法,那我们是不是可以发现一个性质:如果在一个区间中,其1的个数为奇数个,那么这个区间异或值必为1,如果1的个数是偶数个,那么区间异或值必为0,不理解的话,咱们直接上图:
我们看到,在这些数的二进制表示中,从右第0列(下边以0开始)有3个1,所以单看这一列,其区间异或值必为1,同样,从右第2列区间异或值必为0。如果我们想计算数组a在区间[1,5]的区间异或值,我们是不是可以按照上面的方法,算出每一位的最终的区间异或值,然后把二进制数转换成十进制不就是答案了嘛。上图中,区间[1,5]的异或值的二进制为0001,转换成十进制自然是1了。
第二个问题:发现了性质,怎么求任意一个区间的区间异或值呢?
没错,就是前缀和,我们可以预先处理出每一位二进制中每个元素的前缀和。不太明白?我们用b[i][j]来表示前j个数第i位的前缀和(二进制位还是从右向左,下标为0),看下图:
然后,假如我们要在数组[1,2,3,4,5]中,求区间[2,3]的二进制前缀和就可以用前缀和公式来解决了:b[3][i] - b[2 - 1][i],这里的i还是表示你想求哪一位。
用代码实现就是这样:
for (int i = 0; i < 32; i ++)
{
for (int j = 1; j <= n; j ++)
{
int k = a[j];
if (k >> i & 1) b[i][j] ++;
}
}
到这里,我们基本上讲完了,我直接上关于按位或| 按位与&在区间问题中的结论,读者可以自行推导验证(其实是打字太麻烦了,刚好到饭点)
对于按位或|:不管有多少个0,只要有一个1,那么结果对应二进制位肯定是1。
对于按位与&:1出现的次数一定要等于区间的长度,那么结果对应的二进制位才是1。
结合结论,我给出代码,在求出每一位的值之后,就可以用秦九韶算法从高到低位将其转换成十进制了,最后的结果也就是答案。
while (q --)
{
int l, r;
cin >> l >> r;
cout << a[r] - a[l - 1] << ' ';//十进制前缀和
int res1 = 0;//异或
int res2 = 0;//按位或
int res3 = 0;//按位与
for (int i = 31; i >= 0; i --)
{
int cnt = 0;
cnt += b[i][r] - b[i][l - 1];
res1 = res1 * 2 + (cnt % 2);//有奇数个1
res2 = res2 * 2 + (cnt > 0);//至少有1个1
res3 = res3 * 2 + (cnt == (r - l + 1));//1的个数等于区间长度
}
cout << res1 << ' ';
cout << res2 << ' ';
cout << res3 << endl;
}
下面给出这个题的完整参考代码,写得比较朴素:
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 1e6 + 10;
long long a[N];
int b[32][N];
int n, q;
int main()
{
cin.tie(0)->sync_with_stdio(0);
cin >> n >> q;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 0; i < 32; i ++)
{
for (int j = 1; j <= n; j ++)
{
int k = a[j];
if (k >> i & 1) b[i][j] ++;
}
}
for (int i = 1; i <= n; i ++) a[i] += a[i - 1];
for (int i = 0; i < 32; i ++)
{
for (int j = 1; j <= n; j ++)
{
b[i][j] += b[i][j - 1];
}
}
while (q --)
{
int l, r;
cin >> l >> r;
cout << a[r] - a[l - 1] << ' ';//十进制前缀和
int res1 = 0;//异或
int res2 = 0;//按位或
int res3 = 0;//按位与
for (int i = 31; i >= 0; i --)
{
int cnt = 0;
cnt += b[i][r] - b[i][l - 1];
res1 = res1 * 2 + (cnt % 2);
res2 = res2 * 2 + (cnt > 0);
res3 = res3 * 2 + (cnt == (r - l + 1));
}
cout << res1 << ' ';
cout << res2 << ' ';
cout << res3 << endl;
}
return 0;
}
最后,我也是这类题的小白,今天第一次试着解决这类问题,顺便发一下理解,如果读者读着吃力,那一定是我的问题,不明白的可以评论区call我,吃饭~