1、题意:给出n个数,选出来一个子集,使得集合的异或最大!
思路:高斯消元,将给出来的数写成一行二进制,依次排下来,如题目给的排成一个矩阵如下
1011
1001
0101
然后去消元,在消元的过程中保证当前的位置的左下角矩阵全为零,即设置一个变量in表示消元到第几列,如果当前列找不到非零的元素,那么可以调到下一列,行数不变。然后消元一直到 "行数>=n或者列数>=m"停止,这里假设行数是n,列数是m,保证消元过的每一列最多只有一个1,然后最后把每一列的元素异或起来,并写成二进制,这个就是异或和最大。
上面消元的矩阵过程如下如下
1011
0010
0101
->
1011
0101
0010
->用最后一行去消元第一行
1001
0101
0010
然后就是所有列异或得到1110(2)=14(10)
ps:假入这里要求我们求出异或出来使得最大值的是哪些数,我们可以在原来矩阵的右边并上一个n行n列的单位矩阵,消元出来之后也是跟着所有列异或,得到一个二进制串,从左到右分别的第i个位分别表示选不选第i个数,然后这个二进制串就是所要求的集合的二进制表示了!
int n,m,cnt;
int a[MM][MM];
int find(int x,int in)
{
for(int i=x;i<n;i++)if(a[i][in])return i;return -1;
}
void solve()
{
long long x,ans=0;
memset(a,0,sizeof(a));
for(int i=0;i<n;i++){
cin>>x;cnt=0;
while(x>0){
a[i][cnt++]=x&1;
x>>=1;
}
m=max(m,cnt);
}
for(int i=0;i<n;i++){
for(int j=0;j<m/2;j++)swap(a[i][j],a[i][m-j-1]);
}//到这里都是数据的处理,即转化成矩阵
for(int i=0,in=0;i<n&&in<m;i++,in++)
{
int x;
while((x=find(i,in))==-1&&in<m)in++;//找到一列有非零元素的列
if(in==m)break;//找不到非零列,终止
if(x!=i)for(int j=0;j<m;j++)swap(a[i][j],a[x][j]);//将非零元素提到当前行
for(int j=i-1;j>=0;j--)if(a[j][in]){//当前行向下消元
for(int k=0;k<m;k++)a[j][k]^=a[i][k];
}
for(int j=i+1;j<n;j++)if(a[j][in]){//当前行向上消元
for(int k=0;k<m;k++)a[j][k]^=a[i][k];
}
}
for(int j=0;j<m;j++)//全部列异或
{
int t=0;
for(int i=0;i<n;i++)t^=a[i][j];
if(t)ans+=1LL<<(m-j-1);
}
cout<<ans<<endl;
}
int main()
{
while(~scanf("%d",&n))solve();
return 0;
}
贴一份莫涛的代码,简洁时间复杂度也低O(n*64),减少了消元的过程,把可以用已经有的数凑出来的数去掉,最后消元的行数剩下63,这63能生成所给的数据,然后对这63行进行异或,再用压缩优化,使复杂度降低到O(n*64)
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
void MAX_XOR(int n)
{
long long base[64];
memset(base,0,sizeof(base));
for(int k=0;k<n;k++)
{
long long x;
scanf("%lld",&x);
for(int i=62;i>=0;i--)
{
if((x>>i)&1)
{
if(base[i]==0)
{
base[i]=x;
break;
}
else x^=base[i];
}
}
}
for(int i=0;i<63;i++)
{
for(int j=i+1;j<63;j++)
{
if((base[j]>>i)&1)
{
base[j]^=base[i];
}
}
}
long long ans=0;
for(int i=0;i<63;i++)
ans^=base[i];
printf("%lld\n",ans);
}
int main()
{
int n;
scanf("%d",&n);
MAX_XOR(n);
return 0;
}
2、另一类XOR问题
Zeus 和 Prometheus 做了一个游戏,Prometheus 给 Zeus 一个集合,集合中包含了N个正整数,随后 Prometheus 将向 Zeus 发起M次询问,每次询问中包含一个正整数 S ,之后 Zeus 需要在集合当中找出一个正整数 K ,使得 K 与 S 的异或结果最大。
对n个数建立一颗二进制前缀树,然后贪心在树上找数就行了,代码如下
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int cnt=1;
long long dp[5100000][2];
void insert(int root,long long x,int now)
{
if(now<0)return ;
long long &ans=dp[root][(x>>now)&1];
if(ans==-1)ans=cnt++;
insert(ans,x,now-1);
}
long long query(int root,long long x,long long ans,int now)
{
if(now<0)return ans;
long long &temp=dp[root][((x>>now)&1)^1];
if(temp==-1)query(dp[root][(x>>now)&1],x,ans*2+((x>>now)&1),now-1);
else query(temp,x,ans*2+(((x>>now)&1)^1),now-1);
}
int main()
{
int T,Case=1;
scanf("%d",&T);
while(T--)
{
cnt=1;
int n,m;
scanf("%d%d",&n,&m);
memset(dp,-1,sizeof(dp));
for(int i=0;i<n;i++)
{
long long x;
scanf("%I64d",&x);
insert(0,x,32);
}
printf("Case #%d:\n",Case++);
for(int i=0;i<m;i++)
{
long long x;
scanf("%I64d",&x);
printf("%I64d\n",query(0,x,0,32));
}
}
return 0;
}
3、给定一排数,求连续的子串使得异或和最大。
做法:令F[i,j],表示下标是i的到下标是j的数的异或和,根据异或的性质我们可以知道F[i,j]=F[1,i-1] XOR F[1,j],也就是在前缀F中找到一个数t,使得t XOR F[1,j]最大,这样可以转化成第二类问题
题目:LA 4682 - XOR Sum
友情链接:点击打开
以下是我的丑陋的代码:
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int cnt=1;
long long dp[4100000][2];
void insert(int root,long long x,int now)
{
if(now<0)return ;
long long &ans=dp[root][(x>>now)&1];
if(ans==-1)ans=cnt++;
insert(ans,x,now-1);
}
long long query(int root,long long x,long long ans,int now)
{
if(now<0)return ans;
long long &temp=dp[root][((x>>now)&1)^1];
if(temp==-1)query(dp[root][(x>>now)&1],x,ans*2+((x>>now)&1),now-1);
else query(temp,x,ans*2+(((x>>now)&1)^1),now-1);
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
cnt=1;
int n,m;
long long ans=0,now=0;
scanf("%d",&n);
memset(dp,-1,sizeof(dp));
for(int i=0;i<n;i++)
{
long long x;
scanf("%lld",&x);
insert(0,now,33);
now^=x;
ans=max(ans,now^query(0,now,0,33));
}
cout<<ans<<endl;
}
return 0;
}
4、给定一个有n个数的数组,求这个数组的子串异或和小于k的个数
思路:同样用上面的方法,只不过在每个节点上面记录有多少个数要经过这个节点,当低位无论怎么异或都无法大于k时,这个时候返回记录的数就可以了,然后一直向下统计就是答案了
友情链接:点击打开链接
丑陋代码如下:
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int cnt=1;
int dp[4100000][2];
int num[4100000][2];
void insert(int root,int x,int now)
{
if(now<0)return ;
num[root][(x>>now)&1]++;
int &ans=dp[root][(x>>now)&1];
if(ans==-1)ans=cnt++;
insert(ans,x,now-1);
}
///返回当前树节点值和x的异或和比k小的个数
int query(int root,int x,int ans,int now,int k,int fa)
{
int res=0;
if(ans>=k)return 0;///重要的剪枝
int first=(x>>now)&1,second=((x>>now)&1)^1;
if(now<0)return (ans<k?fa:0);
if(dp[root][first]!=-1){
if(ans+(1<<now+1)<k)res+=num[root][first];///无论后面的异或情况如何,异或出来永远小于k
else res+=query(dp[root][first],x,ans,now-1,k,num[root][first]);
}
if(dp[root][second]!=-1){
if(ans+(1<<now+1)<k)res+=num[root][second];///无论后面的异或情况如何,异或出来永远小于k
else res+=query(dp[root][second],x,ans+(1<<now),now-1,k,num[root][second]);
}
return res;
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
cnt=1;
int n,k,now=0;
memset(dp,-1,sizeof(dp));
memset(num,0,sizeof(num));///对每个节点计数
scanf("%d%d",&n,&k);
long long ans=0;
for(int i=0;i<n;i++)
{
int x;
scanf("%d",&x);
insert(0,now,25);
now^=x;
ans+=query(0,now,0,25,k,0);
}
cout<<ans<<endl;
}
return 0;
}