线性基
基,顾名思义里面的东西是用来组成需要的所有东西的基础。线性基即通过线性组合得到需要的所有东西。
严谨一点(太严谨有点奇怪)。有一组 \(k\) 维向量 \(T=\{a_1,a_2,\cdots,a_n\}\),对于值域(需要的所有向量)\(S\),\(\forall x\in S,x=t_1\cdot a_1+t_2\cdot a_2+\cdots+t_n\cdot a_n\),\(S\) 被称为数域,\(T\) 是 \(S\) 的基,且 \((T,+)\) 是 Abel 群(对 \(+\) 封闭且满足结合律交换律,有单位元逆元),则称 \((T,+,\cdot,S)\) 为线性空间。
当 \(a_1,a_2,\cdots,a_n\) 两两“互质”(向量组内部没有向量可以被其他向量表出),叫做极大线性无关组,对于任意 \(R\supseteq T\),\(|T|\) 被称为 \(R\) 的秩,称线性基。
特别的 \(\{0\}\) 的基为 \(\varnothing\)。
\(k\) 维线性空间 \(S\) 可以由 \(k\) 个单位向量线性表示。比如 \(\begin{pmatrix} 1\\ 0\\ 0 \end{pmatrix},\begin{pmatrix} 0\\ 1\\ 0 \end{pmatrix},\begin{pmatrix} 0\\ 0\\ 1 \end{pmatrix}\) 三个基可以表示三维线性空间。记 \(k\) 为线性基的元素个数。通常写作 \(\dim S\)。
线性基的用处:
- 求某向量组的秩
- 求某向量组的一组基
- 动态插入向量
- 判断某向量是否能被 某向量组的一组基 表示
- 求线性空间的极值/特殊值 ★
用的最多的是异或线性基,它可以用来处理线性空间 \((T,\oplus,\And,B^k,B=\{0,1\})\) 的问题,即 \(k\) 维 bool 空间的问题,即每维只有 0/1 两种取值。
构造:记原组为 \(A\),设 \(P\) 为基,\(P[i]\) 表示最高位的 1 为第 \(i\) 位的一个基。
贪心地,枚举 \(i\),对于每个 \(a_i\),设 \(a_i\) 的最高位为 \(j\),则若 \(p[j]=0\),则把 \(p[j]\) 设为 \(a_i\),否则更新 \(a_i\) 为 \(a_i\oplus p[j]\),即降低 \(j\),使 \(a_i\) 可能成为低位的基从而尽快填满基。
现在看如何实现上述用处。
- 直接对每个 \(j\) 看 \(p[j]\) 是否等于 0,等于 0 说明最高位为 \(j\) 的基不存在。秩即为 \(r=\sum [p[j]\neq 0]\)。
- 即构造。
- 显然构造是动态的。
- 设数为 \(x\)。先特判 \(x=0\),如果 \(r\) 满了则无法表示;否则若 \(x\) 的第 \(j\) 位为 1,则将 \(x\) 异或上 \(p[j]\),即消去第 \(j\) 位,直到消完,如果消不完就表示不了。
-
- 极小值:如果 \(r\) 满了则为 \(p[0]\),否则为 0。
- 极大值:记一个变量 \(x\) 为 0,从高到低枚举 \(j\) 若 \(x\) 第 \(j\) 位为 0 则异或上 \(p[j]\)。
- 第 \(k\) 小/大:显然这两个问题可以互相转化。现在考虑第 \(k\) 小。对于这组基,显然可以得到 \(2^r\) 个异或值(选或不选某个 \(p[j]\)),若可以异或出 0,则将 0 设为第 1 小。考虑重构 \(P\),使得只有 \(p[j]\) 的第 \(j\) 为 1,显然可以直接异或得到。然后可以发现只有前 \(r\) 个 1 的数值会变化,显然这几位字典序排一排就可以搞出第 \(k\) 小。
板子(求最大异或和):
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+7;
int n;
int a[maxn];
int p[66];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
for(int j=50;~j;j--){
if(a[i]&(1ll<<j)){
if(!p[j]){
p[j]=a[i];
break;
}else a[i]^=p[j];
}
}
}
int x=0;
for(int j=50;~j;j--){
if(x&(1ll<<j)) continue;
x^=p[j];
}
cout<<x;
return 0;
}
经典例题:最大异或路径
给你一个无向图,求 \(s\) 到 \(t\) 的路径中,边权异或和最大的那条。
先要找性质,把问题转化成线性基可以做的东西。
性质 1:将路径异或上一个环(\(n\ge 3\))相当于走了那个环。显然由于去换上和回到路径上是同一条路,边被异或掉了。
性质 2:如果走环,一定会走完。即使看起来有些环没走完,实际上都可以简化成一条路径异或上若干个环。
有了以上两个性质就可以上线性基了。暴力做法:把所有环加入线性基,随便找一条路径,然后通过线性基将异或和变大。
这是对的。考虑 DFS,当前搜到的 \(u\) 权值为 \(x\),如果 \(u\) 的邻点 \(v\) 已经被搜过,且权值为 \(y\),\((u,v)\) 权值为 \(z\),则该环的权值为 \(x\oplus y\oplus z\),时间复杂度也是对的,为 \(O((n+m)\log x)\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=5e4+7;
int p[77];
void ins(int x){
for(int j=62;~j;j--){
if(x&(1ll<<j)){
if(!p[j]){
p[j]=x;
return;
}else x^=p[j];
}
}
}
struct edge{
int v,w;
};
vector<edge>e[maxn];
int n,m;
int vis[maxn];
int now;
void dfs(int u){
if (u==n) now=vis[u];
for(edge v:e[u]){
if(vis[v.v]==-1){
vis[v.v]=vis[u]^v.w;
dfs(v.v);
}else ins(vis[u]^vis[v.v]^v.w);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m;
for(int i=1,u,v,w;i<=m;i++){
cin>>u>>v>>w;
e[u].push_back({v,w});
e[v].push_back({u,w});
}
for(int i=1;i<=n;i++) vis[i]=-1;
vis[1]=0;
dfs(1);
for(int j=62;~j;j--){
if(now&(1ll<<j)) continue;
now^=p[j];
}
cout<<now;
return 0;
}
P3292 [SCOI2016] 幸运数字
给你一棵树询问 \(u,v\) 路径上选择一些点权使得异或和最大。
线性基 + 倍增。
和 LCA 差不多,记 \(f(j,i)[k]\) 表示 \(i\) 到 \(i\) 的 \(2^j\) 级祖先之间的线性基。则有转移:
merge 直接暴力插入。时间复杂度 \(O(n\log n\log^2 a)\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e4+7;
bool st;
int n,m,w[maxn];
vector<int>e[maxn];
int fa[16][maxn];
int f[16][maxn][63],dep[maxn];
void ins(int x,int y,int z){
for(int j=61;~j;j--){
if(z&(1ll<<j)){
if(!f[x][y][j]){
f[x][y][j]=z;
return;
}else z^=f[x][y][j];
}
}
}
void dfs(int u,int Fa){
fa[0][u]=Fa;
dep[u]=dep[Fa]+1;
ins(0,u,w[u]);
ins(0,u,w[Fa]);
for(int v:e[u])
if(v!=Fa) dfs(v,u);
}
int tmp[77];
void merge1(int x,int y){
for(int i=61;~i;i--){
f[x][y][i]=f[x-1][y][i];
tmp[i]=f[x-1][fa[x-1][y]][i];
}
for(int i=61;~i;i--){
for(int j=61;~j;j--){
if(tmp[i]&(1ll<<j)){
if(!f[x][y][j]){
f[x][y][j]=tmp[i];
break;
}else tmp[i]^=f[x][y][j];
}
}
}
}
int pmp[77];
void merge2(int x,int y){
for(int i=61;~i;i--){
tmp[i]=f[x][y][i];
for(int j=61;~j;j--){
if(tmp[i]&(1ll<<j)){
if(!pmp[j]){
pmp[j]=tmp[i];
break;
}else tmp[i]^=pmp[j];
}
}
}
}
int result(){
int x=0;
for(int j=61;~j;j--){
if(x&(1ll<<j)) continue;
x^=pmp[j];
}
return x;
}
void init(){
for(int j=1;j<=14;j++)
for(int i=1;i<=n;i++)
fa[j][i]=fa[j-1][fa[j-1][i]];
for(int j=1;j<=14;j++)
for(int i=1;i<=n;i++)
merge1(j,i);
}
int lca(int u,int v){
if(u==v) return w[u];
if(dep[u]<dep[v]) swap(u,v);
for(int i=0;i<=61;i++) pmp[i]=0;
int step=dep[u]-dep[v];
for(int j=14;~j;j--){
if(step&(1<<j)){
merge2(j,u);
u=fa[j][u];
}
}
if(u==v) return result();
for(int j=14;~j;j--){
if(fa[j][u]!=fa[j][v]){
merge2(j,u);
merge2(j,v);
u=fa[j][u],v=fa[j][v];
}
}
merge2(0,u);
merge2(0,v);
return result();
}
bool ed;
signed main(){
// cerr<<(&ed-&st)/1048576.0<<" MB\n";
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>w[i];
}
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
dfs(1,0);
init();
while(m--){
int u,v;
cin>>u>>v;
cout<<lca(u,v)<<'\n';
}
return 0;
}