计蒜客 T3156 (连续自然数的位运算(^ 、|、 &))

题目

众所周知,位运算有与,或,异或三种。

  • 与:相同位的两个数字都为 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;
}
 

本题,完。

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值