【数学】线性基
线性基
学习资料:OI Wiki
论记笔记的重要性:20天前学的东西,今天做题差点想不起我学过它 (._."ll)
概念
线性基是向量空间的一组基,通常可以解决有关异或的一些题目。
是由一个集合构造出来的另一个集合,有如下性质:
- 线性基的元素能相互异或得到原集合的元素的所有相互异或得到的值。
- 线性基是满足性质 1 的最小的集合。
- 线性基没有异或和为 0 的子集。
- 线性基中每个元素的异或方案唯一,即,线性基中不同的异或组合异或出的数是不一样的。
- 线性基中每个元素的二进制最高位互不相同。
构造方法
对元集合的每个数 \(x\) 从高位向低位扫,如果第 \(i\) 位是1 ,如果 \(p_i\) 不存在,那么令 \(p_i=x\) 并结束扫描,如果存在,令 \(x=x\,xor\,p_i\) 。
void insert(ll x)
{
for(int i=60;i>=0;i--)
{
if(!(x>>i))continue;
if(!p[i]){
p[i]=x;break;
}
x^=p[i];
}
}
例题
题意:给定n个整数(数字可能重复),求在这些数中选取任意个,使得他们的异或和最大。
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define mkp(a,b) make_pair(a,b)
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
const int mod=998244353;
const int UP=52;
struct LinearBasis{
ll a[UP+1];
LinearBasis(){
std::fill(a,a+UP+1,0);
}
void insert(ll t)
{
for(int j=UP;j>=0;j--)
{
if(!((t>>j)&1))continue;
if(!a[j]){
a[j]=t;return;
}
t^=a[j];
}
}
}lib;
int n;
int main()
{
scanf("%d",&n);
ll x;
for(int i=1;i<=n;i++){
scanf("%lld",&x);
lib.insert(x);
}
ll res=0;
for(int i=UP;i>=0;i--)
if((res^lib.a[i])>res)res^=lib.a[i];
printf("%lld\n",res);
}
题意:给定一张无向连通图,要从 1 走到 n ,每条边可以重复走过多次,求路径经过的边权值 XOR 和的最大值。
解:
已知相同权值相互异或为 0 ,如果对一条路径来回走一遍,则会抵消它们的异或贡献。那么,这张无向图的所有的环路的权值都可以 可选择地作为答案的贡献。
那么答案就是为 dis[n] (dis[n] 为从 1 到 n 的一条路径的异或和) 再异或 所有环路相互异或结果 取最大值。即将环路的异或值放进线性基,之后只需从大到小 让答案异或线性基的值取最优值。
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define mkp(a,b) make_pair(a,b)
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
const int mod=998244353;
int head[maxn],to[maxn<<2],nxt[maxn<<2],pcnt;ll ww[maxn<<2];
inline void add(int u,int v,ll w){
to[++pcnt]=v;nxt[pcnt]=head[u];ww[pcnt]=w;head[u]=pcnt;
}
const int UP=60;
struct LinearBasis{
ll a[UP+1];
LinearBasis(){
std::fill(a,a+UP+1,0);
}
void insert(ll t)
{
for(int j=UP;j>=0;j--)
{
if(!((t>>j)&1))continue;
if(!a[j]){
a[j]=t;return;
}
t^=a[j];
}
}
}lib;
int n,m;
bool vis[maxn];ll dis[maxn];
void dfs(int u,ll d){
dis[u]=d;vis[u]=true;
for(int i=head[u];i;i=nxt[i])
if(!vis[to[i]])dfs(to[i],d^ww[i]);
else lib.insert(d^dis[to[i]]^ww[i]);
}
int main()
{
int u,v;ll w;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%lld",&u,&v,&w);
add(u,v,w);add(v,u,w);
}
dfs(1,0);
ll res=dis[n];
for(int i=UP;i>=0;i--)
if((res^lib.a[i])>res)res^=lib.a[i];
printf("%lld\n",res);
}
3,XOR序列
题意:有 n 个数,对于任意的 x , y ,能否将 x 与这 n 个数中的任意多个数异或任意多次后变为 y 。
解:
将n个数插入线性基后,即求线性基能否异或得到 x^y 。
注意到线性基有一个性质:线性基中每个元素的二进制最高位互不相同。
那么,只需要对 x 从高位到低位判断,如果 x 在二进制第 i 位为 1,则将 x异或 lib.a[i]。
如果在最后 x 的值变为 0,则答案为 YES
,否则答案为 NO
。
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define mkp(a,b) make_pair(a,b)
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
const int mod=998244353;
const int UP=31;
struct LinearBasis{
ll a[UP+1];
LinearBasis(){
std::fill(a,a+UP+1,0);
}
void insert(ll t)
{
for(int j=UP;j>=0;j--)
{
if(!((t>>j)&1))continue;
if(!a[j]){
a[j]=t;return;
}
t^=a[j];
}
}
}lib;
int n;
int main()
{
scanf("%d",&n);
ll x,y;
for(int i=1;i<=n;i++){
scanf("%lld",&x);
lib.insert(x);
}
int Q;
scanf("%d",&Q);
while(Q--)
{
scanf("%lld%lld",&x,&y);
x^=y;
for(int i=UP;i>=0;i--)
if(x&(1<<i)){
//另一种写法 if(!lib.a[i])break;
x^=lib.a[i];
}
if(!x)puts("YES");
else puts("NO");
}
}
题意:有n个数对: <序号,权值>,求最大的权值和,使得选中数对不存在子集使得其序号异或和为 0 。
解:
当有子集异或和为 0 时,必能将子集分成 1 个数 x 和另一个异或和为 x 的子集。
那么,将数对按权值从大到小排序,在点 i ,只要对于当前集合存在与 i 的序号相同的异或和时,就不要将 i 添加进集合,因为集合中的所有元素的权值都大于等于 i 的权值,舍去 i 是最佳的。
即,排序,判断当前序号值是否能被线性基异或得到,若否,则将 当前点权值加进答案,并将当前点序号更新进线性基。
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define mkp(a,b) make_pair(a,b)
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
const int mod=998244353;
const int UP=62;
struct LinearBasis{
ll a[UP+1];
LinearBasis(){
std::fill(a,a+UP+1,0);
}
void insert(ll t)
{
for(int j=UP;j>=0;j--)
{
if(!((t>>j)&1))continue;
if(!a[j]){
a[j]=t;return;
}
t^=a[j];
}
}
}lib;
struct P{
ll v;int w;
bool operator<(const P&p)const{return w>p.w;}
}p[maxn];
int n;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%lld%d",&p[i].v,&p[i].w);
sort(p+1,p+1+n);
int res=0;ll x;
for(int i=1;i<=n;i++)
{
x=p[i].v;
for(int j=UP;j>=0;j--)
if(x&(1ll<<j))
{
if(!lib.a[j])break;
x^=lib.a[j];
}
if(x)res+=p[i].w,lib.insert(x);
}
printf("%d\n",res);
}
题意:博弈,有 n 堆石子,第一回合,每人选择 0 至 n-1 堆石子拿走,之后按niim游戏的规则进行:
两人轮流操作,每次可以选一堆石子拿走若干,可以全拿,不能不拿。无法取石子的人败。
询问保证先手取胜的时候,先手至少要拿走的石子总数。不能保证取胜,则输出 -1 。
解:
要使先手必胜,则第二回合开始时的石子数异或和不能为 0 ,
即,先手要在第一回合让后手无法使石子状态到达异或和为 0 。
贪心:将石子按数量从大到小排序,逐堆将石子插入线性基,当插入过程中,发现该堆石子已经会和线性基导致异或和为 0 时,则不插入,并将该堆石子数量计入答案。这样保证了后手无法再拿若干堆走石子使得最终石子异或和为 0 ,同时也保证了先手拿走的石子总量最小。
(乍一看怎么就是线性基了呢?
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define mkp(a,b) make_pair(a,b)
using namespace std;
typedef long long ll;
const int maxn=5e5+5;
const int inf=0x3f3f3f3f;
const int mod=998244353;
const int UP=31;
int a[maxn],p[UP+1];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
sort(a+1,a+1+n);
ll res=0;bool ju;
for(int i=n;i>=1;i--)
{
ju=false;
for(int j=UP,x=a[i];j>=0;j--)
{
if(!(x>>j))continue;
if(!p[j]){
p[j]=x;
ju=true;break;
}
x^=p[j];
}
if(!ju)res+=a[i];
}
printf("%lld\n",res);
}
6,XOR
题意:求长度为 \(n\) 的数组 \(a\) 中,异或和为 0 的子集大小的和。其中 \(1\leq n\leq 10^5,0\leq a_i\leq 10^{18}\) 。
官方题解+个人理解:
不要直接求异或和为 0 的集合大小,而是求每个元素在集合中的贡献。
先求出这个数组的线性基,会有 \(r\) 个元素插入线性基( \(r\) 至多不会超过61个)。
-
对于剩余的 \(n-r\) 个元素,根据线性基:线性基的元素能相互异或得到原集合的元素的所有相互异或得到的值 这一性质,每个元素可以和其他的 \(n-r-1\) 的元素任意组合 与线性基子集得异或和 0 ,即,对于剩余的 \(n-r\) 个元素,贡献都是 \(2^{n-r-1}\) 。
-
再对线性基里的 \(r\) 个元素进行讨论,对 \(n-r\) 个元素新构建一个线性基。
枚举 \(r\) 个元素,将其余 \(r-1\)个元素插入到 \((n-r)\) 的这个线性基中,再判断 当前枚举到的 \(r_i\) 能否与该线性基子集得异或和为 0,若能,同上一点一样,贡献为 \(2^{n-rr-1}\) 。
在此处 \(rr=r\) ,因为若 \(r_i\) 能与线性基子集得异或和为 0,则说明 \(r_i\) 已被线性基表示,即该线性基和 原大小为 \(n\) 的集合的线性基是等价的。
代码:
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int maxn=1e5+5;
const int mod=1e9+7;
ll a[maxn];
const int UP=62;
struct Lib{
ll p[UP+1]={0};
bool insert(ll x)
{
for(int i=UP;i>=0;i--){
if(!(x>>i))continue;
if(!p[i]){p[i]=x;return true;}
x^=p[i];
}
return false;
}
bool isin(ll x)
{
for(int i=UP;i>=0;i--){
if(!(x>>i))continue;
if(!p[i])return false;
x^=p[i];
}
return true;
}
};
ll p[maxn];
ll p2[maxn];
int main()
{
p2[0]=1;
for(int i=1;i<=100000;i++)p2[i]=p2[i-1]*2%mod;
int n;
while(~scanf("%d",&n))
{
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
Lib b,c;
int r=0,tr=0;
for(int i=1;i<=n;i++)
if(b.insert(a[i]))p[++r]=i;
else if(c.insert(a[i]))tr++;
if(r==n){
puts("0");continue;
}
ll res=(n-r)*p2[n-r-1]%mod;
for(int i=1;i<=r;i++)
{
Lib bb=c;int rr=tr;
for(int j=1;j<=r;j++)
if(i!=j&&bb.insert(a[p[j]]))rr++;
if(bb.isin(a[p[i]]))res=(res+p2[n-rr-1])%mod;
}
//其实最终只要 a[p[i]] 能被线性基表示 rr和r的值就会相同
printf("%lld\n",res);
}
}
题意:对于一个长度为 \(n\) 的序列 \(a\) ,令 \(S=\{x|1\leq x\leq n\}\) ,\(S\) 的幂集 \(2^S\) 定义为 \(S\) 所有子集构成的集合。定义映射 \(f:2^S\to Z,\;f(\emptyset)=0,\;f(T)=XOR\{A_t\},(t\in T)\) 。
现将 \(2^S\) 中每个集合的 \(f\) 值计算出来,从小到大排序,记为序列 \(B\) 。
给一个询问 \(x\) ,输出 \(x\) 在序列 \(B\) 中第 \(1\) 次出现时的下标。
解:
-
有结论:对于一个大小为 \(n\) 的可重集 \(A\),其线性基为 \(P\) , \(|P|=r\) ,那么,集合 \(A\) 内元素相互异或得到的可重异或集合 \(B\) 中,每个数次出现的个数都是 \(2^{n-r}\) 次。
证明:
对于没有插入到线性基的数,其个数为 \(n-r\) ,设其为可重集 \(C\),对于它的任意子集 \(C'\) ,对应异或和为 \(w_{c'}\),那么,根据性质:线性基中每个元素的异或方案唯一,即,线性基中不同的异或组合异或出的数是不一样的。 ,对于 \(\forall x\in B\) ,在线性基 \(P\) 中,都能找到唯一的 子集 \(P'\) ,使得其异或和 \(w_{p'}\) ,有 \(w_{p'}\,XOR\,w_{c'}=x\)。
因此,对于 \(\forall x\in B\) ,都能在 \(A\) 中找到 \(2^{n-r}\) 种方案,使得其异或值为 \(x\) 。
因此,对于题目,只需求在集合 \(B\) 中出小于 \(x\) 的不同的数(包括 0 )的个数 \(rk\) ,答案则为 \(2^{n-r}·rk+1\) 。
-
讨论怎么求 \(rk\) 。
利用性质:
- 线性基的元素能相互异或得到原集合的元素的所有相互异或得到的值。
- 线性基中每个元素的二进制最高位互不相同。
设线性基 \(P\) 中, \(p_i\) 对应着二进制最高位为 \(i\) 的值,设布尔值 \(bp_i=[p_i\ne 0]\) ;
设布尔值 \(bx_i\) 为 \(x\) 二进制位上对应的值。
那么有:\(\forall\,i\;bx_i=1\to bp_i=1\) ,\(\forall\,i\;bp_i=0\to bx_i=0\)
我们可以将 \(bp\) 和 \(bx\) 压缩:剥离所有 \(bp_i=bx_i=0\) 的二进制位,得到新的 \(x'\),那么 \(rk=x'\) ,即 \(rk\) 的值为 \(x'\) 的数值。
注意,为什么不是 \(x'-1\) ,因为题目有 \(f(\emptyset)=0\) ,即,集合 \(B\) 中一定存在 \(0\) 元素,需将 \(0\) 计数。
代码:
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int maxn=1e5+5;
const int mod=10086;
int a[maxn];
const int UP=31;
int p[UP+1];
ll p2=1;
void insert(int x)
{
for(int i=UP;i>=0;i--)
{
if(!(x>>i))continue;
if(!p[i]){
p[i]=x;return;
}
x^=p[i];
}
p2=(p2<<1)%mod;
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1,x;i<=n;i++){
scanf("%d",&x);
insert(x);
}
int x;
scanf("%d",&x);
int rk=0,t=1;
for(int i=0;i<=UP;i++)
if(p[i])
{
if((x>>i)&1)rk+=t;
t<<=1;
}
printf("%d\n",(rk*p2+1)%mod);
}
扩展
上面的线性基都是基于二进制数异或讨论的,这里基于向量的实数线性基进行讨论。
先列出一些线性代数的知识:
-
向量组 \(A\) :\(a_1,a_2,\dots,a_m\) 线性相关 \(\iff\) 齐次线性方程 \(x_1a_1+x_2a_2+\dots+x_ma_m=0\) (或记作 \(Ax=0\) )有非零解。
-
向量组 \(a_1,\dots,a_m\;(m\ge2)\) 线性相关的充分必要条件是存在某个向量 \(a_j\) 能由其余 \(m-1\) 个向量线性表示。
-
设向量组 \(A\) :\(a_1,a_2,\dots,a_m\) 线性无关,而向量组 \(a_1,\dots,a_m,b\) 线性相关,则向量 \(b\) 必能由向量组 \(A\) 线性表示,且表示式是惟一的。
-
最大无关组:
如果在向量组 \(A\) 中能选出 \(r\) 个向量 \(a_1,a_2,\dots,a_r\) ,满足:
- 向量组 \(A_0\) :\(a_1,a_2,\dots,a_r\) 线性无关;
- \(A\) 中任意 \(r+1\) 个向量都线性相关 (等价定义:\(A\) 中任意向量都能由 \(A_0\) 线性表示)
那么称 \(A_0\) 是 \(A\) 的以个最大无关组;最大无关组所含向量的个数 \(r\) 称为向量组 \(A\) 的秩,记作 \(R_A\) 。
根据上面的性质和定义,可以知道,对于向量组 \(A\) ,它的最大无关组即可用线性基求。
模板:
for(int i=1;i<=n;i++)// 向量组A有n个向量 每个向量大小为m
{
//线性基部分
for(int j=1;j<=m;j++)
{
if(fabs(a[i].b[j])<ep)continue;
if(!p[j])
{
p[j]=i;//将 a[i]插入
num++;sum+=a[i].w;
break;
}
// 要该位上的数(设为 x1)与 a[p[j]]向量(设对应位为x2) 消为 0,
// 则将a[p[j]]向量乘 x1/x2: 因为 x1-x2*(x1/x2)=x1-x1=0
d=a[i].b[j]/a[p[j]].b[j];
for(int k=1;k<=m;k++)
a[i].b[k]-=a[p[j]].b[k]*d;
}
}
如果向量组是整数的话,可以用逆元的思想来避免精度的误差:
for(int i=1;i<=n;i++)
{
//线性基部分
for(int j=1;j<=m;j++)// m 位
{
if(!a[i].b[j])continue;
if(!p[j])
{
p[j]=i;//将 a[i]插入
num++;sum+=a[i].w;
break;
}
// 要该位上的数(设为 x1)与 a[p[j]]向量(设对应位为x2) 消为 0,
// 则将a[p[j]]向量乘 x1/x2: 因为 x1-x2*(x1/x2)=x1-x1=0
d=1ll*a[i].b[j]*kpow(a[p[j]].b[j],mod-2)%mod;
for(int k=1;k<=m;k++)
a[i].b[k]=(1ll*a[i].b[k]-d*a[p[j]].b[k]%mod+mod)%mod;
}
}
例题:
题意:有 \(n\) 个大小为 \(m\) 的向量,每个向量对应一个权值 \(w\)。求最大无关组的向量个数,以及最大无关组的最小权值和(最大无关组可能有多个)。\(1\leq n,m\leq500,\,w\in Z,0\leq w \leq1000\) 。
解:
直接对向量按权值从小到大排序,再逐个插入线性基,插入线性基的向量个数即为最大无关组向量个数。
代码:
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef long long ll;
const int mod=998244353;
ll kpow(ll a,ll b)
{
ll ans=1;
while(b)
{
if(b&1)ans=ans*a%mod;
a=a*a%mod;
b>>=1;
}
return ans;
}
struct P{
int b[505], w;
bool operator<(const P&p)const{return w<p.w;}
}a[505];
int p[505];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)scanf("%d",&a[i].b[j]);
for(int i=1;i<=n;i++)scanf("%d",&a[i].w);
sort(a+1,a+1+n);
ll d;
int num=0,sum=0;
for(int i=1;i<=n;i++)
{
//线性基部分
for(int j=1;j<=m;j++)// m 位
{
if(!a[i].b[j])continue;
if(!p[j])
{
p[j]=i;//将 a[i]插入
num++;sum+=a[i].w;
break;
}
// 要该位上的数(设为 x1)与 a[p[j]]向量(设对应位为x2) 消为 0,
// 则将a[p[j]]向量乘 x1/x2: 因为 x1-x2*(x1/x2)=x1-x1=0
d=1ll*a[i].b[j]*kpow(a[p[j]].b[j],mod-2)%mod;
for(int k=1;k<=m;k++)
a[i].b[k]=(1ll*a[i].b[k]-d*a[p[j]].b[k]%mod+mod)%mod;
}
}
printf("%d %d\n",num,sum);
}