刷题的时候经常遇到异或相关的题目,虽然知道是什么意思但是做题的时候总感觉力不从心,总感觉和所学的联系不上,因此总结一些我做过的或者是经典的异或问题
什么是异或?
异或简单来说就是相同的得0,相异得1,异或有一些性质例如满足交换律,结合律,自反性等等,这些性质实际上在题目中考察不多,题目更多的是考察你的思维
01的异或固然容易,但是当数据扩大到数万级别的时候就涉及到了数的拆分,把十进制转换为二进制是解决异或问题常见的一种手段,面对异或相关的问题都需要有进行位运算的思想
异或例题
1.异或和
题目意思就是给出一个区间,求区间内满足i<j的数的两两异或的和
暴力法就是枚举区间内的两个数,两两异或算出和来,当数据量较大的时候显然是不可行的
这里就需要引入一个新概念:位运算,将每一个数都转成二进制的表达形式
根据异或的性质,我们可知异或的结果只和1的个数有关,两个数之间异或,当该位上的数字异或为1,才是有贡献的,异或为0的位对我们的结果就不影响了,因此我们只需要关注1的个数即可
思路明确之后,我们的问题就改变为求区间内的数异或后各位置上的1的数量,又因为我们计算的是两两异或,因此我们要统计(0,1)数对的数量(01数对的数量也就是对答案有贡献的数据的数量)
#include<cstdio>
#include<algorithm>
using namespace std;
int ans[20]={0};//log2(100000)约为17,这里的数组大小应该开到至少20
long long int res=0;
int main()
{int n,a,i,len,mmax=-1;//mmax表示所有数中最大的二进制位数scanf("%d",&n);for(i=1;i<=n;i++){scanf("%d",&a);len=0;while(a){len++;if(a&1)ans[len]++;//记录当前位为1的个数a=a>>1;}mmax=max(mmax,len);}for(i=1;i<=mmax;i++){res+=(long long int)(1<<(i-1))*(long long int)ans[i]*(long long int)(n-ans[i]);//强转为long long}printf("%lld",res);return 0;
}
//借用一下别人的代码,看不懂的兄弟可以去看看原文
原文链接:https://blog.csdn.net/upc122/article/details/106535437
ans统计位上1的数量后,根据排列组合公式可以得到(0,1)数对的组合数,再乘以该位的共享2^n,即可得出答案
ps:个人感觉这题是区间异或和的升级版,区间异或和比这里的更简单,统计某一个区间内的数的异或和a1^a2^a3...^an,只需要位运算加贡献思想即可
2.异或和之和
题目意思是求出l<=r的子段的异或和,再将所有的异或和进行求和
我们都懂的求区间的异或和,但是题目的子段有很多,我们枚举各子段,时间复杂度又将变成O(n2),是不满足数据要求的
很多兄弟在这里就卡住了,解决这个问题需要用到异或的一个性质:自反性 ---a^a=0,一个数和自身进行异或的结果为0,Sa-1^Sb=a~b的异或和(b>a)
那么我们就可以用前缀异或和去表示区间异或和了,我们求出前缀异或和之后就可以只用两个数就可以表示出区间异或和
巧妙的点来了,我们只需要将前缀异或和看作一个数据,那么我们就可以将本题改为求区间内任意两个数的异或的和,这样题目就变得和上一题一样了,时间复杂度也从O2降到O1
#include <bits/stdc++.h>
using namespace std;
long long n,a[100010],q[100010],w[100010][2],ans=0;
int main()
{scanf("%lld",&n);for(int i=1;i<=n;i++)scanf("%lld",&a[i]);for(int i=1;i<=n;i++)q[i]=q[i-1]^a[i];for(int i=0;i<=n;i++)for(int j=20;j>=0;j--)w[j][(q[i]>>j)&1]++;for(int i=0;i<=20;i++)ans+=w[i][0]*w[i][1]*(1<<i);printf("%lld",ans);return 0;
}
求出前缀和之后,就可以统计各位上1的个数,排列(0,1)数对,问题即可解决
关键在于将区间内的数视作一个整体
3.选数异或
题目意思是给出一个区间,问区间内是否存在两个数使得异或为x
首先想暴力法肯定是枚举区间上的两个数,一一检查他们的异或结果,时间复杂度为O2显然是不可行的,我们要想的是如何进行优化
和上面两题不同,这里求的不是异或和,只是单纯的验证两个数的异或结果,所以上面的位运算+贡献思想是用不上的,既然无法优化成O1,我们就想如何剪枝优化,避免不必要的计算
不难想到,检查一个区间的时候只要区间内有一个异或满足条件,那么这个区间必然满足条件,因此我们可以在枚举区间之前先判断子区间是否存在异或,有则直接返回yes
#include<iostream>
using namespace std;
int main()
{int n,m,x,l,r;scanf("%d%d%d",&n,&m,&x);int e[100010];for(int i=1;i<=n;i++){scanf("%d",&e[i]);}int res[100010][2];bool exist=false;int k=0;while(m--){exist=false;scanf("%d%d",&l,&r);for(int j=1;j<=k;j++){if(l<=res[j][0]&&r>=res[j][1]){exist=true;printf("yes\n");break;}}if(exist==true)continue;for(int i=l;i<r;i++){for(int j=i+1;j<=r;j++){if((e[i]^e[j])==x){k++;res[k][0]=i;res[k][1]=j;exist=true;break;}}if(exist)break;}if(!exist)printf("no\n");else printf("yes\n");}return 0;
}
用res集去存储异或数对,每一次查询先判断区间内是否有异或数对,没有才进行枚举,这样就完成了剪枝优化
4.异或数列
题目意思是给出一组数,每次一人选择一个数,可以将自己的数或者别人的数进行异或求和,异或后数大的获胜(补充:a,b开始时都为0)
第一眼看还以为是nim,但是这里会出现平局的情况所以还是有一点不同的
涉及到求异或和,那么就需要用到位运算和贡献思想,要想异或后的数大,那么高位上的数需要为1,因为0和1异或得1,高位贡献多,因此我们抢到高位上的1即可获胜
拿到1之后需要选择让自己变还是让别人变,对此我们要进行分类讨论,总结规律
证明:1为奇,n为偶的情况
3个1,一个0,总共4个
A拿到1, B拿到0
A这个时候不可能让自己为0, 因此只有让B为1,那么最后B有最后一个1,可以让A为0
另一种情况,A给自己1让自己为0,B让自己为1,B也赢了
总结出规律之后,就是位运算+贡献思想+特判即可
#include<bits/stdc++.h>
using namespace std;
// A和B初始的数值都是0。
int num[23];
long long a;
void pre(long long a)
{int cnt=1;while(a){if(a&1)num[cnt]++;cnt++;a>>=1;}
}
int main()
{int T;ios::sync_with_stdio(0);cin.tie(0);cin>>T;while(T--){memset(num,0,sizeof(num));int n,sum=0;cin>>n;for(int i=0;i<n;i++){cin>>a;pre(a);sum^=a;}if(!sum)puts("0");else{for(int i=20;i>0;i--)if(num[i]==1){puts("1");break;}else if(num[i]%2==1){if(n%2==1){puts("1");break;}else if(n%2==0){puts("-1");break;}}}}return 0;
}
蓝桥杯2021年第十二届省赛-异或数列_如果a赢, 输出1 如果b赢, 输出-1 如果平局, 输出0-CSDN博客有看不懂的地方可以去看看另一个兄弟的题解