Bootstrap

线性基学习笔记

线性基学习笔记

定义

线性空间 V V V 内的一个极大线性无关组是 V V V 的一组 hamel 基线性基,简称

以上内容是 OI WIKI 中提及的定义。

更具体一点来说,对于一个向量组 v v v,如果满足对于任意的取值,使 ∑ i = 1 n α i v i ≠ 0 \sum_{i=1}^n\alpha_iv_i\ne0 i=1nαivi=0 α \alpha α 是常数),即不回到原点,那么称这个向量组是线性无关的。而一个极大线性无关组,对于一个 n n n 维空间来说,通常有 n n n 个元素,并且再加入任意一个元素都会使向量组 v v v 线性相关,即可以回到原点。对于任意的 α \alpha α ∑ i = 1 n α i v i \sum_{i=1}^n\alpha_iv_i i=1nαivi 一定可以表示出整个空间内的任何一个点。

这些都是从几何层面上考虑。一个线性基说明我们可以用一种根简单更直观的方式来表示所有原集合中的内容。在 C++ 中包括实数线性基和异或线性基,而最常出现的是异或线性基。

异或线性基

和普通线性基一样的,异或线性基相当于将加法运算代替为异或运算,所以由定义可知异或线性基内的任意元素的异或值均不为 0 0 0

异或线性基通常被用来求解集合内的异或 k k k 大值。

操作

添加元素

线性基的构造有两种方法,一种是动态的贪心构造,一种是静态的高斯消元构造。

贪心
void insert(ull x){
	for(int i=50;~i;i--){
		if(!(x>>i))continue;
		if(!d[i]){
			d[i]=x;
			break;
		}
		x^=d[i];
	}
	return ;
}
高斯消元
void gauss(){
	int j,k=1;
	for(int i=50;~i;i--){
		for(j=k;j<=n;j++){
			if(d[j]&(1ll<<i))break;
		}
		if(j>n)continue;
		swap(d[j],d[k]);
		for(j=1;j<=n;j++){
			if(j!=k&&d[j]&(1ll<<i))d[j]^=d[k];
		}
		k++;
	}
	k--;
	return ;
}

两种方法的复杂度都是 O ( n log ⁡ a ) O(n\log a) O(nloga),其中 a a a 表示值域。高斯消元的常数更大,并且只适用于静态,因此不常选用。

这样构造出的线性基, d i d_i di 所表示的线性基的第 i i i 位一定是 1 1 1,第 i i i 位以前的一定是 0 0 0

查询异或最大值

当然这个问题也适用于下面的异或 k k k 小值问题。

考虑到如果要让异或值最大,那么高位一定要大。又考虑到 d i d_i di 的第 i i i 位一定是 1 1 1,那么我们可以贪心,如果高位能够让结果变大,那么就选择异或。

code
long long MaxXor(){
	long long res=0;
	for(int i=62;i>=0;i--)res=max(res,res^d[i]);
	return res;
}
例题【P3505. [luogu3812]线性基模板】

没什么好说的,纯纯模板题。

code
#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long

int n;
ull ans;
ull p[64];

void insert(ull x){
	for(int i=50;~i;i--){
		if(!(x>>i))continue;
		if(!p[i]){
			p[i]=x;
			break;
		}
		x^=p[i];
	}
}

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		ull a;
		scanf("%llu",&a);
		insert(a);
	}
	for(int i=50;~i;i--)ans=max(ans,ans^p[i]);
	printf("%llu",ans);
	return 0;
}

查询异或最小值

这个问题也可以类比到异或 k k k 小值。

考虑和刚才一样的思路,但是我们发现,如果有元素插入失败,则说明异或值可以为 0 0 0,但由于线性基的定义导致了线性基内元素的异或值不可能为 0 0 0,因此需要特判,反之,输出有确切值的最小的线性基。

code
int MinXor(){
	if(tot<n)return 0;
	for(int i=0;i<=62;i++){
		if(d[i])return d[i];
	}
}

查询异或 k k k 小值

和刚才一样考虑,我们发现,可以让对应二进制位的线性基异或,即如果 k k k 的二进制的第 i i i 位为 1 1 1,那么就让结果异或第 k k k 大的有值的线性基。但如果这个样子,可能会因为之前的某一位上已经有了 1 1 1 而造成结果减小。为了保证这种情况不发生,我们应该保证如果第 i i i 个线性基有值,那么第 i i i 位有且仅有这一个线性基为 1 1 1。于是我们考虑像高斯消元一样重构。接着重复刚才的操作即可(注意特判 0 0 0)。

code
void Rebuild(){
    for(int i=0;i<=62;i++){
        for(int j=i-1;j>=0;j--)d[i]=min(d[i],d[i]^d[j]);
    }
    return ;
}
例题【P575. [bzoj2844]albus就是要第一个出场】

先考虑问题在不可重集下的的情况,那么为了知道某个数的第一次出现的位置,我们可以二分出一个位置,如果过大则向前,过小则向后。现在考虑问题在可重集下的情况。如果线性基内有 ∣ S ∣ |S| S 个元素,那么说明剩下的 n − ∣ S ∣ n-|S| nS 个元素一定都与线性基内的某些元素异或值为 0 0 0。不妨将外面的元素均看为 0 0 0,如此一来,不管外面的元素选择什么,得到的异或值都不变 ,因此每个元素都会重复 2 n − ∣ S ∣ 2^{n-|S|} 2nS 次,并且,因为可以选择空集,所以异或值的集合里一定包含了 0 0 0。最后二分出答案算出该组元素的第一个的下标即可。

code
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int M=10086;
int n,x,Q,tot;
int d[32],arr[32];

void ins(int x){
    for(int i=30;i>=0;i--){
        if(((x>>i)&1)==0)continue;
        if(!d[i]){
            d[i]=x;
            break;
        }
        x^=d[i];
    }
    return ;
}

void rebuild(){
    for(int i=0;i<=30;i++){
        for(int j=i-1;j>=0;j--){
            if((d[i]>>j)&1)d[i]^=d[j];
        }
    }
    for(int i=0;i<=30;i++){
        if(d[i])arr[tot++]=i;
    }
    return ;
}

int kth(int k){
    int res=0;
    for(int i=tot-1;i>=0;i--){
        if((k>>i)&1)res^=d[arr[i]];
    }
    return res;
}

ll QuickPow(ll a,int b){
    ll res=1;
    while(b){
        if(b&1)res=res*a%M;
        a=a*a%M;
        b>>=1;
    }
    return res;
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&x);
        ins(x);
    }
    rebuild();
    scanf("%d",&Q);
    ll l=0,r=(1<<tot)-1,ans=0;
    while(l<=r){
        ll mid=(l+r)>>1;
        if(kth(mid)>=Q){
            ans=mid;
            r=mid-1;
        }else l=mid+1;
    }
    printf("%lld",(ans%M*QuickPow(2,n-tot)%M+1)%M);
    return 0;
}

线性基合并

是的,线性基可以合并,只需要将一个线性基内的元素插入到另一个线性基内即可,时间复杂度是 O ( log ⁡ 2 a ) O(\log^2 a) O(log2a)

void merge(LineBasis x,LineBasis y){
    for(int i=62;i>=0;i--)x.ins(y.d[i]);
    return ;
}

区间异或最大值

可以类比为区间异或 k k k 大值,本质上只需要求出区间的线性基即可。

考虑和 RMQ 相同的思路,因为线性基内插入相同元素会插入失败,从而不影响结果,因此可以通过倍增求解连续 2 k 2^k 2k 个元素的线性基,然后利用线性基合并,将整个区间有重的合并在一起。然后求解即可。

例题【P580. [bzoj4568] [Scoi2016]幸运数字】

考虑到求路径上选点权使异或值最大,考虑线性基,只需要得到路径上的所有点权并插入线性基即可,时间复杂度是 O ( q log ⁡ n + q n ) O(q\log n+qn) O(qlogn+qn) 的。考虑到线性基可以合并,那么我们可以利用倍增思想,将自己到自己的第 2 j 2^j 2j 个祖先(不包括第 2 j 2^j 2j 个祖先)的所有点权存储在线性基里面,在求解 LCA 的时候可以一并合并,这样的时间复杂度是 O ( n log ⁡ n log ⁡ 2 a + q log ⁡ n log ⁡ 2 a ) O(n\log n\log^2 a+q\log n\log^2 a) O(nlognlog2a+qlognlog2a),有一说一似乎比暴力更差。接着可以考虑和区间异或最大值的方法,用两个可重区间将整个区间覆盖,具体方式在于将两个节点到 LCA 的部分分开处理,每个部分相当于一个区间异或最大值,求解即可,复杂度能去掉一个 log ⁡ \log log,是 O ( n log ⁡ n log ⁡ 2 a + q log ⁡ n + q log ⁡ 2 a ) O(n\log n\log^2 a+q\log n+q\log^2 a) O(nlognlog2a+qlogn+qlog2a) 的。

code
#include <bits/stdc++.h>
using namespace std;

const int N=2e4;
int n,m;
long long a[N+5];
struct Graph{
    int cnt;
    int head[N+5],v[(N<<1)+5],to[(N<<1)+5];
    void AddEdge(int x,int y){
        v[cnt]=y;
        to[cnt]=head[x];
        head[x]=cnt++;
        return ;
    }
    void Build(){
        cnt=1;
        int x,y;
        for(int i=1;i<n;i++){
            scanf("%d%d",&x,&y);
            AddEdge(x,y);
            AddEdge(y,x);
        }
        return ;
    }
}G;
struct LineBasis{
    long long d[64];
    void clear(){
        memset(d,0,sizeof d);
        return ;
    }
    void AddNumber(long long x){
        for(int i=60;~i;i--){
            if(((x>>i)&1)==1){
                if(d[i]==0){
                    d[i]=x;
                    break;
                }
                x^=d[i];
            }
        }
        return ;
    }
    void AddLB(LineBasis x){
        for(int i=60;~i;i--)AddNumber(x.d[i]);
        return ;
    }
    void show(){
        for(int i=60;i>=0;i--)printf("%lld ",d[i]);
        printf("\n");
        return ;
    }
};

int Log(int x){
    int res=0;
    while(x){
        res++;
        x>>=1;
    }
    return res-1;
}

struct LCA{
    int l;
    int dep[N+5],faz[N+5][20];
    LineBasis DisXor[N+5][20];
    void Dfs(int x,int fa){
        for(int i=1;i<=l;i++){
            faz[x][i]=faz[faz[x][i-1]][i-1];
            DisXor[x][i].AddLB(DisXor[x][i-1]);
            DisXor[x][i].AddLB(DisXor[faz[x][i-1]][i-1]);
        }
        for(int i=G.head[x];i;i=G.to[i]){
            int y=G.v[i];
            if(y==fa)continue;
            faz[y][0]=x;
            dep[y]=dep[x]+1;
            DisXor[y][0].AddNumber(a[y]);
            Dfs(y,x);
        }
        return ;
    }
    int kth(int x,int k){
        for(int i=l;i>=0;i--){
            if(((k>>i)&1)==0)continue;
            x=faz[x][i];
        }
        return x;
    }
    int lca(int x,int y){
        if(dep[x]<dep[y])swap(x,y);
        x=kth(x,dep[x]-dep[y]);
        if(x==y)return y;
        for(int i=l;i>=0;i--){
            if(faz[x][i]!=faz[y][i]){
                x=faz[x][i];
                y=faz[y][i];
            }
        }
        return faz[y][0];
    }
    void Build(){
        l=Log(n);
        Dfs(1,0);
        return ;
    }
}Lca;
struct Query{
    void Build(){
        int x,y;
        long long ans;
        LineBasis res;
        for(int i=1;i<=m;i++){
            ans=0;
            res.clear();
            scanf("%d%d",&x,&y);
            int lca=Lca.lca(x,y);
            res.AddNumber(a[lca]);
            int Len=Log(Lca.dep[x]-Lca.dep[lca]+1);
            res.AddLB(Lca.DisXor[x][Len]);
            res.AddLB(Lca.DisXor[Lca.kth(x,Lca.dep[x]-Lca.dep[lca]+1-(1<<Len))][Len]);
            Len=Log(Lca.dep[y]-Lca.dep[lca]+1);
            res.AddLB(Lca.DisXor[y][Len]);
            res.AddLB(Lca.DisXor[Lca.kth(y,Lca.dep[y]-Lca.dep[lca]+1-(1<<Len))][Len]);
            for(int i=60;~i;i--)ans=max(ans,ans^res.d[i]);
            printf("%lld\n",ans);
        }
        return ;
    }
}Q;

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
    G.Build();
    Lca.Build();
    Q.Build();
    return 0;
}

奇怪性质

个数固定

正如定义里所说,一个极大线性无关组的大小通常是固定的。所以不管我们怎么插入,这个线性基内元素的个数总是固定的。可以理解为 a 1 ⊕ a 2 = a 3 ⇒ a 1 ⊕ a 3 = a 2 a_1\oplus a_2=a_3\Rightarrow a_1\oplus a_3=a_2 a1a2=a3a1a3=a2

例题【P574. [bzoj2460] [BeiJing2011]元素】

因为线性基内元素个数是固定的,因此选择贪心,将权值大的尽可能靠前插入,这样得到的权值一定最大。

code
#include <bits/stdc++.h>
using namespace std;

int n,ans;
long long p[64];
struct stone{
	int m;
	long long x;
	friend bool operator <(const stone &a,const stone &b);
}a[1005];

bool operator <(const stone &a,const stone &b){
	return a.m>b.m;
}

bool ins(long long x){
	for(int i=62;~i;i--){
		if(!(x>>i))continue;
		if(!p[i]){
			p[i]=x;
			return 1;
		}
		x^=p[i];
	}
	return 0;
}

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld%d",&a[i].x,&a[i].m);
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++){
		if(ins(a[i].x))ans+=a[i].m;
	}
	printf("%d",ans);
    return 0;
}

练习题

P577. [bzoj3569]DZY Loves Chinese II

这道题需要先想一个奇怪的构造,之后的代码就简单了。首先在图里跑一个 DFS 树,将所有的非树边随机赋一个权值,接着将这条非树边在树上的两个端点之间的简单路径上的所有边的权值均异或上这条非树边的权值。当输入的 k k k 条边存在一些边的边权异或值为 0 0 0,那么这个图不联通。

可以想象成每一个环都拥有不同的权值,当一个环内有两条边被同时去掉,那么这个环便不联通,所以整张图便不联通。但是如果一条边在多个环里,那么只要有一个环只去除了它一条边,那么整张图也是联通的,因为所有的不联通的环的两个部分都分别交于这条边的两个端点,所以它们联通。

这应该是哈希在线性基中的体现。

code
#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long

const int N=1e5,M=5e5;
int n,m,q,k,cnt,x,ans,tot;
int head[N+5],u[M+5],v[M+5];
ull d[64],w[M+5],a[N+5];
bool vis[N+5],inT[M+5];
mt19937_64 rnd(20100511);
struct edge{
    int to,v,id;
}e[(M<<1)+5];

void AddEdge(int x,int y,int z){
    e[cnt].v=y;
    e[cnt].to=head[x];
    e[cnt].id=z;
    head[x]=cnt++;
    return ;
}

void Dfs(int x){
    vis[x]=true;
    for(int i=head[x];i;i=e[i].to){
        int y=e[i].v,z=e[i].id;
        if(vis[y])continue;
        inT[z]=true;
        Dfs(y);
    }
    return ;
}

void Get(int x,int faz){
    for(int i=head[x];i;i=e[i].to){
        int y=e[i].v,z=e[i].id;
        if(y==faz||!inT[z])continue;
        Get(y,x);
        w[z]=a[y];
        a[x]^=a[y];
    }
    return ;
}

void Insert(int x){
    for(int i=63;i>=0;i--){
        if(((x>>i)&1)==0)continue;
        if(d[i]==0){
            tot++;
            d[i]=x;
            break;
        }
        x^=d[i];
    }
    return ;
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%d%d",&u[i],&v[i]);
        AddEdge(u[i],v[i],i);
        AddEdge(v[i],u[i],i);
    }
    Dfs(1);
    for(int i=1;i<=m;i++){
        if(!inT[i]){
            w[i]=rnd();
            a[u[i]]^=w[i];
            a[v[i]]^=w[i];
        }
    }
    Get(1,0);
    scanf("%d",&q);
    while(q--){
        tot=0;
        for(int i=63;i>=0;i--)d[i]=0;
        scanf("%d",&k);
        for(int i=1;i<=k;i++){
            scanf("%d",&x);
            Insert(w[x^ans]);
        }
        if(tot<k)printf("Disconnected\n");
        else{
            ans++;
            printf("Connected\n");
        }
    }
    return 0;
}

P578. [bzoj4184] shallot

这道题需要用到线性基删除,但事实上,我翻遍了全网,也没有找到任何一个适用于本题的直接删除手段。因此我们考虑,对于每一个元素,都可以分配一个出现的时间和消失的时间,那么这就可以用到线段树分治,而我们只需要在向下遍历的时候将新元素加入线性基即可。

code
#include <bits/stdc++.h>
using namespace std;
#define lid id<<1
#define rid id<<1|1

const int N=5e5;
int n;
int a[N+5];
set<pair<int,int> >pos;
set<pair<int,int> >::iterator it;
stack<int>s;
vector<int>T[N<<2];
struct LineBasis{
    int d[31];
    void Insert(int x){
        for(int i=30;~i;i--){
            if(((x>>i)&1)==1){
                if(d[i]==0){
                    d[i]=x;
                    break;
                }
                x^=d[i];
            }
        }
        return ;
    }
}Lb;

void Build(int id,int l,int r){
    if(l==r)return ;
    int mid=(l+r)>>1;
    Build(lid,l,mid);
    Build(rid,mid+1,r);
    return ;
}

void Insert(int id,int l,int r,int p,int q,int x){
    if(p<=l&&r<=q){
        T[id].push_back(x);
        return ;
    }
    int mid=(l+r)>>1;
    if(p<=mid)Insert(lid,l,mid,p,q,x);
    if(q>mid)Insert(rid,mid+1,r,p,q,x);
    return ;
}

void Dfs(int id,int l,int r,LineBasis x){
    for(int i=0;i<(int)T[id].size();i++)x.Insert(T[id][i]);
    if(l==r){
        int ans=0;
        for(int i=30;~i;i--)ans=max(ans,ans^x.d[i]);
        printf("%d\n",ans);
    }else{
        int mid=(l+r)>>1;
        Dfs(lid,l,mid,x);
        Dfs(rid,mid+1,r,x);
    }
    return ;
}

int main(){
    scanf("%d",&n);
    Build(1,1,n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        if(a[i]>0)pos.insert(make_pair(a[i],i));
        else{
            it=pos.lower_bound(make_pair(-a[i],0));
            Insert(1,1,n,(*it).second,i-1,-a[i]);
            pos.erase(it);
        }
    }
    for(it=pos.begin();it!=pos.end();it++)Insert(1,1,n,(*it).second,n,(*it).first);
    Dfs(1,1,n,Lb);
    return 0;
}
;