线性基
简介
线性基常用来处理异或问题。对于一个值域 \([1,n]\) ,就可以用一个长度为 \(\lceil \log_2 n \rceil\) 的数组来描述一个线性基。
特别地,线性基中第 \(i\) 位上的数在二进制下也有 \(i\) 位。
性质
线性基满足:对于它所表示的数的集合 \(S\) , \(S\) 中任意多个数异或的结果均可以用线性基中的数异或的结果表示。
插入
令插入的数为 \(x\) ,设 \(x\) 在二进制下有 \(i\) 位。
若线性基的第 \(i\) 位为 \(0\) ,则在第 \(i\) 位上插入 \(x\) ,结束。
反之,则将 \(x\) 异或上当前线性基中第 \(i\) 位的值 \(a_i\) ,并继续插入。
重复以上操作,直到 \(x\) 变为 \(0\) 。
如果结束时 \(x=0\) ,因为 \(x \oplus x=0\) ,所以在原来的线性基中就可以表示 \(x\) 了。
反之,则表示此时往线性基中加入了一个新元素,此时也能表示 \(x\) 了。
判断
用上面插入的方法判断,若最后 \(x=0\) ,则能表示,反之,不能表示。
查询异或最小值
根据线性基的定义,线性基中的所有数在二进制下的位数都不同。
所以,考虑线性基中最小的数,它异或上其它数一定比它原先的值大。
所以,线性基中(即原序列中)的数能通过异或产生的最小值即为线性基中最小的数。
查询异或最大值
考虑贪心,从高位到低位遍历线性基,若这一位上的值 \(a_i\) 在二进制下的第 \(i\) 位为 \(1\) 并且此时答案在二进制下的第 \(i\) 位为 \(0\) ,那么就将答案异或上 \(a_i\) ,因为这样答案一定不会变劣。
因此,遍历完线性基之后得到的答案一定就是线性基中(即原序列中)的数能通过异或产生的最大值。
例题
1
最板子的板子。
#include<iostream>
#include<cstdio>
#define int long long
using namespace std;
const int N=60;
int n,a[N],b[N];
int max_(int x,int y)
{
return x>y?x:y;
}
void ins(int x)
{
for(int i=N-1;i>=0;i--)
if(x&((int)1<<i))
{
if(!b[i])
{
b[i]=x;
return;
}
x^=b[i];
}
}
int query()
{
int sum=0;
for(int i=N-1;i>=0;i--)
sum=max_(sum,sum^b[i]);
return sum;
}
signed main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]),ins(a[i]);
printf("%lld",query());
return 0;
}
2
先进行一步转化,把开关的控制转化为异或上一个数。
很显然,这样的转化是成立的。
于是,把所有开关代表的数丢进线性基,最终,求出线性基中元素的数量 \(x\) ,
可以得到答案为 \(2^x\) 。
(每个元素都有选与不选两种情况,同时,线性基中的数不可能异或得到相同的数。)
#include<iostream>
#include<cstring>
#include<cstdio>
#define int long long
using namespace std;
const int N=60;
int n,m,b[N];
char a[N];
void ins(int x)
{
for(int i=N-1;i>=0;i--)
if(x&((int)1<<i))
{
if(!b[i])
{
b[i]=x;
return;
}
x^=b[i];
}
}
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%s",a);
int sum=0;
for(int j=0;j<n;j++)
sum+=((int)1<<j)*(a[j]=='O');
//cout<<sum<<endl;
ins(sum);
}
int ans=0;
for(int i=0;i<N;i++)
if(b[i]) ans++;
//cout<<ans<<endl;
printf("%lld",((int)1<<ans)%(int)2008);
return 0;
}
3
和 \(XOR\) 最大值有关,考虑用线性基求解。
考虑题目中一句重要的话:
路径可以重复经过某些点或边,当一条边在路径中出现了多次时,其权值在计算 XOR 和时也要被计算相应多的次数。
一条边走多遍,一定不会得到更大的贡献,但是,可以让你不花费更大的代价走到一个环里并走回来。
题目保证图为连通图,所以每个环都一定能走到。
注:下文中链和环的价值即为链和环的 \(XOR\) 和。
把所有环的价值都丢进线性基里,选一条链的价值作为初值,求 \(XOR\) 最大值。
如何选择作为初值的链?
假设 \(1\) ~ \(n\) 的路径有 \(A\) 和 \(B\) 两条。
\(A\) 和 \(B\) 一定会共同组成一个环。
假设我们先选择了 \(A\) 作为初值,如果 \(B\) 更优,因为所有环的价值都已经丢进了线性基里,所以求最大值时一定会发现异或上这个环的价值会使答案更优,所以最终也能得到 \(B\) 的价值。
因为所有 \(1\) ~ \(n\) 的路径一定会两两组成若干个环,所以无论选择哪条链的价值作为初值,最终都可以得到答案。
综上,可以随意选择一条 \(1\) ~ \(n\) 的路径的价值作为初值。
#include<iostream>
#include<cstdio>
#include<vector>
#define int long long
using namespace std;
const int N=5e4+10;
const int M=65;
struct node
{
int to,val;
};
vector <node> e[N];
int n,m,cnt[N],vis[N];
int b[M];
int max_(int x,int y)
{
return x>y?x:y;
}
void ins(int x)
{
for(int i=M-2;i>=0;i--)
if(x&(int)((int)1<<i))
{
if(b[i]) x^=b[i];
else
{
b[i]=x;
return;
}
}
}
int query(int x)
{
int sum=x;
for(int i=M-2;i>=0;i--)
sum=max_(sum,sum^b[i]);
return sum;
}
void dfs(int x,int sum)
{
vis[x]=1,cnt[x]=sum;
for(int i=0;i<e[x].size();i++)
{
if(!vis[e[x][i].to])
dfs(e[x][i].to,sum^e[x][i].val);
else ins(sum^e[x][i].val^cnt[e[x][i].to]);
}
}//判环,并且将环的价值扔进线性基里
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%lld%lld%lld",&x,&y,&z);
e[x].push_back((node){y,z});
e[y].push_back((node){x,z});
}
dfs(1,0);
printf("%lld",query(cnt[n]));
return 0;
}
4
考虑普通 NIM 游戏,若所有元素的异或和不为0,则先手必胜。
因为要最小化不能加入线性基的数的和,所以将元素从大到小排列,然后能取则取。
判断能不能取的部分用线性基实现。
#include<algorithm>
#include<iostream>
#include<cstdio>
using namespace std;
const int N=110;
const int M=60;
int n,a[N],b[M];
long long ans;
bool ins(int x)
{
for(int i=M-1;i>=0;i--)
if(x&(1<<i))
{
if(b[i]) x^=b[i];
else
{
b[i]=x;
return true;
}
}
return false;
}
bool cmp(int x,int y)
{
return x>y;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)
if(!ins(a[i])) ans+=(long long)a[i];
printf("%lld",ans);
return 0;
}
5
用和LCA一样的方法倍增维护路径上的线性基。
合并线性基时暴力将一个线性基内的元素插入另一个线性基里。
复杂度约为 \(O(q \log n \log G_i)\) ,还有一定的常数,需要卡常。
卡常技巧:
1.插入的数为0时直接不插。
2.若当前线性基已经满了直接不插。
#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int N=2e4+10;
const int M=64;
struct node
{
ll A[M];
bool flag;
}F[N][20];
struct edge
{
int nxt,to;
}e[N*2];
int cnt,head[N];
int n,q,d[N];
ll a[N];
int f[N][20],lg[N];
ll MAX(ll x,ll y)
{
return x>y?x:y;
}
void add(int x,int y)
{
e[++cnt].to=y;
e[cnt].nxt=head[x];
head[x]=cnt;
}
node operator + (node x,ll y)
{
if(!y) return x;
if(x.flag) return x;
for(int i=M-1;i>=0;i--)
if(y&(1ll<<i))
{
if(x.A[i]) y^=x.A[i];
else
{
x.A[i]=y;
break;
}
}
return x;
}
node operator + (node x,node y)
{
bool f=false;
for(int i=M-1;i>=0;i--) x=x+y.A[i];
for(int i=0;i<M;i++)
if(!x.A[i])
{
f=true;
break;
}
if(!f) x.flag=true;
return x;
}
ll query(node x)
{
ll sum=0;
for(int i=M-1;i>=0;i--)
sum=MAX(sum,sum^x.A[i]);
return sum;
}
void dfs(int x,int y)
{
f[x][0]=y,F[x][0]=F[x][0]+a[x];
d[x]=d[f[x][0]]+1;
for(int i=1;i<=lg[d[x]]+1;i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int i=1;i<=lg[d[x]]+1;i++)
F[x][i]=F[x][i-1]+F[f[x][i-1]][i-1];
for(int i=head[x];i;i=e[i].nxt)
if(e[i].to!=y) dfs(e[i].to,x);
}
ll LCA(int x,int y)
{
node ans;
for(int i=0;i<M;i++) ans.A[i]=0;
ans.flag=false;
if(d[x]<d[y]) swap(x,y);
while(d[x]>d[y])
{
ans=ans+F[x][lg[d[x]-d[y]]];
x=f[x][lg[d[x]-d[y]]];
}
if(x==y)
{
ans=ans+F[x][0];
return query(ans);
}
for(int i=lg[d[x]];i>=0;i--)
if(f[x][i]!=f[y][i])
{
ans=ans+F[x][i],ans=ans+F[y][i];
x=f[x][i],y=f[y][i];
}
ans=ans+F[x][0],ans=ans+F[y][0],ans=ans+F[x][1];
return query(ans);
}
int read()
{
int sum=0;
char x=getchar();
while(!(x>='0'&&x<='9')) x=getchar();
while(x>='0'&&x<='9') sum=sum*10+x-'0',x=getchar();
return sum;
}
int main()
{
for(int i=2;i<N;i++) lg[i]=lg[i/2]+1;
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
for(int i=1;i<n;i++)
{
int x=read(),y=read();
add(x,y),add(y,x);
}
dfs(1,0);
while(q--)
{
int x=read(),y=read();
printf("%lld\n",LCA(x,y));
}
return 0;
}