线性基 学习笔记
定义
线性空间:由向量集合 \(V\)、基域 \(\mathbb{P}\),组成,其中定义向量加法,标量加法,数乘(标量乘向量)。其中向量加法满足封闭性、结合律、单位元、逆元、交换律,标量加法满足封闭性,数乘满足封闭性、对向量加法与标量加法的分配律、结合律、单位元。比如普通的向量和实数就构成线性空间。
线性组合:向量 \(a_1,a_2,\cdots,a_n\in V\),\(\sum_{i=1}^n k_ia_i\) 为 \(a_1,a_2,\cdots,a_n\) 的一个线性组合,向量 \(\sum_{i=1}^n k_ia_i\) 能被 \(a_1,a_2,\cdots,a_n\) 线性表出。
线性相关、线性无关:如果零向量只有 \(k_1=k_2=\cdots=k_n=0\) 这一种被 \(a_1,a_2,\cdots,a_n\) 线性表出的方式,则 \(a_1,a_2,\cdots,a_n\) 线性无关,否则线性相关。
张成:一组向量能线性表出的所有向量组成的线性空间。
线性基:向量组张成空间的一个极大线性无关组,即再往里面加任何数都线性相关。
异或线性基
把每一个二进制数的看作一个位数维的向量,异或相当于模二意义下的加法,这样的线性基为异或线性基。
异或线性基解决的问题一般会从序列里挑任意个数异或起来。根据定义,线性基的异或能表示原序列异或的所有可能,而且线性基大小只有位数个,减少了复杂度。
插入
钦定线性基中每个数的最高有效位不同。
如果当前数插入后线性相关了,就不插,因为这个数在线性基中的组合能完全替代这个数的作用。否则需要插入。
从高位向低位遍历,如果当前位为一就把插入的数异或线性基的这一位元素,因为前面的已经使用过,后面的影响不到这一位。如果没有则插入这个数,此时插入的值与之前异或过的数构成了原来插入的数。如果最后为零说明插不进去。
bool insert(T k){
for(int i=maxn-1;i>=0;i--){
if(k>>i&1){
if(!a[i])return a[i]=k,1;
k^=a[i];
}
}
return 0;
}
异或最大值
从高位往低位枚举,若异或这个数能变大就贪心地异或。因为能变大说明是 \(0\operatorname{xor}1=1\),剩下的数不会影响这一位。
T query1(T ans=0){
for(int i=maxn-1;i>=0;i--)ans=max(ans,ans^a[i]);
return ans;
}
异或最小值
原序列能异或出的最小值为线性基中最小值,因为其他数的最高位更高,最小值异或其他数时这些数的最高位会被保留。
T query2(){
for(int i=0;i<maxn;i++)if(a[i])return a[i];
}
如果不能选空集,还要考虑有没有插入失败过。如果失败过,最小值为 \(0\)。
异或第 k 小
如果线性基每个元素最高位的一都是唯一的,即把线性基消元成行阶梯简化型矩阵,那么第 \(k\) 小就是 \(k\) 为一的位置上线性基的异或和,因为每个最高位的一都会被保留,异或的结果是按 \(a_0,a_1,a_0\operatorname{xor}a_1,a_2\cdots\) 的顺序排列的。同样要考虑 \(0\)。
void rebuild(){
for(int i=0;i<maxn;i++)for(int j=0;j<i;j++)if(a[i]>>j&1)a[i]^=a[j];
}
T kth(T k,T ans=0){
for(int i=0;i<maxn;i++){
if(a[i]){
if(k&1)ans^=a[i];
k>>=1;
}
}
return k?-1:ans;
}
另外在消元后,异或最大值即为线性基内所有元素的异或和。
线性基求并
直接把 \(B\) 内的元素插入到 \(A\) 里即可。
这也说明线性基是可合并的,可以用线段树维护。这就是 P4839。
template<typename T,int maxn>basis<T,maxn>merge(basis<T,maxn>a,basis<T,maxn>b){
for(int i=0;i<maxn;i++)a.insert(b.a[i]);
return a;
}
线性基求交
考虑对其中一个线性基重构,尽量让元素都在交里。
从小到大枚举 \(B\)。若这个元素能被 \(A\) 和 \(B\) 中更低位的元素表出,就把组合出这个元素在 \(A\) 中的部分加入答案。这相当于对 \(B\) 进行消元,异或上了其他 \(B\) 的元素以进入交空间。
如下,a 为 \(A\) 与 \(B\) 前缀的并的线性基, c 为 a 中元素的表示在 \(A\) 中的部分。
template<typename T,int maxn>basis<T,maxn>merge(basis<T,maxn>a,basis<T,maxn>b){
basis<T,maxn>c=a,ans;
for(int i=0;i<maxn;i++){
if(!b.a[i])continue;
T k=0,t=b.a[i];
for(int j=i;j>=0;j--){
if(t>>j&1){
if(a.a[j])k^=c.a[j],t^=a.a[j];
else{
c.a[j]=k,a.a[j]=t;
break;
}
}
}
if(!t)ans.a[i]=k,ans.cnt++;
}
return ans;
}
前缀线性基、带删线性基
线性基不可差分,所以要通过其他方法取出区间的线性基。对于每个元素,多记录元素插入的时间,让时间尽量靠后,此时 \(r\) 的前缀线性基中时间大于等于 \(l\) 的就是 \([l,r]\) 的线性基。
void build(int n,T b[]){
for(int i=1;i<=n;i++){
for(int j=0;j<maxn2;j++)a[i][j]=a[i-1][j],p[i][j]=p[i-1][j];
int t=i,k=b[i];
for(int j=maxn2-1;j>=0;j--){
if(k>>j&1){
if(!a[i][j]){
a[i][j]=k,p[i][j]=t;
break;
}
if(p[i][j]<t)swap(a[i][j],k),swap(p[i][j],t);
k^=a[i][j];
}
}
}
}
线性基的删除类似,将数据离线,让删除时间尽可能晚。
例题
结论:一个序列的线性基大小一定。
假如有若干个数线性相关,即 \(\operatorname{xor}_{i=1}^nd_i=0\)。那么其中只有最后一个元素插不进去。此时,改变插入顺序不会改变插入个数,因为 \(\operatorname{xor}_{i=1}^{n-1}d_i=d_n\) 说明剩下的 \(n-1\) 个数可以完全代替这个数的作用。
那么贪心地从大往小插入即可。
#include<bits/stdc++.h>
using namespace std;
int n;
long long a[63],ans;
pair<int,long long>t[1005];
bool insert(long long k){
for(int i=62;i>=0;i--){
if(k>>i&1){
if(!a[i])return a[i]=k,1;
k^=a[i];
}
}
return 0;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>t[i].second>>t[i].first;
sort(t+1,t+n+1);
for(int i=n;i>=1;i--)if(insert(t[i].second))ans+=t[i].first;
return cout<<ans<<'\n',0;
}
同理可以切掉 P4301,因为 Nim 游戏的必胜态是石子异或和非零,等价于取掉最少石子让剩下的线性无关。
可以看作一个序列能异或出几个不同的数。求出线性基,线性基线性无关且表示原序列异或的所有可能,所以答案为 \(2^{cnt}\),其中 \(cnt\) 为线性基大小。
int n,m,f;
long long ans=1,a[50];
char s[55];
void insert(long long k){
for(int i=49;i>=0;i--){
if(k>>i&1){
if(!a[i]){
a[i]=k;
return;
}
k^=a[i];
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%s",s);
long long t=0;
for(int j=0;j<n;j++)t=t*2+(s[j]=='O');
insert(t);
}
for(int i=0;i<n;i++)if(a[i])ans<<=1;
return printf("%d\n",ans%2008),0;
}
首先,这条路径由一条 \(1\rightsquigarrow n\) 的路径和一些环组成。其中路径和环的连接可以忽略,因为一来一回抵消了。那么把所有环丢进线性基,求与路径的最大异或和即可。
其中路径可以任意找,因为 \(1\rightsquigarrow n\) 的两条路径相交成若干个环,一条路径可以通过异或若干环变成其他所有路径。
找环时找 DFS 树的返祖边即可。显然这样不能找到所有环,但有结论:这样找出的环能表出原图的所有环。
假如有一个环,异或上出现的非树边对应的树上的简单环,此时还是一些简单环的异或和。但是此时只有树边,构不成环,所以树上简单环通过异或能表示原图所有环。
#include<bits/stdc++.h>
using namespace std;
int n,m;
long long dis[50005],a[63],w;
bool vis[50005];
vector<pair<int,long long>>e[50005];
bool insert(long long k){
for(int i=62;i>=0;i--){
if(k>>i&1){
if(!a[i])return a[i]=k,1;
k^=a[i];
}
}
return 0;
}
void dfs(int pos){
vis[pos]=1;
for(int i=0;i<e[pos].size();i++){
if(vis[e[pos][i].first])insert(dis[pos]^e[pos][i].second^dis[e[pos][i].first]);
else dis[e[pos][i].first]=dis[pos]^e[pos][i].second,dfs(e[pos][i].first);
}
}
int main(){
cin>>n>>m;
for(int i=1,u,v;i<=m;i++)cin>>u>>v>>w,e[u].push_back(make_pair(v,w)),e[v].push_back(make_pair(u,w));
dfs(1);
for(int i=62;i>=0;i--)dis[n]=max(dis[n],dis[n]^a[i]);
return cout<<dis[n]<<'\n',0;
}
CF845G双倍经验。
先想不考虑重复怎么做。假设把线性基消元成行阶梯简化型矩阵,那么可以倒推出这个数是被有一的位上的线性基元素表出的,因为其他元素不能提供这个一。也可由此推出排名。
这说明排名只与线性基对应位置是否为空有关,而消元前后空位是不变的。所以直接算就好了。
考虑重复,有一个很强的结论:每种数被表出的方案数都是 \(2^{n-cnt}\)。对于不在线性基中的数可以任选,而线性基有且仅有一种方案配对。知道这个结论可以做 CF895C和CF959F
#include<bits/stdc++.h>
using namespace std;
const int mod=10086;
int n,q,a[31],ans=1,cnt;
bool insert(int k){
for(int i=30;i>=0;i--){
if(k>>i&1){
if(!a[i])return a[i]=k,1;
k^=a[i];
}
}
return 0;
}
int main(){
cin>>n;
for(int i=1,t;i<=n;i++){
cin>>t;
if(!insert(t))ans=ans*2%mod;
}
cin>>q;
for(int i=0,p=1;i<=30;i++){
if(a[i]){
if(q>>i&1)cnt+=p;
p=p*2%mod;
}
}
return cout<<(ans*cnt+1)%mod<<'\n',0;
}
一个显然的做法是树剖+线段树+线性基。但这太逊了,一共四只 \(\log\)。
观察到线性基插入成功一次就少一个位,而线性基只有 \(30\) 个位,所以超过 \(30\) 个数就必然线性相关。用树状数组维护值,查询时直接暴力跳,把数扔掉线性基里判断即可。
#include<bits/stdc++.h>
using namespace std;
int n,m,a[100005],b[100005],dep[100005],fa[100005],sz[100005],son[100005],top[100005],dfn[100005],cnt;
vector<int>e[100005];
template<typename T,int maxn>struct BIT{
T tr[maxn];
void build(int n,T a[]){
for(int i=1;i<=n;i++){
tr[i]^=a[i];
if(i+(i&-i)<=n)tr[i+(i&-i)]^=tr[i];
}
}
void add(int x,T k){
for(;x<=n;x+=(x&-x))tr[x]^=k;
}
T query(int x,T ans=0){
for(;x;x-=(x&-x))ans^=tr[x];
return ans;
}
};
BIT<int,100005>t;
template<typename T,int maxn>struct basis{
T a[maxn];
bool insert(T k){
for(int i=maxn-1;i>=0;i--){
if(k>>i&1){
if(!a[i])return a[i]=k,1;
k^=a[i];
}
}
return 0;
}
};
basis<int,30>temp;
void dfs1(int pos,vector<int>e[]){
dep[pos]=dep[fa[pos]]+1,sz[pos]=1;
for(int i=0;i<e[pos].size();i++){
if(!dep[e[pos][i]]){
fa[e[pos][i]]=pos,dfs1(e[pos][i],e),sz[pos]+=sz[e[pos][i]];
if(sz[e[pos][i]]>sz[son[pos]])son[pos]=e[pos][i];
}
}
}
void dfs2(int pos,vector<int>e[]){
dfn[pos]=++cnt;
if(son[pos])top[son[pos]]=top[pos],dfs2(son[pos],e);
for(int i=0;i<e[pos].size();i++)if(!top[e[pos][i]])top[e[pos][i]]=e[pos][i],dfs2(e[pos][i],e);
}
void init(int s,vector<int>e[]){
dfs1(s,e),fa[s]=s,top[s]=s,dfs2(s,e);
}
void add(int u,int v,int k){
while(top[u]!=top[v]){
if(dep[top[u]]>dep[top[v]])t.add(dfn[top[u]],k),t.add(dfn[u]+1,k),u=fa[top[u]];
else t.add(dfn[top[v]],k),t.add(dfn[v]+1,k),v=fa[top[v]];
}
if(dep[u]>dep[v])swap(u,v);
t.add(dfn[u],k),t.add(dfn[v]+1,k);
}
bool query(int u,int v){
memset(temp.a,0,sizeof(temp));
if(dep[u]<dep[v])swap(u,v);
while(dep[u]!=dep[v]){
if(!temp.insert(t.query(dfn[u])))return 1;
u=fa[u];
}
while(u!=v){
if(!temp.insert(t.query(dfn[u]))||!temp.insert(t.query(dfn[v])))return 1;
u=fa[u],v=fa[v];
}
return !temp.insert(t.query(dfn[u]));
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>b[i];
for(int i=1,u,v;i<n;i++)cin>>u>>v,e[u].push_back(v),e[v].push_back(u);
init(1,e);
for(int i=1;i<=n;i++)a[dfn[i]]=b[i];
for(int i=n;i>=1;i--)a[i]^=a[i-1];
t.build(n,a);
for(int i=1,u,v,k;i<=m;i++){
string opt;
cin>>opt>>u>>v;
if(opt=="Update")cin>>k,add(u,v,k);
else puts(query(u,v)?"YES":"NO");
}
return 0;
}
线性基很难做到整体异或值。考虑差分,第一个操作变为单点修改。
设差分数组为 \(b\)。当查询 \([l,r]\),发现 \(a_i=a_l\operatorname{xor}\operatorname{xor}_{j=l+1}^i b_j\)。这说明 \(a_l,b_{[l+1,r]}\) 可以表出原数组,那么只需要维护 \(b\) 的基。还需要另一个树状数组维护 \(a\) 的区间修改和单点查询。
#include<bits/stdc++.h>
using namespace std;
int n,m,a[50005];
template<typename T,int maxn>struct BIT{
T tr[maxn];
void add(int x,T k){
for(;x<=n;x+=(x&-x))tr[x]^=k;
}
T query(int x,T ans=0){
for(;x;x-=(x&-x))ans^=tr[x];
return ans;
}
};
BIT<int,50005>t;
struct basis{
int a[31],cnt;
basis(){
cnt=0,memset(a,0,sizeof(a));
}
bool insert(int k){
if(cnt==31)return 0;
for(int i=30;i>=0;i--){
if(k>>i&1){
if(!a[i])return cnt++,a[i]=k,1;
k^=a[i];
}
}
return 0;
}
int query1(int ans){
for(int i=30;i>=0;i--)ans=max(ans,ans^a[i]);
return ans;
}
};
basis tr[200005];
basis merge(basis a,basis b){
for(int i=0;i<=30;i++)a.insert(b.a[i]);
return a;
}
void pushup(int pos){
tr[pos]=merge(tr[pos<<1],tr[pos<<1|1]);
}
void build(int pos,int nl,int nr,int a[]){
if(nl==nr)tr[pos].insert(a[nl]);
else{
int mid=(nl+nr)>>1;
build(pos<<1,nl,mid,a),build(pos<<1|1,mid+1,nr,a),pushup(pos);
}
}
void add(int pos,int nl,int nr,int g,int k){
if(nl==nr){
tr[pos]=basis(),tr[pos].insert(k);
return;
}
int mid=(nl+nr)>>1;
if(g<=mid)add(pos<<1,nl,mid,g,k);
if(g>mid)add(pos<<1|1,mid+1,nr,g,k);
pushup(pos);
}
basis query(int pos,int nl,int nr,int gl,int gr){
if(gl<=nl&&nr<=gr)return tr[pos];
int mid=(nl+nr)>>1;
basis ans;
if(gl<=mid)ans=merge(ans,query(pos<<1,nl,mid,gl,gr));
if(gr>mid)ans=merge(ans,query(pos<<1|1,mid+1,nr,gl,gr));
return ans;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=n;i>=1;i--)a[i]^=a[i-1],t.add(i,a[i]);
build(1,1,n,a);
for(int i=1,opt,l,r,k;i<=m;i++){
cin>>opt>>l>>r>>k;
if(opt==1){
t.add(l,k),add(1,1,n,l,a[l]^=k);
if(r<n)t.add(r+1,k),add(1,1,n,r+1,a[r+1]^=k);
}
else{
basis temp;
if(l<r)temp=query(1,1,n,l+1,r);
temp.insert(t.query(l)),cout<<temp.query1(k)<<'\n';
}
}
return 0;
}
能被 \(1,2,3,\cdots\) 表出等于能被这几个集合的交的线性基表出。所以可以做线性基前缀交,二分第一个不能被表出的位置。\(O(n\log^2V)-O(\log n\log V)\),大概 1e9,寄。
观察性质,答案位置上的线性基一定比上一个位置少了数,而线性基只有 \(\log V\) 的位置,有效位置也是 \(\log V\),在这里面二分,\(O(n\log^2V)-O(\log V\log\log V)\)。如果实现太烂会被卡。
更好的做法是取出每一位最后出现的元素建出一个新基,这使得元素尽量在前缀的交空间中,能够表示更多的数。答案为组合出 \(x\) 的元素的最早消失时间。
#include<bits/stdc++.h>
using namespace std;
char buf1[2097152],*ip1=buf1,*ip2=buf1;
inline int getc(){
return ip1==ip2&&(ip2=(ip1=buf1)+fread(buf1,1,2097152,stdin),ip1==ip2)?EOF:*ip1++;
}
template<typename T>void in(T &a)
{
T ans=0;
char c=getc();
for(;c<'0'||c>'9';c=getc());
for(;c>='0'&&c<='9';c=getc())ans=ans*10+c-'0';
a=ans;
}
template<typename T,typename... Args>void in(T &a,Args&...args)
{
in(a),in(args...);
}
int n,q,p[70],cnt;
unsigned long long x;
struct basis{
unsigned long long a[64];
int cnt;
basis(){
memset(a,0,sizeof(a)),cnt=0;
}
bool insert(unsigned long long k,bool f){
if(cnt==64)return 0;
for(int i=63;i>=0;i--){
if(k>>i&1){
if(!a[i]){
if(f)a[i]=k,cnt++;
return 1;
}
k^=a[i];
}
}
return 0;
}
};
basis t[100005],c,ans;
basis merge(basis a,basis b){
c=a,ans=basis();
for(int i=0;i<64;i++){
if(!b.a[i])continue;
unsigned long long k=0,t=b.a[i];
for(int j=i;j>=0;j--){
if(t>>j&1){
if(a.a[j])k^=c.a[j],t^=a.a[j];
else{
c.a[j]=k,a.a[j]=t;
break;
}
}
}
if(!t)ans.a[i]=k,ans.cnt++;
}
return ans;
}
int main(){
in(n,q);
for(int i=1,k;i<=n;i++){
in(k);
for(int j=1;j<=k;j++)in(x),t[i].insert(x,1);
}
for(int i=2;i<=n;i++)t[i]=merge(t[i],t[i-1]);
for(int i=0;i<64;i++)for(p[i]=1;t[p[i]].a[i];p[i]++);
while(q--){
in(x);
int ans=n+1;
for(int i=63;i>=0;i--)if(x>>i&1)ans=min(ans,p[i]),x^=t[p[i]-1].a[i];
cout<<ans<<'\n';
}
return 0;
}
根据 P4151的经验,可以将初始的环扔进线性基,加边时在线性基中加入 \(dis_x\operatorname{xor}dis_y\operatorname{xor}z\),离线后使用带删线性基。修改等于先删除后插入。
`
#include<bits/stdc++.h>
using namespace std;
int n,m,q,cnt1,cnt2,id[1005],p[1005];
struct query{
int u,v,t;
bitset<1000>w;
}a[1005];
bool vis[505];
bitset<1000>dis[505],w;
struct basis{
bitset<1000>a[1000];
int p[1000];
bool insert(bitset<1000>k,int t){
for(int i=999;i>=0;i--){
if(k[i]){
if(t>p[i])swap(k,a[i]),swap(t,p[i]);
if(!t)return 1;
k^=a[i];
}
}
return 0;
}
bitset<1000>query1(int t){
bitset<1000>ans;
for(int i=999;i>=0;i--)if(p[i]>t&&!ans[i])ans^=a[i];
return ans;
}
};
basis t;
vector<pair<int,bitset<1000>>>e[505];
void dfs(int pos){
vis[pos]=1;
for(int i=0;i<e[pos].size();i++){
if(vis[e[pos][i].first])t.insert(dis[pos]^e[pos][i].second^dis[e[pos][i].first],q+1);
else dis[e[pos][i].first]=dis[pos]^e[pos][i].second,dfs(e[pos][i].first);
}
}
int main(){
cin>>n>>m>>q,cnt2=q+1;
for(int i=1,u,v;i<=m;i++)cin>>u>>v>>w,e[u].push_back(make_pair(v,w)),e[v].push_back(make_pair(u,w));
dfs(1);
for(int i=1,x,y;i<=q;i++){
string opt;
cin>>opt;
if(opt=="Add")cin>>x>>y>>w,id[i]=++cnt1,a[cnt1]=query{x,y,q+1,w},p[cnt1]=cnt1;
else if(opt=="Cancel")cin>>x,a[p[x]].t=i;
else cin>>x>>w,a[p[x]].t=i,id[i]=--cnt2,a[cnt2]=query{a[p[x]].u,a[p[x]].v,q+1,w},p[x]=cnt2;
}
for(int i=0;i<=q;i++){
if(id[i])t.insert(dis[a[id[i]].u]^a[id[i]].w^dis[a[id[i]].v],a[id[i]].t);
bitset<1000>ans=t.query1(i);
int j=999;
while(!ans[j])j--;
for(;j>=0;j--)putchar(ans[j]?'1':'0');
putchar('\n');
}
return 0;
}
[[数学]]

浙公网安备 33010602011771号