Problem
给出n(≤100000)个数字,要求将其分成两个集合,集合1的异或和为x1,集合2的异或和为x2,求出一种划分方案,使得在x1+x2最大的前提下x1尽量小。
Solution
这道题我刚看感觉很不可做,于是打了个暴力。
正解竟然是神奇的高斯消元。
首先,我们瞄到“异或”这个词,马上联想到位运算,继而联想到从高到低逐位来搞。当然我在比赛时也想到了这一点。(虽然也就想到了这一点)
于是,我们可以构建64个方程,方程fj形如:a1x1^a2x2^…^anxn=1/0。其中,xi表示第i个数在二进制下的第j位,ai表示是否要将第i个数分给集合1;而若在所有数中,有奇数个的第j位为1,则方程的右边为0,反之则反。因为若有奇数个数的第j位为1,则划分为两个集合后,肯定是一个集合为奇数,另一个集合为偶数,而又要使x1尽量小,所以将0分配给集合1;若有偶数个,则划分为两个集合后,可能两个都是奇数,也可能都是偶数,那我们要让x1+x2最大,所以最好都分配为奇数,于是异或和为1。
然后就开始上高斯消元。注意,我们必须优先解答案为1的方程,原因下面再说。由于要满足黑体的那句话,我们要以一种奇特的方式解方程。
我们先枚举j从0到1,表示那个位置有偶数/奇数个1;然后将答案为!j的方程(从高到低逐位放)塞进方程组,设其为f[tot]。然后,我们用上面的方程消当前方程,也即f[tot]^=f[k]。若消完后,f[tot]的左边的所有系数均变成0了,则说明这个方程费了——要么与上面的方程本质相同,要么就不能成立。这样就删掉这个方程。若还有非0的系数,则用这个方程“反哺”上面的方程。
这样的话,由于我们是先搞偶数方程,所以会先满足偶数方程。因为如果某个奇数方程被之前的偶数方程消完了,但是它的答案又为1,则说明它不可能成立,那我们就要无视掉这个方程,去满足那些偶数方程——因为满足一个偶数方程会产生两个1的收益,而我们必须保证x1+x2最大;而若某个方程被同类方程消完了,但答案亦为1,则说明它又不能成立,我们要优先使前面的方程成立,因为我们是从高到低逐位做的。
最后,所有方程的非0系数均会被消成≤1个,则那个位置的未知数就直接为答案;否则为自由元。由于本题方程数只有64,而未知数有n个,所以至少会有n-64个之多的自由元。对于它们,随便取即可。
当然,由于它是异或方程组,所以我们可以用c++的bitset优化,这样可以去掉一个log的复杂度。
时间复杂度:
。
Code
#include <cstdio>
#include <bitset>
using namespace std;
#define N 100010
#define M 64
#define ll long long
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
int i,j,k,n,tot,fir[M],ans[N];
ll num,xs,x;
bitset<N>map[M+1],f[M+1];
int main()
{
scanf("%d",&n);
fo(i,1,n)
{
scanf("%lld",&num);
xs^=num;
x=1;
fo(j,0,M)
{
map[j][i]=x#
x<<=1;
}
}
fo(j,0,1)
fd(i,M,0)
if(((xs>>i)&1)==j)
{
f[++tot]=map[i];fir[tot]=n+2;f[tot][n+1]=!j;
fo(k,1,tot-1)
if(f[tot][fir[k]])
f[tot]^=f[k];
fo(k,1,n)
if(f[tot][k])
{
fir[tot]=k;
break;
}
if(fir[tot]>n)
{
tot--;
continue;
}
fo(k,1,tot-1)
if(f[k][fir[tot]])
f[k]^=f[tot];
}
fo(i,1,tot)ans[fir[i]]=f[i][n+1];
fo(i,1,n)printf("%d ",2-ans[i]);
}