编程语言
首页 > 编程语言> > 「算法笔记」线性基

「算法笔记」线性基

作者:互联网

一、定义

线性基是向量空间的一组基,通常可以解决有关异或的一些题目。

通俗一点的讲法就是由一个集合构造出来的另一个集合,它的性质如下:

每个序列都拥有至少一个线性基。线性基中的第 \(i\) 个数在二进制下最高位 \(1\) 的位置为 \(i\)。

二、线性基的操作

1. 插入与判断

具体来说,就是向一个集合中插入一个元素,同时维护这个集合的线性基。

令插入的数为 \(x\)。将 \(x\) 转为二进制。

设 \(x\) 在二进制下的最高位 \(1\) 的位置为 \(i\)。

若结束时 \(x=0\),则原来的线性基中已经可以表示出原先的 \(x\) 了;反之,则说明此时往线性基中加入了一个新元素,此时也能表示 \(x\) 了。

void insert(int x){
    for(int i=N-1;i>=0;i--){    //从高位向低位扫
        if(((x>>i)&1)==0) continue;
        if(!a[i]){a[i]=x;break;}     //线性基的第 i 个数不存在 
        else x^=a[i];
    }
} 

判断一个数是否可以被线性基中的数异或得到:用上面插入的方法判断,若结束时 \(x=0\),则能表示;反之,不能表示。

2. 合并

两个线性基是可以暴力合并的。

对于集合 \(A,B\),把 \(B\) 线性基中的元素依次插入到 \(A\) 的线性基中即可得到 \(A\cup B\) 的线性基。

三、线性基的应用

1. 查询异或最小值

注意这里的最小值,指的是线性基中取若干个数异或可以得到的数的最小值。

线性基中的元素最高位 \(1\) 的位置都不同。考虑线性基中最小的数,它异或上其他数显然会变大。所以答案就是线性基中所有元素中最小的那个。

而若是查询一个集合中取若干个数(而不是这个集合的线性基),使得它们的异或和最小,就要再看看最小值是否有可能为 \(0\),即判断在插入集合元素的过程中,是否存在结束时 \(x=0\) 的情况(说明原来的线性基中已经可以表示出原先的 \(x\) 了)。

2. 查询异或最大值

具体来说,就是查询一个集合中取若干个数,使得它们的异或和最大。

先构造出这个集合的线性基。

考虑贪心,从高到低位扫,由于若当前扫到第 \(i\) 个数,意味着可以保证答案的第 \(i\) 位为 \(1\),且后面没有机会改变第 \(i\) 位,所以若异或上当前扫到的 \(a_i\) 会使答案变大,就把答案异或上 \(a_i\)。其中 \(a_i\) 为线性基中的第 \(i\) 个数。

具体地,若此时的答案异或上 \(a_i\) 能使答案变大(其实就是在二进制下 \(a_i\) 的第 \(i\) 位为 \(1\) 且答案的第 \(i\) 位为 \(0\) 时),则将答案异或上 \(a_i\)。扫完线性基之后得到的答案一定就是集合中的数可以通过异或表示出来的最大值。

int query(){    //查询异或最大值
    int ans=0;
    for(int i=N-1;i>=0;i--)
        ans=max(ans,ans^a[i]);
    return ans;
}

3. 查询异或第 k 小

给出一个集合,求其第 \(k\) 小的子集异或和。

Part 1. 首先,求出这个集合的线性基 \(a\),选择线性基的一个 非空子集 共有 \(2^{|a|}-1\) 种方案(能通过异或表示出 \(2^{|a|}-1\) 个数)。如果 \(|a|<n\),则说明至少有一个没有被插入到线性基中的数可以被线性基中的数表示出来,选择线性基中的一些数与这个数,可以得到其异或和为 \(0\),这样有 \(2^{|a|}\) 种方案。

然后,考虑给出线性基,求选择若干数可以组成的第 \(k\) 小的数(由于线性基没有异或和为 \(0\) 的子集,所以要特殊考虑 \(0\),若能异或表示出 \(0\),那么 \(0\) 肯定是最小值,则要把查询的 \(k\) 减 \(1\))。

Part 2. 将 \(k\) 表示为一个长度为 \(|a|\) 的二进制数(若不足,可在高位补 \(0\))。\(k\) 的二进制排列符合以下性质:

线性基的 \(|a|\) 个元素控制了异或后结果的 \(|a|\) 个二进制位,而二进制数的规律恰好与从线性基中选数的两条规律 相对应

于是就可以:枚举 \(k\) 所有为 \(1\) 的二进制位,如果第 \(i\) 位为 \(1\),则将线性基中控制的第 \(i\) 小的二进制位的元素异或到答案中。

//HDU 3949 
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=60;
int t,n,q,x,k,a[N],b[N],cnt,tot;
bool flag;
void insert(int x){
    for(int i=N-1;i>=0;i--){
        if(((x>>i)&1)==0) continue;
        if(!a[i]){a[i]=x;break;} 
        else x^=a[i];
    }
    if(!x) flag=1;    //flag: 标记是否存在异或和为 0 的情况 
} 
int query(int k){
    if(k>(1ll<<cnt)-1) return -1;
    int ans=0;
    for(int i=0;i<cnt;i++)
        if((k>>i)&1) ans^=b[i];
    return ans;
}
signed main(){
    scanf("%lld",&t);
    while(t--){
        scanf("%lld",&n),flag=0,cnt=0,fill(a,a+N,0);
        for(int i=1;i<=n;i++)
            scanf("%lld",&x),insert(x);
        for(int i=N-1;i>=0;i--)
            for(int j=i-1;j>=0;j--)
                if((a[i]>>j)&1) a[i]^=a[j];    //重构线性基,将每一位都独立,使每一位的选择都不会影响下一位。相当于线性基中的元素与其它元素异或,得到的仍满足线性基的性质。此时线性基中任一元素都要满足:最高位的 1 在线性基中只出现一次。 
        for(int i=0;i<N;i++)
            if(a[i]) b[cnt++]=a[i];    //b[i] 表示线性基中控制第 i 小的二进制位的元素 
        scanf("%lld",&q),printf("Case #%lld:\n",++tot);
        while(q--){
            scanf("%lld",&k),k-=flag;
            printf("%lld\n",query(k));
        }
    }
    return 0; 
}

4. 求子集异或值排名

给出一个集合,以及一个数 \(x\)。这个集合的所有子集(可以为空)的异或值从小到大排序得到序列 \(\{b_i\}\),求 \(x\) 在 \(\{b_i\}\) 中第一次出现的下标。

首先,求出这个集合的线性基 \(a\)。

考虑线性基所控制的某个二进制位,如果 \(x\) 的这一位为 \(1\),那么线性基中控制这一位的元素一定被选择,这样可以求出 \(x\) 在去重后的 \(\{b_i\}\) 中第一次出现的下标。

之后,计算每个重复的数字出现了多少次。设给定集合中不在线性基中的数的集合为 \(S\),显然 \(|S|=n-|a|\)。考虑它的一个子集 \(S'\)(可以为空),\(S'\) 的异或和一定可以 唯一表示 为 \(S\) 中若干个数的异或和,将它们都异或起来,就可以的到 \(0\)。那么就有 \(2^{n-|a|}\) 中方案得到 \(0\)。所以,对于每一个 \(b_i\),它的出现次数至少为 \(2^{n-|a|}\)。接着证明它的上界,假设在 \(S\) 中任意选,最终都可以凑出这个数,而选择 \(a\) 中的数的方案一定是唯一的,即上界也为 \(2^{n-|a|}\)。

求出线性基中子集异或和小于 \(x\) 的子集个数 \(cnt\),答案为 \(cnt \times 2^{n-|a|}+1\)。

//BZOJ 2844
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=60,mod=10086;
int n,x,k,a[N],b[N],cnt,ans;
int mul(int x,int n,int mod){
    int ans=mod!=1;
    for(x%=mod;n;n>>=1,x=x*x%mod)
        if(n&1) ans=ans*x%mod;
    return ans;
}
void insert(int x){
    for(int i=N-1;i>=0;i--){
        if(((x>>i)&1)==0) continue;
        if(!a[i]){a[i]=x;break;} 
        else x^=a[i];
    }
} 
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
        scanf("%lld",&x),insert(x);
    scanf("%lld",&k);
    for(int i=0;i<N;i++)
        if(a[i]) b[cnt++]=i;    //转化后显然对最高位的 1 不影响,实现时可以不写“查询异或第 k 小”中「重构线性基使最高位的 1 在线性基中只出现一次」的部分。 
    for(int i=0;i<cnt;i++)
        if((k>>b[i])&1) ans+=(1ll<<i),ans%=mod;    //ans: k 在去重后的「所有子集(可以为空)的异或值从小到大排序得到序列」中的排名 
    printf("%lld\n",(ans%mod*mul(2,n-cnt,mod)%mod+1)%mod);
    return 0; 
}

四、例题

1. Luogu P3857 彩灯

题目大意:有 \(n\) 个彩灯,并且有 \(m\) 个开关控制它们。当一个开关被按下的时候,它会把所有它控制的彩灯改变状态(即亮变成不亮,不亮变成亮)。给定每个开关所控制彩灯的范围,求这些彩灯的样式的方案数。答案对 \(2008\) 取模。

(初始时所有彩灯都是不亮的状态。两种样式不同当且仅当有至少一个彩灯的状态不同。)

Solution:

考虑把开关的控制转化为将所有彩灯的状态异或上一个数 \(x\)。根据异或的性质,该转化成立。

构造出 \(x\) 的集合的线性基,求出线性基中元素的数量 \(k\)。

由于线性基中的每个元素都有选与不选两种情况,并且线性基中不同的异或组合异或出的数都是不同的(线性基的性质),所以答案就是 \(2^k\)。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=60;
int n,m,x,a[N];
char s[N];
void insert(int x){
    for(int i=N-1;i>=0;i--){
        if(((x>>i)&1)==0) continue;
        if(!a[i]){a[i]=x;break;} 
        else x^=a[i];
    }
} 
int query(){    //求出线性基中元素的数量
    int cnt=0;
    for(int i=N-1;i>=0;i--)
        if(a[i]) cnt++;
    return cnt;
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%s",s+1),x=0;
        for(int j=1;j<=n;j++)
            if(s[j]=='O') x|=(1ll<<(j-1));
        insert(x);
    }
    printf("%lld\n",(1ll<<query())%2008);
    return 0; 
}

2. Luogu P4151 最大 XOR 和路径

题目大意:给一个 \(n\) 个点 \(m\) 条边的无向有权图,可能有重边或自环,保证图连通。求从 \(1\to n\) 的路径的最大异或和。

路径可以重复经过某些点或边,当一条边在路径中出现了多次时,其权值在计算异或和时也要被计算相应多的次数。

\(n\leq 5\times 10^4,m\leq 10^5,d_i\leq 10^{18}\),其中 \(d_i\) 为边权。

Solution:

先考虑不经过环的情况(即链的情况),找到一条路径。

考虑增广。如图,从某一点开始,经过一个环,再原路返回。

往返的路径两次异或后对答案的贡献为 \(0\),所以只需考虑环的异或和。增广的路径就是环上的路径。

由于保证图为连通图,所以每个环都能走到。

把所有环的异或和丢进线性基,选一条链作为初值,求异或和最大值。

那么如何选择作为初值的链?假设 \(1\to n\) 的路径有 \(a\) 和 \(b\) 两条,并且我们选择了 \(a\) 作为初值。若 \(b\) 更优,由于 \(a\) 和 \(b\) 共同组成一个环,且所有环的价值都已经丢进了线性基,而 \(a\) 的异或和异或上 \(a\) 和 \(b\) 共同组成的环的异或和,就能得到 \(b\) 的异或和。求最大值时一定会发现异或上这个环的异或和会使答案更优,从而得到 \(b\) 的异或和。

\(1\to n\) 的路径一定会两两组成若干个环,无论选择哪条链作为初值,最终都可以得到答案。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,M=61;
int n,m,x,y,z,cnt,hd[N],to[N<<1],nxt[N<<1],val[N<<1],a[M],d[N];
bool vis[N];
void add(int x,int y,int z){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt,val[cnt]=z;
}
void insert(int x){
    for(int i=M-1;i>=0;i--){
        if(((x>>i)&1)==0) continue;
        if(!a[i]){a[i]=x;break;} 
        else x^=a[i];
    }
} 
int query(int x){
    int ans=x;
    for(int i=M-1;i>=0;i--)
        ans=max(ans,ans^a[i]);
    return ans;
}
void dfs(int x){
    vis[x]=1;
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i],z=val[i];
        if(!vis[y]) d[y]=d[x]^z,dfs(y);
        else insert(d[x]^d[y]^z);    //将环的异或和丢进线性基 
    }
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%lld%lld%lld",&x,&y,&z);
        add(x,y,z),add(y,x,z);
    }
    dfs(1),printf("%lld\n",query(d[n]));
    return 0; 
}

3. Luogu P4301 新 Nim 游戏

题目大意:传统 \(\text{Nim}\) 游戏:现在有 \(n\) 堆石子,第 \(i\) 堆有 \(a_i\) 个。两人轮流操作,每人每次可以从任选一堆中取走任意多个石子,但是不能不取。取走最后一个石子的人获胜(即无法再取的人就输了)。

新 \(\text{Nim}\) 游戏:第一轮,先手和后手可以取走若干个整堆的石子,可以一堆都不拿,但不能全部取走。接下来为传统 \(\text{Nim}\) 游戏。

问先手第一轮拿的石子数目的最小值。若不能保证取胜,输出 \(-1\)。

\(1\leq n\leq 100,1\leq a_i\leq 10^9\)。

Solution:

传统 \(\text{Nim}\) 游戏先手必胜,当且仅当 \(a_1\oplus a_2\oplus \cdots \oplus a_n\neq 0\)。

所以,若先手取完石子后,后手无论怎么取,都不能使剩余石子堆的异或和为 \(0\)(异或和为 \(0\) 意味着后手必胜),则先手必胜。

那么,在新 \(\text{Nim}\) 游戏中先手必胜,当且仅当先手取完石子后不存在剩余石子堆集中的子集,使得它的异或和为 \(0\)。

考虑线性基。在插入线性基时,若结束时 \(x=0\),意味着原来的线性基中已经可以通过异或表示出原先的 \(x\) 了,那么 \(x\) 与线性基中表示 \(x\) 的数异或起来就是 \(0\)。为了使后手无法使石子堆的异或和为 \(0\),先手就要把 \(x\) 取走。

于是问题转化为如何使第一轮拿的石子数目最小。

贪心:从大到小,能取则取(即结束时 \(x=0\) 时就取,否则插入线性基)。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=110,M=60;
int n,p[N],a[M],ans;
bool solve(int x){
    for(int i=M-1;i>=0;i--){
        if(((x>>i)&1)==0) continue;
        if(!a[i]){a[i]=x;break;} 
        else x^=a[i];
    }
    return x==0;
} 
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
        scanf("%lld",&p[i]);
    sort(p+1,p+1+n,greater<int>());
    for(int i=1;i<=n;i++)
        if(solve(p[i])) ans+=p[i];
    printf("%lld\n",ans);
    return 0; 
}

4. Luogu P3292 幸运数字

题目大意:给定一棵 \(n\) 个节点的树。求点 \(x\) 到 \(y\) 的简单路径上,任意选择若干个点的点权异或和的最大值。

\(n\leq 2\times 10^4,q\leq 2\times 10^5,w\leq 2^{60}\),其中 \(w\) 为点权。

Solution:

设点权的最大值为 \(w\)。

两个线性基是可以暴力合并的。对于集合 \(A,B\),把 \(B\) 线性基中的元素依次插入到 \(A\) 的线性基中即可得到 \(A\cup B\) 的线性基。定义 \(\text{merge}\) 运算合并两个线性基。显然 \(\text{merge}\) 运算的复杂度是 \(\mathcal{O(\log^2 w)}\) 的。

考虑树上倍增。设 \(f_{i,j}\) 表示节点 \(i\) 向上跳 \(2^j\) 步所到达的节点编号,\(g_{i,j}\) 表示节点 \(i\) 向上跳 \(2^j\) 步所经过结点(不包括节点 \(i\))的点权组成的线性基。则有:\(f_{i,j}=f_{f_{i,j-1\ \ }\ ,j-1},g_{i,j}=g_{i,j-1}\text{ merge }g_{f_{i,j-1\ \ }\ ,j-1}\)。

可以通过 DFS 预处理出 \(f\) 与 \(g\)。

对于每一组询问 \((x,y)\),令 \(t=\text{lca}(x,y)\)。我们把 \((x,y)\) 的路径拆成 \((x,t)\) 和 \((y,t)\) 两条路径,分别 RMQ。具体地,以 \((x,t)\) 为例,令 \(k=\log_{2}({dep}_x-{dep}_t+1)\),令 \(\text{jump}(x,k)\) 为 \(x\) 向上跳 \(k\) 步所到达的节点标号,把 \(g_{x,k}\) 和 \(g_{\text{jump}(x,{dep}_x-{dep}_t+1-2^k),k}\) 合并,求出合并得到的线性基的最大异或和即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e4+5,M=60;
int n,q,a[N],x,y,cnt,hd[N],to[N<<1],nxt[N<<1],f[N][25],dep[N],t,k;
struct node{
    int a[M];
    void insert(int x){ 
        for(int i=M-1;i>=0;i--){
            if(((x>>i)&1)==0) continue;
            if(!a[i]){a[i]=x;break;} 
            else x^=a[i];
        }
    } 
    int query(){    //求最大异或和 
        int ans=0;
        for(int i=M-1;i>=0;i--)
            ans=max(ans,ans^a[i]);
        return ans;
    }
}g[N][25],ans;
node operator + (node x,node y){    //合并 x 和 y 
    node res=x;
    for(int i=0;i<M;i++)
        if(y.a[i]) res.insert(y.a[i]);
    return res;
}
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
void dfs(int x,int fa){
    dep[x]=dep[fa]+1,g[x][0].insert(a[x]);
    for(int i=0;i<=19;i++)
        f[x][i+1]=f[f[x][i]][i],g[x][i+1]=g[x][i]+g[f[x][i]][i];
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(y==fa) continue;
        f[y][0]=x,dfs(y,x);
    }
}
int LCA(int x,int y){     //LCA 
    if(dep[x]<dep[y]) swap(x,y);
    for(int i=20;i>=0;i--){ 
        if(dep[f[x][i]]>=dep[y]) x=f[x][i];
        if(x==y) return x;
    } 
    for(int i=20;i>=0;i--)
        if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
    return f[x][0];
} 
int get(int x,int k){    //求 x 向上跳 k 步所到达的节点标号
    for(int i=20;i>=0;i--)
        if((k>>i)&1) x=f[x][i];
    return x;
}
signed main(){
    scanf("%lld%lld",&n,&q);
    for(int i=1;i<=n;i++)
        scanf("%lld",&a[i]);
    for(int i=1;i<n;i++){
        scanf("%lld%lld",&x,&y);
        add(x,y),add(y,x);
    }
    dfs(1,0);    //预处理出 f 和 g 
    while(q--){
        scanf("%lld%lld",&x,&y),t=LCA(x,y);
        k=log(dep[x]-dep[t]+1)/log(2),ans=g[x][k];
        if(dep[x]-dep[t]+1!=(1<<k)) ans=ans+g[get(x,dep[x]-dep[t]+1-(1<<k))][k];
        k=log(dep[y]-dep[t]+1)/log(2),ans=ans+g[y][k];
        if(dep[y]-dep[t]+1!=(1<<k)) ans=ans+g[get(y,dep[y]-dep[t]+1-(1<<k))][k];
        printf("%lld\n",ans.query()); 
    }
    return 0;
}

5. BZOJ 3569 DZY Loves Chinese II

题目大意:给定一张 \(n\) 个点 \(m\) 条边的无向连通图,多次询问,每次询问删掉 \(k\) 条边后图是否连通。询问相互独立,强制在线,\(k\) 条边的编号需异或之前询问答案为连通的数量。

\(n\leq 10^5,m\leq 5\times 10^5,q\leq 5\times 10^4,1\leq k\leq 15\),保证没有重边和自环。

Solution:

考虑把无向连通图拆成一棵树和一些边。

对于每条非树边,我们随机一个权值给它。对于树边,它的权值就是所有覆盖它的非树边的权值的异或和。

那么删掉 \(k\) 条边后图不连通,当且仅当这 \(k\) 条边中存在一个子集的权值异或和为 \(0\),即把一条树边以及覆盖它的非树边都删去了。

线性基维护即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,M=5e5+5,K=60;
int n,m,q,x,y,cnt,hd[N],to[M<<1],nxt[M<<1],id[M<<1],a[K],k,w[M<<1],d[N],ans;
bool vis[N],flag;
void add(int x,int y,int z){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt,id[cnt]=z;
} 
void dfs(int x,int fa){
    vis[x]=1;
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i],z=id[i];
        if(y==fa) continue;
        if(vis[y]){
            if(!w[z]) w[z]=rand()+1,d[x]^=w[z],d[y]^=w[z];    //对于非树边,随机一个权值 
        }
        else dfs(y,x),w[z]=d[y],d[x]^=d[y];    //树边的权值 
    }
}
bool solve(int x){
    for(int i=K-1;i>=0;i--){
        if(((x>>i)&1)==0) continue;
        if(!a[i]){a[i]=x;break;} 
        else x^=a[i];
    }
    return x==0;
} 
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%lld%lld",&x,&y);
        add(x,y,i),add(y,x,i);
    }
    dfs(1,0),scanf("%lld",&q);
    while(q--){
        scanf("%lld",&k),flag=1,fill(a,a+K,0);
        for(int i=1;i<=k;i++){
            scanf("%lld",&x);
            if(solve(w[x^ans])) flag=0;    //存在异或和为 0 的情况 
        }
        puts(flag?"Connected":"Disconnected"),ans+=flag;
    }
    return 0;
}

 

标签:基中,int,异或,笔记,--,算法,ans,线性
来源: https://www.cnblogs.com/maoyiting/p/13958352.html