趣味XOR

14 篇文章 1 订阅

sgu275

友情链接

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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值