一.基础
位运算经常考察异或的性质、状态压缩、与位运算有关的特殊数据结构、构造题。
位运算只能应用于整数,且一般为非负整数,不能应用于字符、浮点等类型。
左移操作相当于对原数进行乘以2的幂次方的操作,低位补0
右移操作相当于对原数进行除以2的幂次方的操作,高位补0
&与
|或
~按位取反
^按位异或
在讨论二进制数的位数时,通常采用的是从右向左的计数方法,其中最右边的位被称为第0位。
1.判断x的奇偶性:若x&1的结果是1,表示x二进制最后一位是1,则x是奇数;否则为偶数
2.获取x二进制中的第m位:右移m位,然后和1相与(取最后一位)。即x>>m&&1
3.将x的第i位改成1:1左移i位,和x相或,即x|(1<<i)
4.将x的第i位改成0:构造出只有第i位是0,其他都是1,与x相与。即x&(~(1<<i))
5.快速判断一个数字是否为2的幂次方:也就是x的二进制表示中只能有一个1。也就是x-1这位是0,往后全是1。我们将x和x-1相与,若为0,则是2的幂次方
6.获取二进制位中最低位(最右侧)的1:lowbit(x)。最低位的1及其右边都不动,左边全为0
二.例题
【例1】二进制中 1 的个数
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int a[100] = { 0 };
unsigned int x;//通常不希望处理负数的二进制表示,这里使用无符号整数
cin >> x;
int cnt = 0;
while (x) { //进制转换
a[cnt++] = x % 2;
x = x / 2;
}
reverse(a, a + cnt);
int ans = 0;//计数
for (int i = 0; i < cnt; i++) {
if (a[i] == 1)
ans++;
}
cout << ans;
}
当然也可以不进行进制转换
使用x&(x-1)清除最后一位的1
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
unsigned int x;
cin >> x;
int ans = 0;//计数
while (x) {
x = x & (x - 1); // 清除最低位的1
ans++; // 计数器加1
}
cout << ans;
}
也可以不断右移,判断最后一位是否为1
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
unsigned int x;
cin >> x;
int ans = 0;
while (x) {
if (x & 1)
ans++;
x = x >> 1;
}
cout << ans;
}
也可以使用刚刚学到的lowbit(x)
#include <iostream>
#include <algorithm>
using namespace std;
unsigned int lowbit(unsigned int x) { //固定代码
return x & (-x);
}
int main() {
unsigned int x,y;
cin >> x;
int ans = 0;
while (x) {
y = lowbit(x);
ans++;
x = x & (~y);//把x最后的1变为0
}
cout << ans;
}
【例2】区间或
评测系统
常规方法会超时,我们采用“拆位”
区间内所有二进制数的第0位若有1,则记为1,最终结果+20×1
区间内所有二进制数的第1位若有1,则记为1,最终结果+21×1
区间内所有二进制数的第2位若有1,则记为1,最终结果+22×1
区间内所有二进制数的第3位若没有1,则记为0,最终结果+23×0
判断第i位是否有1,可以通过观察这一位上所有二进制数的前缀和是否>0
注:2i可以用1<<i来表示
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 5;
int main() {
int n, q;
cin >> n >> q;
int a[N] = { 0 };
int prefix[35][N] = { 0 };//记录前缀和
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 0; i <= 30; i++) {//从右往左第i位的前缀和
for (int j = 1; j <= n; j++) {
prefix[i][j] = prefix[i][j - 1] + (a[j] >> i & 1);//先移动i位,再确定最后一位是1还是0
}
}
int l, r;
while (q--) {
int ans = 0;
cin >> l >> r;
for (int i = 0; i < 30; i++) {
ans += (1 << i) * (prefix[i][r] - prefix[i][l - 1] > 0 ? 1 : 0);//2^i*1或0
}
cout << ans << endl;
}
return 0;
}
【例3】异或森林
评测系统
【解析仅供参考】
当我们求x的因数时,一般从1遍历到根号x,若根号x左侧有一个因数,则右侧也一定有一个因数。如16的因数是1,2,4,8,16,4左右各有2个因数。这使得完全平方数的因数总是有奇数个,而这个4,不与任何其他因数配对。对于非完全平方数,它们的因数总是成对出现的,没有任何一个因数能够单独存在而不与其他因数配对。所以因数个数为偶数个,则一定不是完全平方数。
即根号x是整数,则x一定是完全平方数,x的因数个数一定为奇数个
在给定范围内,完全平方数的个数通常小于非完全平方数的个数。如1~100内,完全平方数只有1、4、9、16、25、36、49、64、81、100
用总数减去完全平方数的个数就是偶数个因数的个数
总数应该为n*(n+1)/2
如5个数时,可选的子数组个数为5+4+3+2+1
a[i]不超过n,所有a[i]异或最终位数也不会改变,也就是不超过2n
2n是20000,根号下不到200
借助前缀异或和数组prexor,枚举所有的平方数,若某区间的异或和正好等于某个平方数,说明这个区间得到的结果是一个完全平方数
若满足sq==prexor[j]^prexor[i],说明区间[j+1,i]上的异或和是一个完全平方数
但这样我们需要遍历所有的i和j,时间复杂度太高
根据异或的性质,a^b=c可写为a^c=b,我们有prexor[j]==sq^prexor[i]
prexor[i]最大是1e4,sq最大是200,我们可以枚举所有的i和sq,统计所有小于i的j的个数(用cnt数组记录)
在构建prexor数组时,我们就可以记录哪些prexor[j]是合法的(存在的),若不存在就-0,存在就减去出现的次数(存在的子数组个数)
for (int i = 1; i <= n; i++) {
prexor[i] = prexor[i - 1] ^ a[i];
cnt[prexor[i]]++;
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= 200; j++) {
int sq = j * j;
ans -= cnt[prexor[i] ^ sq];//cnt[prexor[j]]
}
}
但忽略了区间的左右端点关系,我们要保证j<i
可采用“滚动更新”的方式,如i=1时,表示1之后的j都不考虑(都为0)
for (int i = 1; i <= n; i++) {
prexor[i] = prexor[i - 1] ^ a[i];
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= 200; j++) {
int sq = j * j;
ans -= cnt[prexor[i] ^ sq];//cnt[prexor[j]]
}
cnt[prexor[i]]++;
}
另外prexor[0]也是合法的(左端点可以从0起),题目要求0的因数个数视为奇数,也是我们需要减掉的,所以cnt[0]应该为1
cnt选择异或和数组prexor作为下标,prexor最大不超过2n,所以将数组大小N调整为1e5+5
得到最终代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5+5;
int main() {
int n;
cin >> n;
int a[N], prexor[N] = { 0 };
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int cnt[N] = { 0 };
cnt[0] = 1;
for (int i = 1; i <= n; i++) {
prexor[i] = prexor[i - 1] ^ a[i];
}
int ans = n*(n+1)/2;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= 200; j++) {
int sq = j * j;
ans -= cnt[prexor[i] ^ sq];//cnt[prexor[k]]
}
cnt[prexor[i]]++;
}
cout <<ans;
}
三.练习
1.最小的或运算
评测系统
分析:我们逐个分析a和b的二进制位。当a和b的二进制为都为0时,对应x的二进制位为0或1均可,为了最小我们取0。当a和b对应的二进制位为0和1时(或反之),x对应的二进制位为1才能保证相等。当a和b的二进制位都为1时,对应x的二进制位为0或1均可,为了最小我们取0。显然这是异或运算的结果。
注意long long和括号
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
long long a,b;
cin>>a>>b;
cout<<(a^b);
}
2.简单的异或难题
分析:两个相同的数异或是0,0和任何数异或都是这个数本身
所以区间内出现次数为偶数的数,不会影响最终结果
采用前缀异或和
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 5;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n, m;
int a[N], prexor[N];
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
prexor[i] = prexor[i - 1] ^ a[i];
}
while (m--) {
int l, r;
cin >> l >> r;
cout << (prexor[l - 1] ^ prexor[r]) << endl;
}
return 0;
}
3.出列
分析:
由表得,第k次出列时,会将二进制后k位为0的留下
又因为序号是连续从1开始排列的,最后留下的同学初始二进制序号一定为1后面k个0(也就是2k)
我们希望找到一个最大的2k使其不超过同学个数n
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
int k = 0;
while ((1 << k) <= n) {
k++;
}
cout << (1 << (k - 1));
}
4.小蓝学位运算
分析:
设a的前缀异或和数组为prexor,则原问题转化为求prexor[l-1]^prexor[r]
注意n>8192的情况,这时候一定会出现两个完全一样的数,他们的异或结果是0,使得最后连乘的结果也为0,这种情况要特殊考虑
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6+5;//数组大小
const int N2 = 1e9 + 7;//模除
int main() {
int n;
cin >> n;
int a[N], prexor[N];
if (n > 8192) { //特殊考虑
cout << "0";
return 0;
}
for (int i = 1; i <= n; i++) {
cin >> a[i];
prexor[i] = prexor[i - 1] ^ a[i];
}
long long ans = 1;//注意不要写成0
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++) {
long long xor_var = prexor[i - 1] ^ prexor[j];
ans = (ans * xor_var) % N2;
}
}
cout << ans;
return 0;
}
5.位移
评测系统
分析:
如
010010
100100
左移右侧补0,右移左侧补0,我们删掉左右的0,中间的字符串若相等,就可以变换a
但不相等时也可以
如
a=1001
b=100000
将a右移1位,再左移3位就可得到b
从中间字符串的角度看,a变为1001,b变为1
得出,只要b是a的子串就可以
特殊的,b为0时恒成立
#include <iostream>
#include <algorithm>
using namespace std;
string change(int x) {
int left = 0, right = 31;
while ((left<right)&&(((x>>right)&1)==0)) { //处理左侧0
right--;
}
while ((left < right) && (((x >> left) & 1) == 0)) { //处理右侧0
left++;
}
string str;
for (int i = right; i >= left; i--) {
str += ((x >> i) & 1) + '0';//+ '0'将数字转为字符串类型
}
return str;
}
int main() {
ios::sync_with_stdio(false); //用cin/cout必须关闭流同步
cin.tie(NULL);
cout.tie(NULL);
int t;
cin >> t;
while (t--) {
int a, b;
cin >> a >> b;
if (b == 0) {
cout << "Yes" << '\n'; //用endl会超时
continue;
}
string aa = change(a);
string bb = change(b);
bool flag = 0;
int cha = aa.size() - bb.size();
for (int i = 0; i <= cha; i++) {
flag = 1;
for (int j = 0; j < bb.size(); j++) {
if (aa[i+j] != bb[j]) {
flag = 0;
break;
}
}
if (flag == 1) {
cout << "Yes" << '\n';
break;
}
}
if(flag==0)
cout << "No" << '\n';
}
}
6.笨笨的机器人
评测系统
分析:
用0表示往左移动(用减表示),用1表示往右移动(用加表示)
在有n条指令的情况下,用n位的0或1表示每一个数字的状态
最终可能停留的位置由这一串n位的二进制数控制
这串二进制数可能的取值为0~n个1
也就是总数(分母)有2n种可能
这种情况也涵盖了可能跳回原点(也就是全0)的情况
而分子就是最终有多少串二进制数,使得最终停留位置是7的整数倍,我们用cnt计数
我们遍历所有可能的二进制位(从0到n个1),这串二进制数最右侧的数表示对a[0]的操作
注意输出结果要求四舍五入,而printf并不是严格的四舍五入,需要使用round函数四舍五入
round函数接受一个浮点数作为参数,四舍五入返回最接近的整数。
如0.631938041会变为1.00000
#include <iostream>
#include <math.h> //round头文件
using namespace std;
const int N = 1e3 + 5;
int main() {
int n;
int a[N];
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int cnt=0;//分子
for (int i = 0; i <= (1 << n) - 1; i++) { //从 0 到 n个1 遍历
int end = 0;//最终停留位置
for (int j = 0; j < n; j++) {
if (((i >> j) & 1) == 1) {
end += a[j]; //是1往右移动
}
else {
end -= a[j]; //是0往左移动
}
}
if (end % 7 == 0) {
cnt++;
}
}
int fenmu = 1 << n;
//输入:9
//5 7 8 9 8 74 5 21 6
double ans = (double)cnt / fenmu*10000;//1562.5000000000000
ans = round(ans);//1563.0000000000000
printf("%.4lf", ans/10000);
}
7.选题
评测系统
分析:有n个数字,n的规模很小,每个数字都有选和不选两种状态。我们用0表示不选,1表示选。在判断是否有3种不同的值时,使用map判断更简洁
#include <iostream>
#include <algorithm>
#include <map>
using namespace std;
const int N = 50;
int main() {
int n, l, r, x;
cin >> n >> l >> r >> x;
int a[N];
for (int i = 0; i <= n; i++) {
cin >> a[i];
}
int cnt=0;//计数
map<int, int> q;//记录是否有3种不同的值
for (int i = 0; i <= (1 << n)-1; i++) { //从全0到n个1
int maxn = 0, minn = 1e6+5, sum = 0;//注意min要给一个很大的值
q.clear();//清空map
for (int j = 0; j < n; j++) {
if ((i >> j & 1) == 1) {
q[a[j]] = 1;//给键赋值
sum += a[j];
maxn = max(maxn, a[j]);
minn = min(minn, a[j]);
}
}
if (sum >= l && sum <= r && maxn - minn >= x && q.size() >= 3) {
cnt++;
}
}
cout << cnt;
}
8.迷失之数
评测系统
分析:
A数组下标从1开始
输出第一个数一定是A数组中最大的那个数
用这个最大的数和A数组中其余的所有数相或,再减去这个最大的数,从前往后,找到那个最大的数,这就是要输出的第二个数
以此类推,每次都用上一轮的前缀或和结果与A数组中其余的所有数相或,再减去上一轮的前缀或和结果,找到那个最大的数并输出
#include <iostream>
using namespace std;
const int N = 1e6 + 5;
int main() {
int n;
cin >> n;
int A[N];
int maxA = -1;
int maxAi = 0;
for (int i = 1; i <= n; i++) {
cin >> A[i];
if (A[i] > maxA) {
maxA = A[i];
maxAi = i;
}
}
int temp = A[maxAi];//记录前缀或和的值
cout << maxA << " ";
int determine[N];//判断当前数是否已被输出
determine[maxAi] = 1;//记录已输出
for (int j = 1; j < n; j++) {
int maxn = -1;
int maxi = 0;
for (int i = 1; i <= n; i++) {
if (determine[i]==1)
continue;
if (((temp | A[i]) - temp) > maxn) {
maxn = (temp | A[i]) - temp;
maxi = i;
}
}
if (maxn == 0) { //加速:如果后面剩的都是同一个数(或是没有意义的数),不用重复考虑
break;
}
temp = temp | A[maxi];
cout << A[maxi] << " ";
determine[maxi] = 1;
}
for (int i = 1; i <= n; i++) { //按序输出剩余的数
if (determine[i] == 0) {
cout << A[i] << " ";
}
}
}