感谢大佬博客:https://blog.youkuaiyun.com/qaq__qaq/article/details/53812883
定义
设数集TT的值域范围为[1,2n−1][1,2n−1]。
TT的线性基是TT的一个子集A={a1,a2,a3,...,an}A={a1,a2,a3,...,an}。
AA中元素互相xor所形成的异或集合,等价于原数集TT的元素互相xor形成的异或集合。
可以理解为将原数集进行了压缩。
性质
1.设线性基的异或集合中不存在00。
2.线性基的异或集合中每个元素的异或方案唯一,其实这个跟性质1是等价的。
3.线性基二进制最高位互不相同。
4.如果线性基是满的,它的异或集合为[1,2n−1][1,2n−1]。
5.线性基中元素互相异或,异或集合不变。
维护
插入
如果向线性基中插入数x,从高位到低位扫描它为1的二进制位。
扫描到第i时,如果ai不存在,就令ai=x,否则x=x⊗ai。
x的结局是,要么被扔进线性基,要么经过一系列操作过后,变成了0。
bool insert(ll val){
for(int i=63;i>=0;--i){///最多搞63,不能搞64,1ll<<64爆longlong了
if(val&(1ll<<i)){
if(!d[i]){
d[i]=val;///如果第i位的线性基不存在则插入
break;
}
val^=d[i];///如果第i位已经被占则寻找该值插入的下一个位置
}
}
return val>0;///如果val==0则表示未插入线性基中
}
合并
将一个线性基暴力插入另一个线性基即可。
Linner_base merge(const Linner_base& a,const Linner_base& b)///合并线性基
{
Linner_base res=a;
for(int i=0;i<=63;i++){
if(b.d[i]){
res.insert(b.d[i]);
}
}
return res;
}
查询
存在性
如果要查询x是否存于异或集合中。
从高位到低位扫描x的为1的二进制位。
扫描到第i位的时候x=x⊗ai
如果中途x变为了0,那么表示x存于线性基的异或集合中。
最大值
从高位到低位扫描线性基。
如果异或后可以使得答案变大,就异或到答案中去。
ll query_max(ll res=0)///如果不含初值res=0,否则res=初值
{
for(int i=63;i>=0;--i){
res=max(res^d[i],res);
}
return res;
}
最小值
最小值即为最低位上的线性基。如果含有初值,则与最大值类似
ll query_min(ll res=0)
{
if(!res){///无初值
for(int i=0;i<=63;++i)
if(d[i]){
res=d[i];
break;
}
}
else {///有初值
for (int i = 0; i <=63; ++i) {
res = min(res ^ d[i], res);
}
}
return res;
}
第K小
根据性质3。
我们要将线性基改造成每一位相互独立。
具体操作就是如果i<j,aj的第i位是1,就将aj异或上ai。
经过一系列操作之后,对于二进制的某一位i。只有ai的这一位是1,其他都是0。
所以查询的时候将k二进制拆分,对于1的位,就异或上对应的线性基。
最终得出的答案就是k小值。
void rebuild()///使每个基底a[i]都只是第i位为1,其余均为0
{
for(int i=63;i>=0;i--)
for(int j=i-1;j>=0;--j){
if(d[i]&(1ll<<j))
d[i]^=d[j];
}
for(int i=0;i<=63;i++){
if(d[i]){
nd[tot++]=d[i];
}
}
}
ll query_kthmin(ll k)///求第k小
{
ll ans=0;
if(k>=(1ll<<tot)) return -1;
for(int i=0;i<=63;++i){
if(k&(1ll<<i)){
ans^=nd[i];
}
}
return ans;
}
例题1:BZOJ2460
题意:有N个元素,每个元素有a,b两种属性,要求N个中选任意多个元素,使a异或起来不为0,b加起来最大。
分析:首先考虑b加起来最大,对N个元素按b进行从大到小排序,依次插入线性基,由于线性基的所以基底异或不会为0,刚好满足a的限制,最后把所有能插入线性基的元素的b加起来即可。
Ac code:
#include<cstdio>
#include <iostream>
#include <cmath>
#include <stack>
#include <vector>
#include <algorithm>
#include <set>
#include <queue>
#include <cstring>
using namespace std;
const int maxn = 1e3 + 5;
typedef long long ll;
struct Node{
ll id;
int val;
bool operator<(const Node& a)const{
return val>a.val;
}
}a[maxn];
struct Linner_base{
ll d[65];
void init()
{
memset(d,0,sizeof d);
}
bool insert(ll id)
{
for(int i=63;i>=0;--i){
if(id&(1ll<<i)){
if(!d[i]){
d[i]=id;
break;
}
id^=d[i];
}
}
return id>0;
}
}LB;
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i].id>>a[i].val;
sort(a+1,a+n+1);
ll ans=0;
for(int i=1;i<=n;i++){
if(LB.insert(a[i].id)){
ans+=a[i].val;
}
}
cout<<ans<<endl;
return 0;
}
例题2:hdu3949
题意:给你n个数,从中选任意多个数异或起来,问所有能组成的数中的第k小的数是多少
分析:线性基求第K小板子题,关键在与是否可以产生0的判断。
Ac code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e4+5;
struct Linner_base{
ll d[65],nd[65];
int tot;
void init(){
tot=0;
memset(d,0,sizeof d);
memset(nd,0,sizeof nd);
}
bool insert(ll val){
for(int i=63;i>=0;--i){///最多搞63,不能搞64,1ll<<64爆longlong了
if(val&(1ll<<i)){
if(!d[i]){
d[i]=val;
break;
}
val^=d[i];
}
}
return val>0;
}
void rebuild()///使每个基底a[i]都只是第i位为1,其余均为0
{
for(int i=63;i>=0;i--)
for(int j=i-1;j>=0;--j){
if(d[i]&(1ll<<j))
d[i]^=d[j];
}
for(int i=0;i<=63;i++){
if(d[i]){
nd[tot++]=d[i];
}
}
}
ll query_kthmin(ll k)///求第k小
{
ll ans=0;
if(k>=(1ll<<tot)) return -1;
for(int i=0;i<=63;++i){
if(k&(1ll<<i)){
ans^=nd[i];
}
}
return ans;
}
}LB;
int main()
{
ios::sync_with_stdio(0),cin.tie(0);
int t,n,q;
cin>>t;
ll x;
int cas=0;
while(t--){
cout<< "Case #" << ++cas<< ":" <<endl;
cin>>n;
LB.init();
bool flag=0;
for(int i=1;i<=n;i++){
cin>>x;
if(!LB.insert(x))///判断是否可生成0
flag=1;
}
LB.rebuild();
ll k;
cin>>q;
while(q--){
cin>>k;
if(!flag) cout<<LB.query_kthmin(k)<<endl;
else{
cout<<LB.query_kthmin(k-1)<<endl;
}
}
}
return 0;
}
总板子:
struct Linner_base{
ll d[65],nd[65];
int tot;
void init(){
tot=0;
memset(d,0,sizeof d);
memset(nd,0,sizeof nd);
}
bool insert(ll val){
for(int i=63;i>=0;--i){///最多搞63,不能搞64,1ll<<64爆longlong了
if(val&(1ll<<i)){
if(!d[i]){
d[i]=val;
break;
}
val^=d[i];
}
}
return val>0;
}
void rebuild()///使每个基底a[i]都只是第i位为1,其余均为0
{
for(int i=63;i>=0;i--)
for(int j=i-1;j>=0;--j){
if(d[i]&(1ll<<j))
d[i]^=d[j];
}
for(int i=0;i<=63;i++){
if(d[i]){
nd[tot++]=d[i];
}
}
}
Linner_base merge(const Linner_base& a,const Linner_base& b)///合并线性基
{
Linner_base res=a;
for(int i=0;i<=63;i++){
if(b.d[i]){
res.insert(b.d[i]);
}
}
return res;
}
ll query_max(ll res=0)
{
for(int i=63;i>=0;--i){
res=max(res^d[i],res);
}
return res;
}
ll query_min(ll res=0)
{
if(!res){///无初值
for(int i=0;i<=63;++i)
if(d[i]){
res=d[i];
break;
}
}
else {///有初值
for (int i = 0; i <=63; ++i) {
res = min(res ^ d[i], res);
}
}
return res;
}
ll query_kthmin(ll k)///求第k小
{
ll ans=0;
if(k>=(1ll<<tot)) return -1;
for(int i=0;i<=63;++i){
if(k&(1ll<<i)){
ans^=nd[i];
}
}
return ans;
}
}LB;
例题:https://ac.nowcoder.com/acm/contest/881/H
参考博客:https://www.cnblogs.com/Yinku/p/11212303.html
题意:给定n个整数,求其中异或和为 0 的子集的大小的和。
关键解题思路:首先转化为每个可以通过异或表示 00 的数贡献它参与的子集数。而不是直接求每个子集的大小,这样很不好求。
分析:
首先转化为每个可以通过异或表示0的数贡献它参与的子集数。
对n个数进行一次消元,得到一组大小为r的线性基B1。
那么剩下的n−r个满足以下的规律:
设现在要计算的元素为X1,其他n−r−1个元素任意组合,总能得到一个整数,再加入X1也是一个整数,这个整数能被线性基B1表示,则这个X1可以对他们贡献总计个次。
接下来算线性基B1里的数的贡献。
再对这n−r个数进行一次消元, 得到另一组线性基B2。枚举B1的一个线性基X2,对其他的r-1+B2个数消元,得到某个线性基B3。
Ac code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
const ll mod=1e9+7;
struct Linner_base{
ll d[64];
int r;
void init(){
r=0;
memset(d,0,sizeof d);
}
void Copy(Linner_base &b){
r=b.r;
memcpy(d,b.d,sizeof b.d);
}
void insert(ll val){
for(int i=63;i>=0;--i){
if(val&(1ll<<i)){
if(!d[i]){
d[i]=val;
++r;
break;
}
val^=d[i];
}
}
}
bool check(ll val)
{
for(int i=63;i>=0;--i){
if(val&(1ll<<i)){
if(!d[i]){
return 0;
}
val^=d[i];
}
}
return 1;
}
}B1,B2,B3;
ll Pow(ll a,ll b)
{
ll ans=1,base=a;
while(b){
if(b&1)
ans=(ans*base)%mod;
base=(base*base)%mod;
b>>=1;
}
return ans;
}
ll a[maxn];
vector<int>id;
int main()
{
int n;
while(~scanf("%d",&n)){
B1.init(),B2.init(),B3.init();
id.clear();
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
for(int i=1;i<=n;i++){
if(B1.check(a[i])){///插不进B1就插入B2中
B2.insert(a[i]);
}
else{///先把能插入B1的插入B1
B1.insert(a[i]);
id.push_back(i);
}
}
ll ans=0,p;
if(n>B1.r){///第一步:求线性基B1外的每个数在出现在异或为0的子集中的次数
p=Pow(2,n-B1.r-1);
ans=(ans+p*(n-B1.r))%mod;
}
for(auto &i:id){///方法:考虑B1中的每个数x,若去掉x后的B1和B2合起来组成B3,判断x是否在B3中,如果在就加上x的贡献
B3.Copy(B2);
for(auto &j:id){
if(i!=j) B3.insert(a[j]);
}
if(B3.check(a[i])){///第二步:求线性基内部每个数出现在异或为0的子集中的次数
ans=(ans+p)%mod;
}
}
printf("%lld\n",ans);
//fflush(stdout);
}
return 0;
}
例题:http://acm.hdu.edu.cn/showproblem.php?pid=5544
题意:给你一张图,N个点,M条边,每条边都有一条权值,1号结点是起点,从起点开始,最后回到起点,求经过的边的权值异或最大和。
注意:一条边被走多次也会被异或多次
分析:由于从1出发又回到1,不在环上的边相当于没走,故问题就转化为了从多个环中选任意个环进行异或,使得值最大,显然直接用线性基即可。
Ac code:
#include <bits/stdc++.h>
using namespace std;
const int N=5e4+10;
const int M=1e5+10;
typedef long long ll;
int head[N];
struct Edge{
int v,nxt;
ll w;
}edge[M<<1];
int tot,n;
ll ans,val[N];
bool vis[N];
struct Linner_base{
ll d[64];
void init(){
memset(d,0,sizeof d);
}
bool insert(ll val)
{
for(int i=63;i>=0;--i){
if(val&(1ll<<i)){
if(!d[i]){
d[i]=val;
break;
}
val^=d[i];
}
}
return val>0;
}
ll query_max(ll res=0)
{
for(int i=63;i>=0;--i)
res=max(res,res^d[i]);
return res;
}
}LB;
void addedge(int u,int v,ll w)
{
edge[tot].v=v;
edge[tot].w=w;
edge[tot].nxt=head[u];
head[u]=tot++;
}
void init()
{
tot=0;
memset(head,-1,sizeof head);
memset(vis,0,sizeof vis);
memset(val,0,sizeof val);
}
void dfs(int u,ll sum,int pre)
{
vis[u]=1;
val[u]=sum;///存第一次走到u点的sum值
for(int i=head[u];~i;i=edge[i].nxt){
int v=edge[i].v;
ll w=edge[i].w;
if(v==pre) continue;
if(!vis[v]) dfs(v,sum^w,u);
else{
LB.insert(sum^w^val[v]);
}
}
}
int main()
{
int t,m;
scanf("%d",&t);
int cas=0;
while(t--){
init();
LB.init();
scanf("%d%d",&n,&m);
int u,v;
ll w;
while(m--){
scanf("%d%d%lld",&u,&v,&w);
addedge(u,v,w);
addedge(v,u,w);
}
dfs(1,0,0);
printf("Case #%d: %lld\n",++cas,LB.query_max());
}
return 0;
}