线性基学习笔记
1.$CDQ$ 分治总结2.基环树算法总结3.(抄自己luogu上的博客)莫队总结4.珂朵莉树总结5.虚树总结6.从龟速乘到 $Miller-Rabin$ 算法(数论算法总结)7.浅谈李超线段树8.浅谈长链剖分9.浅谈平衡树10.可持久化数据结构算法总结11.线段树运用进阶12.群论学习笔记
13.线性基学习笔记
14.LCT 学习笔记15.原根学习笔记+BSGS复习笔记16.多项式算法初探:从 FFT 到 NTT17.下降幂、斯特林数学习笔记18.多项式算法再探:FMT 和 FWT19.min-max 容斥(最值反演)学习笔记20.自适应 Simpson 积分法学习笔记21.SAM 学习笔记22.min_25 筛 学习笔记一、概念
线性基实际上就是维护了一个数组 \(p\),满足 \(p_i\) 在二进制下的最高位为第 \(i\) 位。
二、实现
现在我们有一个数组 \(a\),我们要构造他的线性基 \(p\)。
每次插入 \(a_i\) 时,我们都从高位往低位遍历,用以寻找第一个空位插入它。当然,我们也不能直接把原数插入。为了方便查询是否存在该数,每次到达 \(a_i\) 最高位但是 \(p_j\) 已经有值时,\(a_i\gets a_i\oplus p_j\) 。
时间复杂度 \(O(n\log V)\)。
void add(int x){
for(int i=52;~i;i--)
if(x&(1ll<<i)){
if(p[i]) x^=p[i];
else return p[i]=x,void();
}
}
三、应用
- 查询当前插入的数值是否能够异或出 \(x\)。
实际上我们可以理解为插入 \(x\),能插得进去就说明不能,否则就是可以。
int check(int x){
for(int i=52;~i;i--){
if((x&(1ll<<i))==0) continue;
if(p[i]) x^=p[i];
else return 0;
}return 1;
}
- 查询当前数能凑出的异或最大值 \(/\) 最小值。
最小值就是从小往大枚举第一个 \(>0\) 的 \(p_i\),假如在之前发现能凑出 \(0\),那就是 \(0\)。判断是否能凑出 \(0\) 很简单,直接在插入前判断是否能够凑出要插入的数就可以。
最大值从大往小枚举,假如异或 \(p_i\) 后 \(ans\) 更大,那么 \(ans\gets ans\oplus p_i\)。
int maxn(){
int ans=0;
for(int i=52;~i;i--)
ans=max(ans,ans^p[i]);
return ans;
}int minn(){
if(fl) return 0;
for(int i=0;i<53;i++)
if(p[i]) return p[i];
}
- 查询一个值在线性基中的排名
我在 \([luogu4869]\ albus\) 就是要第一个出场 中遇到了这个问题。当然,这个问题还要和下文的特殊性质 \(1\) 结合。
我的方法是在查询中暴力跑出当前选小的后的值,假如要比 \(x\) 小,就去右子树,否则就去左子树。时间复杂度 \(O(\log^2V)\)。
网上好像有单词查询 \(O(\log V)\) 预处理 \(O(\log^2V)\) 的做法,懒得学了。
//luogu4869 albus就是要第一个出场
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=35,p=10086;
int n,ps[N],d[N],q,id,sum;
int qpow(int x,int y){
int re=1;
while(y){
if(y&1) re=re*x%p;
x=x*x%p,y>>=1;
}return re;
}void add(int x){
for(int i=30;~i;i--)
if(x&(1ll<<i)){
if(!ps[i]){
for(int j=30;j>i;j--) d[j]++;
return ps[i]=x,sum++,void();
}x^=ps[i];
}
}int ans(int x,int nw,int y){
if(nw<0) return (y==x?1:0);
if(!ps[nw]) return ans(x,nw-1,y);
int z=min(y,y^ps[nw]),now=nw-1;
while(~now) z=max(z,z^ps[now--]);
if(z>=x) return ans(x,nw-1,min(y,y^ps[nw]));
return (ans(x,nw-1,max(y,y^ps[nw]))+qpow(2,d[nw])*id)%p;
}signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1,a;i<=n;i++)
cin>>a,add(a);
id=qpow(2,n-sum),cin>>q;
cout<<ans(q,30,0);
return 0;
}
当然,线性基也可以辅助其他算法或思想(如贪心)使用。
四、特殊性质
- 长度为 \(n\) 的数组 \(V\),其线性基为 \(B\),定义 \(c_v=\bigoplus\limits_{a\in v}a\),\(num_k=\sum\limits_{v\subseteq V}[c_v=k]\),则 \(\forall k\in\mathbb{N},num_k\in\{0,2^{n-|B|}\}\)。
五、经典套路
- 带删除离线线性基
这个玩意可以用线段树分治离线维护。
//BZOJ4184 Shallot
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5,M=35;
int n,ps[M],vis[N];
unordered_map<int,int>pr;
vector<int>g[N*4];
void add(int x){
for(int i=30;~i;i--)
if(x&(1<<i)){
if(ps[i]) x^=ps[i];
else return ps[i]=x,void();
}
}int maxn(){
int ans=0;
for(int i=30;~i;i--)
ans=max(ans,ans^ps[i]);
return ans;
}void chg(int x,int l,int r,int L,int R,int v){
if(L<=l&&r<=R)
return g[x].push_back(v),void();
int mid=(l+r)/2;
if(L<=mid) chg(x*2,l,mid,L,R,v);
if(R>mid) chg(x*2+1,mid+1,r,L,R,v);
}void solve(int x,int l,int r){
int nps[M];
for(int i=0;i<31;i++)
nps[i]=ps[i];
for(auto y:g[x]) add(y);
int mid=(l+r)/2;
if(l==r) cout<<maxn()<<"\n";
else solve(x*2,l,mid),solve(x*2+1,mid+1,r);
for(int i=0;i<31;i++)
ps[i]=nps[i];
}int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>vis[i];
if(vis[i]<0){
chg(1,1,n,pr[-vis[i]],i-1,-vis[i]);
vis[pr[-vis[i]]]=0,vis[i]=0;
}else pr[vis[i]]=i;
}for(int i=1;i<=n;i++)
if(vis[i]) chg(1,1,n,i,n,vis[i]);
solve(1,1,n);
return 0;
}
- 树上可以走重边的路径异或和 \(=\) 一条路径的异或和 \(\oplus\) 多个环的异或和。
利用该思想,可以将路径和环的异或和扔到线性基中。对于这个线性基来说,本质不同的路径长度就是所有路径长度在线性基中能取到的最小值的种类数。
//BZOJ2322 梦想封印
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=20005,M=65;
struct edge{int to,w;};
int n,m,q,ps[M],ans[N],d[N],nm,cc;
int u[N],v[N],w[N],xr[N],vs[N];
vector<edge>g[N];set<int>st;
int add(int x){
for(int i=62;~i;i--){
if(!((1ll<<i)&x)) continue;
if(!ps[i]){
ps[i]=x,nm++;
return 1;
}x^=ps[i];
}return 0;
}int lst(int x){
for(int i=62;~i;i--)
x=min(x,x^ps[i]);
return x;
}void work(){
st.clear();
for(int i=1;i<=n;i++)
st.insert(lst(xr[i]));
}void dfs(int x,int fa){
vs[x]=1,cc++;
for(auto ed:g[x]){
int y=ed.to,c=ed.w;
if(y==fa) continue;
if(!vs[y]){
xr[y]=xr[x]^c;
st.insert(lst(xr[y]));
dfs(y,x);continue;
}int cr=xr[x]^xr[y]^c;
if(add(cr)) work();
}
}signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m>>q,st.insert(0);
for(int i=1;i<=m;i++)
cin>>u[i]>>v[i]>>w[i];
for(int i=1;i<=q;i++)
cin>>d[i],ans[d[i]]=1;
for(int i=1;i<=m;i++){
if(!ans[i]){
g[u[i]].push_back({v[i],w[i]});
g[v[i]].push_back({u[i],w[i]});
}else ans[i]=0;
}dfs(1,0),ans[q]=(1ll<<nm)*st.size()-1;
for(int i=q-1;~i;i--){
int x=u[d[i+1]];
int y=v[d[i+1]];
int c=w[d[i+1]];
g[x].push_back({y,c});
g[y].push_back({x,c});
if(vs[x]&&vs[y]){
int cr=xr[x]^xr[y]^c;
if(add(cr)) work();
}else if(vs[x]^vs[y]){
if(!vs[x]) swap(x,y);
st.insert(lst(xr[x]^c));
xr[y]=xr[x]^c,dfs(y,x);
}ans[i]=(1ll<<nm)*st.size()-1;
}for(int i=0;i<=q;i++)
cout<<ans[i]<<"\n";
return 0;
}
- 线性基合并
直接暴力把一个线性基的东西 \(add\) 到另一个里就行了,没什么好说的。
int ps[65],b[65];
void merge(){
for(int i=0;i<=62;i++) add(b[i]);
}//把 b 合并到 a 里
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)