前言
线性基,通常用来解决这样一类问题,给你若干个数,让你从这些数的子集的异或和最值或者其它位运算相关操作
这种问题方面,线性基的性能显得非常优越,空间复杂度一般都是
O
(
l
o
g
值
域
)
O(log值域)
O(log值域)的,然后各种常见操作的复杂度在
O
(
l
o
g
值
域
)
O(log值域)
O(log值域)到
O
(
l
o
g
2
值
域
)
O(log^2值域)
O(log2值域)之间
最重要的是,线性基很好理解、很好写!
实现方式介绍
就拿做前面那个问题来说吧
首先,讲几个前置知识:
对于一个大小为n的集合
前置知识1
取它的子集的异或和的取值是有限的(解释:显然枚举所有子集就好,复杂度 O ( 2 n ) O(2^n) O(2n))
前置知识2
让其中一个数a异或上这个集合的另一个数b变成c,在这个集合里删除a添加c,它的子集的异或和的取值种类不变(解释:如果想要获得原来的a的效果,只需要加入c并且把b是否选择取反即可)
一个线性基里面有 O ( l o g 值 域 ) O(log值域) O(log值域)个位置,第i个位置的元素的最高位是 2 i 2^i 2i,根据这个性质就可以成功推出线性基操作的具体实现方式
定义一个线性基
只要开一个O(log值域)的数组就好了
struct Linear_Basis
{
int a[maxn];
Linear_Basis(){memset(a,0,sizeof(a));}
}
插入操作
对于一个线性基,现在要插入一个数,这个时候就从这个数的高位开始比对,如果相应的线性基是空的,那么就加进去(然后break),否则就异或上线性基里的数,来去除这个最高位上的值(由前置知识2可知正确性不变),那么如果到最后变成0了,那么说明这个数插入之后对线性基没有任何影响,否则说明有,复杂度 O ( l o g 值 域 ) O(log值域) O(log值域)
inline bool insert(register int val)
{
for(register int i=maxn-1;i>=0;i--)
if(val&bit[i])
{
if(!a[i])
{
a[i]=val;
break;
}
val^=a[i];
}
return val>0;
}
询问最大值操作
从高位往低位贪心,如果异或上这个数能够让答案变大那就异或,这么做一定优(如果能让这一位变成1那么就比低的位怎么变都要有用),复杂度 O ( l o g 值 域 ) O(log值域) O(log值域)
inline int query_max()const
{
register int res=0;
for(register int i=maxn-1;i>=0;i--)if((res^a[i])>res)res^=a[i];
return res;
}
询问不为0的最小值操作
从低位到高位,第一个有值的就是答案,复杂度 O ( l o g 值 域 ) O(log值域) O(log值域)
inline int query_min()const
{
for(register int i=0;i<maxn;i++)if(a[i])return a[i];
return 0;
}
合并操作
把线性基里的每一个元素都提出来,加入另一个线性基,复杂度 O ( l o g 2 值 域 ) O(log^2值域) O(log2值域)
inline void merge(const Linear_Basis &b)
{
for(register int i=0;i<maxn;i++)if(b.a[i])insert(b.a[i]);
}
化简线性基
枚举一个元素,把能消掉的位都化简掉,便于之后的第k小值的操作,并且提取出有值的位,复杂度 O ( l o g 2 值 域 ) O(log^2值域) O(log2值域)
inline void rebuild()
{
for(rg int i=maxn-1;i>=0;i--)
for(rg int j=i-1;j>=0;j--)
if(a[i]&bit[j])
a[i]^=a[j];
cnt=0;
for(rg int i=0;i<maxn;i++)
if(a[i])
p[cnt++]=a[i];
}
询问第k小操作
先化简一遍,预处理复杂度 O ( l o g 2 值 域 ) O(log^2值域) O(log2值域),单次询问只需要按进制异或起来即可,复杂度为 O ( l o g 值 域 ) O(log值域) O(log值域)
inline int kthquery(const int k)const
{
rg int res=0;
if(k>=bit[cnt])return -1;
for(rg int i=maxn-1;i>=0;i--)
if(k&bit[i])
res^=p[i];
return res;
}
整合代码
#include<cstdio>
#include<cctype>
#include<cstring>
#include<ctime>
namespace fast_IO
{
const int IN_LEN=10000000,OUT_LEN=10000000;
char ibuf[IN_LEN],obuf[OUT_LEN],*ih=ibuf+IN_LEN,*oh=obuf,*lastin=ibuf+IN_LEN,*lastout=obuf+OUT_LEN-1;
inline char getchar_(){return (ih==lastin)&&(lastin=(ih=ibuf)+fread(ibuf,1,IN_LEN,stdin),ih==lastin)?EOF:*ih++;}
inline void putchar_(const char x){if(oh==lastout)fwrite(obuf,1,oh-obuf,stdout),oh=obuf;*oh++=x;}
inline void flush(){fwrite(obuf,1,oh-obuf,stdout);}
}
using namespace fast_IO;
//#define getchar() getchar_()
//#define putchar(x) putchar_((x))
typedef long long LL;
#define int LL
#define rg register
template <typename T> inline T max(const T a,const T b){return a>b?a:b;}
template <typename T> inline T min(const T a,const T b){return a<b?a:b;}
template <typename T> inline T abs(const T a){return a>0?a:-a;}
template <typename T> inline void swap(T&a,T&b){T c=a;a=b;b=c;}
template <typename T> inline T gcd(const T a,const T b){if(a%b==0)return b;return gcd(b,a%b);}
template <typename T> inline T square(const T x){return x*x;};
template <typename T> inline void read(T&x)
{
char cu=getchar();x=0;bool fla=0;
while(!isdigit(cu)){if(cu=='-')fla=1;cu=getchar();}
while(isdigit(cu))x=x*10+cu-'0',cu=getchar();
if(fla)x=-x;
}
template <typename T> void printe(const T x)
{
if(x>=10)printe(x/10);
putchar(x%10+'0');
}
template <typename T> inline void print(const T x)
{
if(x<0)putchar('-'),printe(-x);
else printe(x);
}
const int maxn=51;
int bit[maxn];
inline void init()
{
bit[0]=1;for(rg int i=1;i<maxn;i++)bit[i]=bit[i-1]<<1;
}
int n,p[maxn],cnt;
struct Linear_Basis
{
int a[maxn];
Linear_Basis(){memset(a,0,sizeof(a));}
inline bool insert(int val)
{
for(rg int i=maxn-1;i>=0;i--)
if(val&bit[i])
{
if(!a[i])
{
a[i]=val;
break;
}
val^=a[i];
}
return val>0;
}
inline void merge(const Linear_Basis &b)
{
for(rg int i=0;i<maxn;i++)if(b.a[i])insert(b.a[i]);
}
inline int query_max()const
{
rg int res=0;
for(rg int i=maxn-1;i>=0;i--)if((res^a[i])>res)res^=a[i];
return res;
}
inline int query_min()const
{
for(rg int i=0;i<maxn;i++)if(a[i])return a[i];
return 0;
}
inline void rebuild()
{
for(rg int i=maxn-1;i>=0;i--)
for(rg int j=i-1;j>=0;j--)
if(a[i]&bit[j])
a[i]^=a[j];
cnt=0;
for(rg int i=0;i<maxn;i++)
if(a[i])
p[cnt++]=a[i];
}
inline int kthquery(const int k)const
{
rg int res=0;
if(k>=bit[cnt])return -1;
for(rg int i=maxn-1;i>=0;i--)
if(k&bit[i])
res^=p[i];
return res;
}
}Q;
signed main()
{
init();
read(n);
for(rg int i=1;i<=n;i++)
{
int x;read(x);
Q.insert(x);
}
print(Q.query_max());
return flush(),0;
}
模板题
经典应用(update by 2018/12/28)
给出一个包含
n
n
n个
m
m
m位二进制数的可重集合,求有多少个子集满足所有数的异或值为
0
0
0
这是一个非常经典的问题,做起来很简单,只要把每个数插到线性基里,记最后线性基里的元素为
x
x
x,那么最后答案为
2
n
−
x
2^{n-x}
2n−x
复杂度
Θ
(
n
m
⌈
m
64
⌉
)
\Theta(nm\left\lceil\frac m{64}\right\rceil)
Θ(nm⌈64m⌉),非常优越
当然,如果
n
n
n比较小的话,复杂度可以变为
Θ
(
n
m
⌈
n
64
⌉
)
\Theta(nm\left\lceil\frac n{64}\right\rceil)
Θ(nm⌈64n⌉)
有一种很好的写法是:对于一位,根据所有数在这一位上是否为
1
1
1来构造一个长度为
n
n
n的
01
01
01串,把它放在长度为
n
n
n的线性基里就好了
(转化的正确性证明:把所有的
n
n
n个
m
m
m位
01
01
01串看成矩阵,那么线性基中的元素数量等于矩阵秩的数量,矩阵转置后秩的数量不变,所以可以这么做)
证明:若一个数能加入线性基中,说明这个数如果被选择,那么将没有办法通过已有的数将其的贡献抵消,无贡献;若一个数不能加入线性基中,那么我们可以选入这个数,并且翻转线性基中对应位的选择状态,那么选或不选,方案数乘
2
2
2
例题:hdu3915
代码
#include<cstdio>
#include<cctype>
#include<algorithm>
#include<cstring>
#define rg register
typedef long long LL;
template <typename T> inline T max(const T a,const T b){return a>b?a:b;}
template <typename T> inline T min(const T a,const T b){return a<b?a:b;}
template <typename T> inline void mind(T&a,const T b){a=a<b?a:b;}
template <typename T> inline void maxd(T&a,const T b){a=a>b?a:b;}
template <typename T> inline T abs(const T a){return a>0?a:-a;}
template <typename T> inline void swap(T&a,T&b){T c=a;a=b;b=c;}
template <typename T> inline T gcd(const T a,const T b){if(!b)return a;return gcd(b,a%b);}
template <typename T> inline T lcm(const T a,const T b){return a/gcd(a,b)*b;}
template <typename T> inline T square(const T x){return x*x;};
template <typename T> inline void read(T&x)
{
char cu=getchar();x=0;bool fla=0;
while(!isdigit(cu)){if(cu=='-')fla=1;cu=getchar();}
while(isdigit(cu))x=x*10+cu-'0',cu=getchar();
if(fla)x=-x;
}
template <typename T> inline void printe(const T x)
{
if(x>=10)printe(x/10);
putchar(x%10+'0');
}
template <typename T> inline void print(const T x)
{
if(x<0)putchar('-'),printe(-x);
else printe(x);
}
const int mod=1000007;
int T,N,bit[101];
int base[31],sum;
int main()
{
bit[0]=1;
for(rg int i=1;i<=100;i++)bit[i]=(bit[i-1]<<1)%mod;
read(T);
while(T--)
{
memset(base,0,sizeof(base)),sum=0;
read(N);
for(rg int i=1;i<=N;i++)
{
int x;read(x);
for(rg int j=30;j>=0;j--)
if((x&(1<<j)))
{
if(base[j])x^=base[j];
else
{
sum++;
base[j]=x;
break;
}
}
}
print(bit[N-sum]),putchar('\n');
}
return 0;
}
总结
在使用线性基的过程中,要看好数据范围,注意longlong的问题。然后,线性基也是一个很灵活的东西,可以自己更改具体实现的方式来增强功能,然后就可以做出各种题目啦。当然线性基使用范围不算广,所以一般比赛里碰到的话最多是作为算法的一个部分,但是思维方式非常妙,可以好好研究一下