【学习笔记】线性基
# 零:【前言】
学一遍忘一遍(雾)
# 壹:【定义】
一堆数字能得到的非 \(0\) 异或和的结果,能被元素个数最少的集合,不多不少的全部得到
那么就说,元素个数最少的集合,是这一堆数字的异或空间线性基
有以下结论
结论一:
一堆数字中,任意的 \(a\) 和 \(b\),用 \(a \oplus b\) 代替 \(a\) 和 \(b\) 中的一个数字,不会影响异或和的组成】
证明:
记 \(a \oplus b=x,a\) 和 \(b\) 可以异或出的集合为 \(\{a,b,x\}\)
那么用 \(x\) 替换掉 \(b\) ,那么 \(a\) 和 \(x\) 可以异或出的集合仍为 \(\{a,b,x\}\)
所以替换后构建集合不变,因此成立
结论二:
一堆数字中,任意的 \(a\) 和 \(b\) 满足 \(a \oplus b=0\) ,那么舍弃 \(a\) 和 \(b\) 中的任意一个数组,不会影响异或和的组成
证明:
\(a\) 和 \(b\) 异或出的集合为 \(\{a\}\)
\(a\) 异或出的集合为 \(\{a\}\)
所以舍弃之后构建集合不变,结论成立
结论三:
一堆数字能否异或出 \(0\),在求出异或空间线性基后,需要被单独标记
解释:
以上我们都在非0的情况下进行讨论
集合 \(\{1,2,3\}\) 可以异或出 \(0\) ,而它的线性基 \(\{1,2\}\) 不可以异或出 \(0\)
因此需要单独标记是否可以异或出 \(0\)
# 贰:【构建】
一:【普通消元】
1.【操作】
我们现在有一个序列 \(a\) ,我们要求其异或空间线性基。
考虑将 \(a_i\) 分别插入线性基。先将 \(a_i\) 进行二进制分解。找到二进制下,最高位的那个 \(1\) 。
若此时这一位还没有基,那么就把这一位的基设为 \(a_i\)
否则就将 \(a_i\) 与这一位的基异或(结论一),去找下一位 \(1\) 重复此操作。
直到 \(a_i\) 变为 \(0\) (结论二:舍弃)或者找到一位没有值的基插入。
每个基底只有一个数,这样可以保证线性基合法的同时大小不超过 \(log_2(max)\)
对于如何判断原集合是否可以异或出 \(0\) ,直接判断原集合和线性基大小是否相等即可
2.【代码】
void insert(LL x){
for(int i=51;i>=0;i--){
if(!(x>>i&1)) continue;
if(!basis[i]){
basis[i]=x;
return ;
}
x^=basis[i];
}
}
3.【应用】
(1)线性基大小
(2)原集合可以构造集合大小
(3)是否可以异或出 \(0\)
(4)最大异或和
按基底从高到低构造 \(ans\) ,如果这一位 \(ans\) 有 \(1\) ,跳过,否则异或基底元素,正确性显然
实际操作时可以 \(ans=max(ans,ans \oplus basis[i])\)
二:【高斯消元】
1.【操作】
高斯消元求出来的线性基和普通消元求出来的线性基不同的地方在于,高斯消元求出来的线性基是标准形式的线性基。
什么是标准形式?就是对于求出来的线性基,二进制下,每一位对应着只有一个 \(1\) 并不会出现同一位多个 \(1\) 情况。
而在普通消元中,我们因为只找最高位的 \(1\) 所以我们无法保证他的低位的 \(1\) 和其他基的低位的 \(1\) 没有重合。
高斯消元求线性基的过程类似于,高斯消元解线性方程组。
类似解线性方程组。假设当前要算出第 \(i\) 位的基。从第 \(i\) 行往下找,找到第一个 \(a_j\) 使得他的该二进制位为 \(1\) ,将 \(a_i\) 与 \(a_j\) 交换。
然后再次遍历 \(a_1\) 到 $a_n $若 \(a_j\) 这位也是 \(1\) 则将 \(a_j\) 与 \(a_i\) 异或,来保证这一位,只能出现 \(a_i\) 一个 \(1\) 。
2.【代码】
int cnt=1;
for(int i=51;i>=0;i--){
for(int j=cnt;j<=n;j++){
if(a[j]>>i&1){
swap(a[cnt],a[j]);
break;
}
}
if(a[cnt]>>i&1){
for(int j=1;j<=n;j++){
if(j==cnt) continue;
if(a[j]>>i&1) a[j]^=a[cnt];
}
cnt++;
}
}
3.【应用】
这里的应用指的是普通消元无法处理的应用
(1):不包括 \(0\) 第 \(k\) 小异或和(标准形式的线性基类二进制)
将 \(k\) 二进制分解,若第 \(i\) 位是 \(1\) ,则将第 \(i\) 的基异或进答案。
# 参:【拓展】
一:【向量空间线性基】
1.定义
\(k\) 维空间下,\(n\) 个向量可以线性组合出的集合,可以被向量最少的集合,不多不少的线性组合出
称这个向量最少的集合为向量空间线性基
2.构建
类似于异或空间线性基,分为普通消元和高斯消元
不过是把 \(\oplus\) 运算改为了 \((*,+)\) 运算
3.例题
第一个是求线性空间线性基的大小,第二个是求最小花费
我们按照花费从小到大排序,然后普通消元构建线性基即可
代码
// Problem: P3265 [JLOI2015] 装备购买
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3265
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define eps 1e-4
using namespace std;
using LL=long long;
const int N=510;int n,m;
struct vec{
double tp[N];
int val;
bool operator<(const vec b)const{
return this->val<b.val;
}
}a[N];
int basis[N];
int cnt=0;
int ans=0;
void insert(int x){
for(int i=1;i<=m;i++){
if(-eps<=a[x].tp[i]&&a[x].tp[i]<=eps) continue;
if(!basis[i]){
basis[i]=x;
cnt++;
ans+=a[x].val;
return ;
}
double alpha=a[x].tp[i]/a[basis[i]].tp[i];
for(int j=1;j<=m;j++){
a[x].tp[j]-=alpha*a[basis[i]].tp[j];
}
}
}
void solve(){
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++) cin>>a[i].tp[j];
}
for(int i=1;i<=n;i++) cin>>a[i].val;
sort(a+1,a+1+n);
for(int i=1;i<=n;i++) insert(i);
cout<<cnt<<" "<<ans<<"\n";
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
while(t--) solve();
return 0;
}
二:【线段树+线性基合并】
每个线段树节点上维护一个线性基,查询时暴力合并区间内各个节点的线性基,贪心取最大值即可,复杂度为 \(log^3\)
代码
// Problem: P4839 P 哥的桶
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4839
// Memory Limit: 500 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define eps 1e-6
using namespace std;
using LL=long long;
const int N=5e4+10,M=32;
struct Thebasis{
int basis[M];
int cnt;
Thebasis(){
memset(basis,0,sizeof basis);
cnt=0;
}
void insert(int x){
for(int i=M-1;i>=0;i--){
if(!(x>>i&1)) continue;
if(!basis[i]){
basis[i]=x;
cnt++;
return ;
}
x^=basis[i];
}
}
int query(){
int ans=0;
for(int i=M-1;i>=0;i--){
ans=max(ans,ans^basis[i]);
}
return ans;
}
void merge(Thebasis b){
for(int i=M-1;i>=0;i--){
if(b.basis[i]) insert(b.basis[i]);
}
}
};
struct seg_tree{
int l,r;
Thebasis t;
#define l(q) tree[q].l
#define r(q) tree[q].r
#define t(q) tree[q].t
}tree[N<<2];
void build(int q,int l,int r){
l(q)=l;r(q)=r;
if(l==r) return ;
int mid=l+r>>1;
build(q<<1,l,mid);
build(q<<1|1,mid+1,r);
}
void update(int q,int tp,int d){
int l=l(q),r=r(q);
t(q).insert(d);
if(l==r) return ;
int mid=l+r>>1;
if(tp<=mid) update(q<<1,tp,d);
else update(q<<1|1,tp,d);
}
Thebasis query(int q,int L,int R){
int l=l(q),r=r(q);
if(L<=l&&r<=R) return t(q);
Thebasis ans;
int mid=l+r>>1;
if(L<=mid) ans.merge(query(q<<1,L,R));
if(mid<R) ans.merge(query(q<<1|1,L,R));
return ans;
}
void solve(){
int n,m;cin>>n>>m;
build(1,1,m);
while(n--){
int op;cin>>op;
if(op==1){
int k,x;cin>>k>>x;
update(1,k,x);
}
else{
int l,r;cin>>l>>r;
Thebasis ans=query(1,l,r);
cout<<ans.query()<<"\n";
}
}
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
while(t--) solve();
return 0;
}
三:【线性基+LCA】
\(log^3\) 过不去,所以不能倍增然后暴力合并,需要其他方法解决
定义 \(S(i)\) 表 \(root->i\) 的线性基集合
查询 \((u,v)\) 时,记 \(w=LCA(u,v)\) ,合并 \(S(u)\) 和 \(S(v)\) 中所有深度 \(k>=dep(w)\) 的基,然后贪心取最大值
那么我们维护时,\(S(i)\) 由 \(S(fa)\) 转移过来的时候,不能按照一般的普通消元的方式插入,而需要按照 \(Dep\) 较大优先的方式迭代替换 \(S(fa)\)
代码
// Problem: P3292 [SCOI2016] 幸运数字
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3292
// Memory Limit: 250 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define Pair pair<long long,long long>
#define val first
#define dep second
#define eps 1e-6
using namespace std;
using LL=long long;
const int N=2e4+10,M=61;
struct Thebasis{
Pair basis[M];
int cnt;
Thebasis(){
for(int i=0;i<M;i++) basis[i]={0,0};
cnt=0;
}
void copys(Thebasis b){
for(int i=M-1;i>=0;i--) this->basis[i]=b.basis[i];
}
void insert(Pair x){
for(int i=M-1;i>=0;i--){
if(!(x.val>>i&1)) continue;
if(!basis[i].val){
basis[i]=x;
cnt++;
return ;
}
if(x.dep>basis[i].dep) swap(basis[i],x);
x.val^=basis[i].val;
}
}
void merge(Thebasis b){
for(int i=M-1;i>=0;i--){
if(b.basis[i].val) insert(b.basis[i]);
}
}
Thebasis gets(int dep){
Thebasis ans;
for(int i=M-1;i>=0;i--){
if(this->basis[i].dep>=dep) ans.insert(this->basis[i]);
}
return ans;
}
LL query(){
LL ans=0;
for(int i=M-1;i>=0;i--){
ans=max(ans,ans^basis[i].val);
}
return ans;
}
};
struct node{
LL dep,lucky,fa;
Thebasis sets;
LL siz,son,top;
node(){
dep=lucky=fa=0;
siz=son=top=0;
}
}a[N];
vector<int> mp[N];
void Son(int u,int pa){
a[u].fa=pa;
a[u].dep=a[pa].dep+1;
a[u].siz=1;
a[u].sets.copys(a[pa].sets);
a[u].sets.insert({a[u].lucky,a[u].dep});
for(auto v:mp[u]){
if(v==pa) continue;
Son(v,u);
a[u].siz+=a[v].siz;
if(a[v].siz>a[a[u].son].siz) a[u].son=v;
}
}
void Line(int u,int tp){
a[u].top=tp;
if(!a[u].son) return ;
Line(a[u].son,tp);
for(auto v:mp[u]){
if(v==a[u].fa||v==a[u].son) continue;
Line(v,v);
}
}
int LCA(int x,int y){
while(a[x].top!=a[y].top){
if(a[a[x].top].dep<a[a[y].top].dep) swap(x,y);
x=a[a[x].top].fa;
}
if(a[x].dep>a[y].dep) swap(x,y);
return x;
}
void solve(){
int n,q;cin>>n>>q;
for(int i=1;i<=n;i++) cin>>a[i].lucky;
for(int i=1;i<n;i++){
int x,y;cin>>x>>y;
mp[x].push_back(y);
mp[y].push_back(x);
}
Son(1,0);
Line(1,0);
while(q--){
int x,y;cin>>x>>y;
int z=LCA(x,y);
Thebasis ans;
ans.merge(a[x].sets.gets(a[z].dep));
ans.merge(a[y].sets.gets(a[z].dep));
cout<<ans.query()<<"\n";
}
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
while(t--) solve();
return 0;
}
四:【线性基+图分析】
发现答案肯定是由随便一条由 \(1\) 到 \(n\) 的路径异或上图中若干个环。
构造方案的话只需要从 \(1\) 号点走到环上任意一点,转完整个环,再回到 \(1\) 号点,会发现只有环上的值被取了,其他路径上的值都被异或抵消了。
从 \(1\) 到 \(n\) 路径的选择随意就好了,因为如果有另一条更优的路径的话,这两条路径就会构成一个环,最优解用这个环异或掉原来选择的路径,就会自动变为更优的路径。
dfs搜环维护线性基即可
代码
// Problem: P4151 [WC2011] 最大 XOR 和路径
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4151
// Memory Limit: 500 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define Pair pair<long long,long long>
#define w first
#define v second
#define eps 1e-6
using namespace std;
using LL=long long;
const int N=5e4+10,M=64;
vector<Pair> mp[N];
struct Thebasis{
LL basis[M];
int cnt;
Thebasis(){
memset(basis,0,sizeof basis);
cnt=0;
}
void insert(LL x){
for(int i=M-1;i>=0;i--){
if(!(x>>i&1)) continue;
if(!basis[i]){
basis[i]=x;
cnt++;
return ;
}
x^=basis[i];
}
}
LL query(LL x){
LL ans=x;
for(int i=M-1;i>=0;i--){
ans=max(ans,ans^basis[i]);
}
return ans;
}
}sets;
LL dist[N],vis[N];
void dfs(int u,int pa,LL val){
vis[u]=1;
dist[u]=val;
for(auto e:mp[u]){
LL v=e.v,w=e.w;
if(v==pa) continue;
if(!vis[v]) dfs(v,u,val^w);
else sets.insert(val^w^dist[v]);
}
}
void solve(){
int n,m;cin>>n>>m;
while(m--){
LL a,b,c;cin>>a>>b>>c;
mp[a].push_back({c,b});
mp[b].push_back({c,a});
}
dfs(1,0,0);
cout<<sets.query(dist[n])<<"\n";
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
while(t--) solve();
return 0;
}
# 肆:【再拓展(实际上是后来补的)】
肆的内容可能与上面有交
一:查询异或和为 0 的子集数
这实际上就是询问线性基的秩的大小。设插入了 \(𝑛\) 个数,线性基的秩为 \(𝑚\),则异或和为 \(0\) 的子集数即为 \(2^{𝑛−𝑚}\)。
二:查询任意子集的异或和异或一个数 \(𝑘\) 的最大值/最小值
假设求的是最大值,最小值同理。
从高到低贪心考虑。对于每一位,如果这一位是 \(0\) 的话,就异或上线性基这一位的基,这样会把这一位改为 \(1\),肯定更优。
时间复杂度 \(𝑂(\log 𝑉 )\)。
int qry(uint k) {
for (int i = 29; i >= 0; --i) if (~k >> i & 1) k ^= f[i];
return k;
}
三:查询子集异或和第 \(𝑘\) 小
设秩为 \(𝑚\),则有结论:每一个能异或出来的数都会出现恰好 \(2^{𝑛−𝑚}\) 次。
现在只需要求出线性基能异或出的数中,第 \(𝑘\) 小的数即可。
从高到低枚举每一位。
如果线性基这一位上有基向量,则这一位上既可以取到 \(0\) 也可以取到 \(1\),且设更低位一共有 \(𝑥\) 个基,则取到 \(0\) 和取到 \(1\) 的数各有 \(2^𝑥\) 个;
这一位上没有基向量的话则啥都不用干。
四:线性基合并
暴力合并即可
五:前缀线性基
每次插入数时额外提供一个该数的时间戳 \(𝑡\)。查询时要求所有时间戳 \(≥ 𝑡\) 的数形成的线性基。
对线性基中每个基向量额外记录这个基的时间戳。
当插入数时,如果当前位置已经有基了,在异或的同时把两个时间戳中较大的那个留到当前位置,较小的那个继续插入。
查询时把所有时间戳 \(≥ 𝑡\) 的基向量拉出来组成一个线性基。
uint f[N]; int ti[N];
void ins(uint k, int t) {
for (int i = 30 - 1; i >= 0; --i) if (k >> i & 1) {
if (!f[i]) {
f[i] = k, ti[i] = t;
return;
}
if (t > ti[i]) {
swap(k, f[i]), swap(t, ti[i]);
}
k ^= f[i];
}
return;
}
六:可删除并查集
我只会离线做法QWQ
1.线段树分治
2.维护时间点
仿照前缀线性基,我们有复杂度更好的离线做法。
离线的时候,我们可以计算出每个数的删除时间。进而可以在线性基里维护每个基的删除时间。
当插入一个数的时候,如果它当前异或的基的删除时间比它早,那么我们不如直接交换这个基和你当前的数。
让你当前的数代替这个位置,让那个基来继续执行插入的操作。
容易发现这样构造的线性基,小的基的删除时间都比大的基早,因此删除就可以直接删除了。
时间复杂度仍是 \(𝑂(𝑛 \log 𝑉 )\)。
七:\(K\) 进制线性基
读者自悟不难
inline int Inv(int x) {
int a, b; exgcd(x, k, a, b);
return (a + k) % k;
}
inline void insert(Vector x) {
for (int i = m - 1; ~i; i--) {
if (!x[i]) continue;
if (!bas[i][i]) {
bas[i] = x * Inv(x[i]);
x = x * (k / gcd(k, x[i]));
}
while (x[i]) {
int t = bas[i][i] / x[i];
bas[i] = bas[i] - x * t;
swap(x, bas[i]);
}
}
}

浙公网安备 33010602011771号