题目
众所周知,位运算有与,或,异或三种。
- 与:相同位的两个数字都为 1,则为 1;若有一个不为 1,则为 0。
- 或:相同位只要一个为 1 即为 1。
- 异或:相同位不同则为 1,相同则为 0 。
小 Z 觉得她们非常的有趣,为了体现自己的强大,小 Z 一口气学会了三种运算,并出了一道题准备考考你。给出 l,r 以及运算 ⨁ ,询问 [l,r] 的每一个数通过 ⨁ 运算后的值。其中运算会给出,op=1 运算为与,op=2 运算为或,op=3运算为异或
分析
题目很清晰,数据很不友好,这是逼着我们找 O(1) 的写法
三种位运算题目已经解释过了,这里给不太懂位运算的小伙伴举个栗子
eg:
a = 200(10) = 1100 1000 (2)
b = 102(10) = 0110 0110 (2)
- a & b = 0100 0000(2) = 64(10)
- a | b = 1110 1110 (2) = 238(10)
- a ^ b = 1010 1110(2) = 174(10)
- 单目运算符 << 和 >> 简单的应用
x = 10(10) = 1010 (2)- num = x << 3 = 1010000 (2) = 40(10)
- num = x >> 3 = 1(2) = 1(10)
动手算一下就知道了
题目可以分成三个子问题,我们来一个一个的解决
与运算
与运算的特点是,相同位的两个数字都为 1,则为 1。根据这个特点,我们可以想到,最后的结果肯定是和 0 出现最多的那个数有关的,现在问题就转化成,在(l, r)中找到 0 最多的那个数。
对于区间(l,r)的连续与运算,我们先来看个实列;
eg:
r = 164(10) = 1010 0100 (2)
l = 132(10) = 1000 0100 (2)
我们引入一个中间变量 x = 160(10) = 1010 0000 (2),而 x & (x-1) = 1000 0000,可以发现 1000 0000 和区间(l,r)任意数的与运算结果都是 1000 0000,也就说区间(l,r)的连续与运算的结果为 1000 0000。
为什么呢?
x = 160(10) = 1010 0000 (2)
x -1 = 159(10) = 1001 1111 (2)
(x - 1) & x = 1000 0000 (2)
(x - 1) & x 可以把标记位往右的全部数位清零
这就很明显了,我们可以通过寻找(l,r)中间的 x ,来实现清零的效果
我们再来观察一下 x 与 l,r的关系
r = 164(10) = 1010 0100 (2)
l = 132(10) = 1000 0100 (2)
x = 160(10) = 1010 0000 (2)
我们可以发现,x 与 r 在标记位左侧都是相同的,x 的后半部分肯定都是 0 ,而标记位是 r 和 l 最后出现相同位不都为 1 或 0 的位置
我们可以通过扫描二进制下的 r 和 l 找到标记位,通过 r 来确定 x
代码
void solve_1(ll l, ll r){ // 与运算 &
ll num = r; // num 保留一下 r
int x = 0; // 纪录最后出现相同位不都为 1 或 0 的位置
// 从右往左扫描,出现相同位不都为 1 或 0 的位置就更新
// 循环结束以后,x 就是最后出现相同位不都为 1 或 0 的位置
for(int i = 1; r != 0; i++){
if((l & 1) ^ (r & 1)){
x = i;
}
l >>= 1;
r >>= 1;
}
// num 右移 x 位,再左移 x 位,就可以把 num 的后 x 位清零
// 模拟一下就很清楚了
num >>= x;
num <<= x;
cout << num << endl;
}
或运算
或运算的特点是,相同位只要一个为 1 即为 1。我们类比与运算,我们找到出现 1 最多的那个数就OK了。
还用刚刚的例子:
r = 164(10) = 1010 0100 (2)
l = 132(10) = 1000 0100 (2)
我们再次引入中间变量 x = 160(10) = 1010 0000 (2),而 x | (x-1) = 1011 1111,可以发现 1011 1111 和区间(l,r)任意数的与运算结果都是 1011 1111,也就说区间(l,r)的连续与运算的结果为 1011 1111。
x 的确定和刚刚一模一样,就不再说了,细节看代码
代码
void solve_2(ll l, ll r){ // 与运算 |
ll num = r; // 确定标记处
int x = 0;
for(int i = 1; r != 0; i++){
if((l & 1) ^ (r & 1)){
x = i;
}
l >>= 1;
r >>= 1;
}
// 或运算要把后 x 位都变成 0 ,与运算要把后 x 位都变成 1
// 通过把 1 左移 x 位得到 2^x
// 这里注意 1 要强制转化为 ll 型
// 减法的优先级比左移高,我们要先左移后减 1 ,记得加括号 (这个错误找了好久,/(ㄒoㄒ)/~~)
if(x != 0) num |= ((ll)1 << x) - 1; // 这里的 ((ll)1 << x) - 1 就是分析里边的 x | (x - 1)
cout << num << endl;
}
异或运算
异或运算的特点是相同位不同则为 1,相同则为 0 。这里就很麻烦了,区间(l,r)中所有的数都对答案有贡献,我们要计算每一位上 0 和 1出现的次数。显然,直接统计区间(l,r)中所有数字的每一位上 0 和 1出现的次数是困难的。
异或运算两个很重要的性质:
- 两个相同的数异或的结果为 0,即 x ^ x = 0。
- 任何数 x 和 0 异或的结果为 x,即 x ^ 0 = x。
可以运用这两个性质对问题进行转化,我们定义 f(l,r) 为区间(l,r)的连续异或,则有 f(l,r) = f( 0 , l - 1 ) ^ f( 0 , r ) 。现在我们需要找到一个o(1)的方法计算出 f(0, x) 就OK。
这里可以选择打表去找规律
eg:
根据表组的数据我们可以得出:
- 当 x % 4 = 0 时 f(0, x) = x
- 当 x % 4 = 1 时 f(0, x) = 1
- 当 x % 4 = 2 时 f(0, x) = x + 1
- 当 x % 4 = 3 时 f(0, x) = 0
具体的证明过程,这里附上大佬的帖子,传送门
由 f(l,r) = f( 0 , l - 1 ) ^ f( 0 , r ) 和我们上边得出的结论,这个子问题也就解决了
代码
void solve_3(ll l, ll r){ // 异或 ^
l--;
ll res_l; // 计算(0,l-1)的连续异或
if(l % 4 == 0) res_l = l; // 这个是 l 不是 1
if(l % 4 == 1) res_l = 1;
if(l % 4 == 2) res_l = l + 1;
if(l % 4 == 3) res_l = 0;
ll res_r; // 计算(0,r)的连续异或
if(r % 4 == 0) res_r = r;
if(r % 4 == 1) res_r = 1;
if(r % 4 == 2) res_r = r + 1;
if(r % 4 == 3) res_r = 0;
ll res = res_l ^ res_r;
cout << res << endl;
}
主函数
int main() {
ios::sync_with_stdio(false);
int ncase;
cin >> ncase;
ll op, l, r;
while(ncase--){
cin >> l >> r >> op;
if(op == 1) solve_1(l, r);
if(op == 2) solve_2(l, r);
if(op == 3) solve_3(l, r);
}
return 0;
}
本题,完。