xor问题的一种重要知识
学习了这篇:Menci - 线性基学习笔记
由于那一篇内容比较详尽,所以只提一点关键内容,方便记忆
同时也补充了求线性基交/并的内容
跟矩阵中的线性基差不多,xor的线性基大概是这个意思:
有一个长度为$n$的数组$a$,$m=max\{\lceil log_2 (a_i)\rceil \}$;令$A$为$a$的线性基,则任意$a_i,1\leq i\leq n$可以表示为线性基中几个数的xor
求线性基的方法有点类似 用矩阵求方程组的消元过程
若将$a$与$A$中的每个数分别用二进制拆成$m$位,则分别对应了$n\times m$与$m\times m$的矩阵
我们的目标是将$a$数组对应的矩阵消元,并精简为一个$m\times m$的上三角矩阵(即$A$)
具体是这样操作的:
依次加入$a$中元素
对于当前的$a_i$,令$val=a_i$,设最高的$1$在第$x$位(为了方便位运算,记$0\leq x<m$)
1. 若$A_x\neq 0$,则$val=val\ xor\ A_x$,并继续判断最高的$1$
2. 若$A_x=0$,则$A_x=val$
对于所有$A_{j,x}=1,j\neq i$,消去得$A_j=A_j\ xor\ val$(用$A_x$消其他)
对于所有$A_{x,k}=1,k<x$,消去得$A_x=A_x\ xor\ A_k$(用其他消$A_x$)
跟矩阵的求上三角是基本一致的
(2的后面两步在很多情况下是不需要的:若没有这两步,矩阵仍然是上三角;只不过经过这两步处理后,具有了下面的性质2,于是能够从小到大地取能表示的xor值,具体见第一个例题)
用这种方法求出的$A$,有这些性质:
1. 对于$\forall j>i,A_{i,j}=0$ (即二进制拆开后为上三角矩阵)
2. 若$A_{i,i}=1$,则$\forall j\neq i,A_{i,j}=0$ (即最高位有独一性)
代码实现不是很长
#include <cstdio> #include <cstring> using namespace std; typedef long long ll; const int N=100005; const int M=60; void Linear_Basis(ll *a,int n,ll *A) { for(int i=1;i<=n;i++) { ll val=a[i]; for(int j=M-1;j>=0;j--) if((val>>j)&1) { if(A[j]!=0) { val^=A[j]; continue; } A[j]=val; for(int k=j-1;k>=0;k--) if((A[j]>>k)&1) A[j]^=A[k]; for(int k=j+1;k<M;k++) if((A[k]>>j)&1) A[k]^=A[j]; break; } } } int n; ll a[N]; ll A[M]; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%lld",&a[i]); Linear_Basis(a,n,A); return 0; }
单步插入的线性基也是需要的,具体原因见第三个例题【其实这才是最常用的版本】
inline bool Insert(ll *A,ll x) { for(int i=M-1;i>=0;i--) if((x>>i)&1) { if(A[i]) { x^=A[i]; continue; } A[i]=x; for(int j=i-1;j>=0;j--) if((A[i]>>j)&1) A[i]^=A[j]; for(int j=i+1;j<M;j++) if((A[j]>>i)&1) A[j]^=A[i]; return true; } return false; }
假设求交/并的对象是两组线性基$A,B$
求线性基并,就是指求一组新的线性基$C$,使得任意能被$A,B$表示的数都能被$C$表示、且不能被$A,B$表示的数也不能被$C$表示
而求线性基交,是指求一组新的线性基$C$,使得任意能被$C$表示的数都能被$A,B$表示、且不能被$C$表示的数也不能被$A,B$表示
跟集合意义上的交/并有些差距
求并是比较简单的:把$A,B$全部往$C$中插入就行了
显然这样得出的线性基$C$能满足上面的要求
但是求交就不那么显然了
搜到的几篇讲的都跳了步骤,所以不如从头推一边
设线性基$A$所对应的线性空间为$V_A$(就是$A$能表示的所有数的集合),线性基$B$所对应的线性空间为$V_B$
令$W=V_A\cap B$(即$W$是线性基$B$中能被$A$表示的基向量的集合)
则有定理:若$A\cup (B\setminus W)$线性无关,那么$W$就是我们所想求的$A,B$的线性基交
这个定理的证明是比较容易的:
既然有$A\cup (B\setminus W)$线性无关,那么集合$B\setminus W$所能表示的数显然都不能被$A$表示(因为$A\cup (B\setminus W)$也构成一组线性基,而线性基所张成的线性空间中,每一个数只有唯一的表示)
而$W$中的基向量都能被$A$表示,$W$所能表示的数就更能被$A$表示了
于是根据定义,$W$恰为线性基交
但是我们不能保证对于线性空间$V_B$的任意一组线性基$B$,都有$A\cup (B\setminus W)$线性无关
比如,想对$A=\{5\},B=\{1,4\}$求线性基交,显然结果是$C=\{5\}$;但是$B$中的$1,4$都不能用$A$表示,且$1,4,5$线性相关
于是我们所需要做的,就是调整出一组线性基$B‘$(记$W‘=V_A\cap B‘$),使得$A\cup (B‘\setminus W‘)$线性无关
还是上面的例子,我们可以调整出$B‘=\{1,5\}$,这样一来线性空间$V_B$不变,同时能顺利求出线性基交$C$
这种调整是有规律可循的;简单点说,“调整”的过程就是在消除$B$对$B_i$的影响(等看完下面的过程就能理解这句话了)
我们可以动态地维护一个线性基$all$,表示尝试插入当前$B$中基向量后,$A\cup (B\setminus W)$所对应的线性基
我们依次将$B$中的基向量尝试插到$all$中(从高位到低位、从低位到高位皆可),假设当前插入基向量$B_i$
1. $B_i$能够插进$all$中;这意味着$B_i$与当前的$A\cup (B\setminus W)$线性无关,那么显然$B_i\notin W$,$B_i\in all$
2. $B_i$不能插进$all$中;这意味着$B_i$可以通过$A\cup \{B_0,B_1,...,B_{i-1}\}$表示,那么$B_i\notin all$,并需要进行调整、使得调整后的$B_i‘\in W$
(1) 若$B_i$只用$A$即可表示,那么无需调整,$B_i\in W$
(2) 若$B_i$需要$A$及$B_{p_1},B_{p_2},...,B_{p_m}$($p_j<i,B_{p_j}\in all$)才能表示,那么我们考虑$B_i‘=B_i\ xor\ B_{p_1}\ xor\ ...\ xor\ B_{p_m}$
显然,这时$B_i‘$只用$A$就可以表示了
同时,由于$B$是一组线性基,所以$B_i,B_i‘$均与$B$中其他基向量线性无关;那么$\{B_1,...,B_{i-1},B_i‘,B_{i+1},...,B_n\}$仍构成一组线性基
这样的调整过后,就有$B_i‘\in W$
具体是这样操作:
0. 先做好准备工作,我们在合并过程中需要一个额外的线性基$all$,初始时$all=A$;需要一个数组$BB$,初始时全$0$,表示$B_i‘$;需要一个变量$prod$,初始时为$0$,表示$B$整体对$B_i$的影响(有$B_i‘=B_i\ xor\ prod$)
1. 令$x=B_i$,尝试将$x$插入$all$,记当前的最高位$1$为第$j$位;若$all_j\neq 0$,那么$x=x\ xor\ all_j$,$prod=prod\ xor\ BB_j$;若$all_j=0$,则插入成功,$all_j=x$,$BB_j=B_i\ xor\ prod$
2. 若最终没有插入成功,那么将$B_i$调整后的值$B_i\ xor\ prod$插入结果线性基$W$中
代码如下(由于可能需要将$B_i‘$插入$W$,所以配有$Insert$函数)
总复杂度$O(m^2)$,其中$m$为线性基的最大秩
inline bool Insert(ui *A,ui x) { for(int i=M-1;i>=0;i--) if((x>>i)&1) if(A[i]) x^=A[i]; else { A[i]=x; return true; } return false; } ui all[M],BB[M]; inline void Merge(ui *A,ui *B,ui *W) { for(int i=0;i<M;i++) all[i]=A[i],BB[i]=0; for(int i=M-1;i>=0;i--) { bool flag=false; ui x=B[i],prod=0; for(int j=M-1;j>=0;j--) if((x>>j)&1) if(all[j]) { x^=all[j]; prod^=BB[j]; } else { flag=true; all[j]=x; BB[j]=B[i]^prod; break; } if(!flag && B[i]) Insert(W,B[i]^prod); } }
最基本的例题:HDU 3949 ($XOR$)
由于线性基能表示任意$a$的子集的xor值,所以只考虑从线性基$A$中选一个子集
想求第$k$小的xor值,关键就是要搞清楚怎么按照顺序选
先考虑最小值(这里需要特殊处理):若$A$的秩(即非$0$的$A_i$个数)小于$n$,则必存在$a$中的一个元素能被$a$中另外一些元素的xor值表示,那么最小值为$0$;否则最小值不为$0$
然后选第$k$小就可以考虑成从$A$中按二进制选择(比如第$10$小,二进制为$1010$,就是$A$中第$4$小xor$A$中第$2$小)
这样的正确性在于,若$i<j,A_i\neq 0,A_j\neq 0$,必有$A_0\ xor\ ...\ xor\ A_i<2^{i+1}\leq A_j$,所以选择$A_j$无论如何都比选择$A_0,...,A_i$中的子集更大
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=100005; const int M=60; void Linear_Basis(ll *a,int n,ll *A) { for(int i=1;i<=n;i++) { ll val=a[i]; for(int j=M-1;j>=0;j--) if((val>>j)&1) { if(A[j]!=0) { val^=A[j]; continue; } A[j]=val; for(int k=j-1;k>=0;k--) if((A[j]>>k)&1) A[j]^=A[k]; for(int k=j+1;k<M;k++) if((A[k]>>j)&1) A[k]^=A[j]; break; } } } int n; ll a[N]; ll A[M]; int main() { int T; scanf("%d",&T); for(int it=1;it<=T;it++) { memset(A,0LL,sizeof(A)); int n; scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%lld",&a[i]); Linear_Basis(a,n,A); vector<ll> v; for(int i=0;i<60;i++) if(A[i]) v.push_back(A[i]); printf("Case #%d:\n",it); int m=v.size(),q; scanf("%d",&q); while(q--) { ll x; scanf("%lld",&x); x=x-(m!=n); if((1LL<<m)-1<x) printf("-1\n"); else { ll ans=0; for(int i=0;i<m;i++) if((x>>i)&1) ans^=v[i]; printf("%lld\n",ans); } } } return 0; }
稍微进阶一点的题目:Luogu P3292 (辛运数字,$SCOI2016$)
既然是树上路径上的问题,倍增是一个很容易想到的方向
事实上,不仅可以通过倍增求出LCA,也可以通过倍增求出一点向根节点走$2^x$步时 路径上所有$G_i$所组成的线性基
两个线性基的合并是$O(60^2)$的,即把一组插入到另一组中
之后就是跟一般的倍增求LCA完全一样的处理了,只是多一个线性基而已
时间复杂度$O(nlogn\cdot 60^2+mlogn\cdot 60)$,空间是$O(nlogn\cdot 60)$
#include <cstdio> #include <vector> #include <cstring> using namespace std; typedef long long ll; const int N=20005; const int LOG=19; const int M=61; inline void Insert(ll *A,ll x) { for(int i=M-1;i>=0;i--) if((x>>i)&1) if(A[i]) x^=A[i]; else { A[i]=x; return; } } int n,m; ll a[N]; ll A[N][LOG][M]; vector<int> v[N]; int d[N],to[N][LOG]; inline void dfs(int x,int fa) { d[x]=d[fa]+1; to[x][0]=fa; Insert(A[x][0],a[x]); for(int i=0;i<v[x].size();i++) if(v[x][i]!=fa) dfs(v[x][i],x); } ll base[M]; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%lld",&a[i]); for(int i=1;i<n;i++) { int x,y; scanf("%d%d",&x,&y); v[x].push_back(y); v[y].push_back(x); } dfs(1,0); for(int i=1;i<LOG;i++) for(int j=1;j<=n;j++) { int top=to[j][i-1]; to[j][i]=to[top][i-1]; for(int k=0;k<M;k++) A[j][i][k]=A[j][i-1][k]; for(int k=0;k<M;k++) if(A[top][i-1][k]) Insert(A[j][i],A[top][i-1][k]); } while(m--) { int x,y; scanf("%d%d",&x,&y); if(d[x]<d[y]) swap(x,y); memset(base,0LL,sizeof(base)); for(int i=LOG-1;i>=0;i--) if(to[x][i] && d[to[x][i]]>=d[y]) { for(int j=0;j<M;j++) if(A[x][i][j]) Insert(base,A[x][i][j]); x=to[x][i]; } for(int i=LOG-1;i>=0;i--) if(to[x][i]!=to[y][i]) { for(int j=0;j<M;j++) { if(A[x][i][j]) Insert(base,A[x][i][j]); if(A[y][i][j]) Insert(base,A[y][i][j]); } x=to[x][i],y=to[y][i]; } if(x!=y) { Insert(base,a[x]); Insert(base,a[y]); Insert(base,a[to[x][0]]); } else Insert(base,a[x]); ll ans=0; for(int i=M-1;i>=0;i--) if((ans^base[i])>ans) ans^=base[i]; printf("%lld\n",ans); } return 0; }
这题有点把我整自闭...:牛客ACM 881H ($XOR$,$2019$牛客暑期多校第一场)
题解的思路十分优秀:求所有子集的大小之和,相当于对于每个元素,求包含该元素有多少集合
先把所有$a_i$整出一组线性基$A$,大小为$m$
对于所有不在线性基内的$n-m$个元素,若必选其中一个,则剩下的$n-m-1$个就可以任选,因为线性基总可以摆平所有情况;这样,对于这$n-m$个元素,共有$(n-m)\cdot 2^{n-m-1}$的贡献
而对于必选线性基内元素的情况,计算贡献就复杂一点了
1. 若这个元素能被剩余$n-1$个元素表示,那么就相当于存在另一组不包含该元素的$m$元线性基,则剩下的$n-m-1$可以任选;于是有$2^{n-m-1}$的贡献
2. 若这个元素不能被剩余$n-1$个元素表示,那么必选该元素时一定没有办法使整体xor值为$0$;于是贡献为$0$
我们可以对基外的$n-m$个元素再求一次线性基$B$,于是线性基$B$能代表那$n-m$个基外元素
那么想要判断是否存在不包含某基内元素$a_i$的另一组$m$元线性基,就相当于从$A$中移除$a_i$,并尝试加入$B$中的全部元素——这等效于对除了$a_i$外的全部元素做了一次线性基;得到一新线性基$C$
这时只要判断将$a_i$加入$C$后,$C$的秩是否增大即可
【我自己的坑点】
坑点1:将$A$、$B$、$C$中的线性基最高位独一化
其实这是毫无必要的,因为这题并不像上一题一样需要从小到大依次取出xor值;加了这一步骤会白白多一个$O(64)$的常数
坑点2:若能通过替换一个元素得到一组新的线性基,则相替换的两个元素在线性基中的位置相同
看起来很对,但实际上是个假结论!
在合成新的线性基时,应当先加入$B$中基,再加入$A$中基所对应的元素(不能直接加入$A$中的基,因为可能受到了被替换元素的影响)
但在这时,由于先加进来的是$B$中的基,所以可能导致$A$中对应某元素所在位置与$A$中不相同
比如自己找到的一个hack:
$3$
$7\ 4\ 4$
在$A$中,根据加入的先后顺序,$A_2$对应元素为$7$,$A_1$对应元素为$4$,$B_2$对应元素为$4$;去除$7$,加入$4$,则$C_2=4,C_1=0$——$A_2$被$B$中$4$占了,而$A$中$4$因此失去了$A_1$的位置
所以,碰到需要删除线性基的时候,不能总想玩骚的,还是老老实实写单步插入的比较好
希望花在这题上的时间不要白费...
#include <cstring> #include <cstdio> #include <vector> using namespace std; typedef long long ll; const int N=100005; const int M=60; const int MOD=1000000007; inline bool Insert(ll *A,ll x) { for(int i=M-1;i>=0;i--) if((x>>i)&1) { if(A[i]) { x^=A[i]; continue; } A[i]=x; /* for(int j=i-1;j>=0;j--) if((A[i]>>j)&1) A[i]^=A[j]; for(int j=i+1;j<M;j++) if((A[j]>>i)&1) A[j]^=A[i];*/ return true; } return false; } int n; ll a[N]; ll A[M],B[M],C[M]; bool vis[N]; vector<ll> v; inline ll quickpow(ll x,int t) { if(t<0) return 0; ll res=1; while(t) { if(t&1) res=res*x%MOD; x=x*x%MOD; t>>=1; } return res; } int main() { int n; while(~scanf("%d",&n)) { v.clear(); memset(A,0LL,sizeof(A)); memset(B,0LL,sizeof(B)); memset(vis,false,sizeof(vis)); for(int i=1;i<=n;i++) scanf("%lld",&a[i]); for(int i=1;i<=n;i++) if(Insert(A,a[i])) { vis[i]=true; v.push_back(a[i]); } for(int i=1;i<=n;i++) if(!vis[i]) Insert(B,a[i]); int m=v.size(),dlt=0; for(int i=0;i<v.size();i++) { for(int j=0;j<M;j++) C[j]=B[j]; for(int j=0;j<v.size();j++) if(j!=i) Insert(C,v[j]); if(Insert(C,v[i])) dlt++; } printf("%lld\n",quickpow(2,n-m-1)*(n-dlt)%MOD); } return 0; }
杭电多校的一题:HDU 6579 ($Operation$,$2019\ Multi-University\ Training\ Contest\ 1$)
用线段树维护区间线性基是不太可行的,因为单次查询是$O((logn)^2)$
题解给出的思路很秀:保存区间$[1,1],[1,2],...,[1,n]$的线性基,然后离线查询
不过保存的区间$[1,i]$的线性基不是随随便便哪一组都行的
当区间扩大、有一新元素加入时,线性基在两种情况下会被更新:
1. 线性基的某一位为$0$,这时和一般情况一样直接加入
2. 线性基的某一位不为$0$,但该基所对应元素在数组$a$中的下标小于当前元素的下标,那么就用当前元素(上三角消元后的值)作为基,并用被替换的基继续尝试更新线性基
这样一来,想要查询$a_l,...,a_r$中的最大xor值,就相当于用区间$[1,r]$的线性基中 对应元素在$a$中下标大于等于$l$ 的基求最大xor值
通过上述做法,能够使每个元素在不与下标更大的元素冲突的情况下 尽量作为位置最高的基,从而保证优先使下标靠后的元素作为基(需要脑补,有点绕orz...)
学习了
#include <cstdio> #include <cstring> #include <iostream> using namespace std; const int N=1000005; const int M=30; int a[N]; void Insert(int *A,int *pos,int x,int p) { for(int i=M-1;i>=0;i--) if((x>>i)&1) { if(A[i]) { if(p>pos[i]) { swap(A[i],x); swap(pos[i],p); } x^=A[i]; continue; } A[i]=x; pos[i]=p; /* for(int j=i-1;j>=0;j--) if((A[i]>>j)&1) A[i]^=A[j]; for(int j=i+1;j<M;j++) if((A[j]>>i)&1) A[j]^=A[i];*/ return; } } int n,m; int A[N][M]; int pos[N][M]; int main() { int T; scanf("%d",&T); while(T--) { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=n;i++) { for(int j=0;j<M;j++) { A[i][j]=A[i-1][j]; pos[i][j]=pos[i-1][j]; } Insert(A[i],pos[i],a[i],i); } int ans=0; while(m--) { int op,l,r,x; scanf("%d",&op); if(op==0) { scanf("%d%d",&l,&r); l=(l^ans)%n+1; r=(r^ans)%n+1; if(l>r) swap(l,r); ans=0; for(int i=M-1;i>=0;i--) if((ans^A[r][i])>ans && pos[r][i]>=l) ans^=A[r][i]; printf("%d\n",ans); } else { scanf("%d",&x); x^=ans; a[++n]=x; for(int i=0;i<M;i++) { A[n][i]=A[n-1][i]; pos[n][i]=pos[n-1][i]; } Insert(A[n],pos[n],x,n); } } } return 0; }
今年多校看来跟线性基杠上了,补了不少知识点
牛客ACM 884B ($xor$,$2019$牛客暑期多校第四场)
首先按线段树那样,对区间求线性基交,是$O(32^2\cdot n)$的
难道在查询的时候也需要像线段树一样合并查询区间吗?那样要合并$logn$个小区间,单次复杂度为$O(32^2\cdot logn)$
其实不需要!我们只需要在$logn$个小区间内,尝试插入要查询的值$x$即可,若不能插入则该区间的set能表示$x$
这样,单次复杂度降到了$O(32\cdot logn)$
这个操作十分巧妙,以后在特定题目中想对线段树的查询优化时,可以往这方面想
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef unsigned int ui; const int N=50005; const int M=32; inline bool Insert(ui *A,ui x,int type) { for(int i=M-1;i>=0;i--) if((x>>i)&1) if(A[i]) x^=A[i]; else { if(type) A[i]=x; return true; } return false; } ui all[M],BB[M]; inline void Merge(ui *A,ui *B,ui *W) { for(int i=0;i<M;i++) all[i]=A[i],BB[i]=0; for(int i=M-1;i>=0;i--) { bool flag=false; ui x=B[i],prod=0; for(int j=M-1;j>=0;j--) if((x>>j)&1) if(all[j]) { x^=all[j]; prod^=BB[j]; } else { flag=true; all[j]=x; BB[j]=B[i]^prod; break; } if(!flag && B[i]) Insert(W,B[i]^prod,1); } } int n,m; int sz=1; ui t[N<<2][M]; inline bool Query(int k,int l,int r,int a,int b,int x) { if(a>r || b<l) return true; if(a>=l && b<=r) return !Insert(t[k],x,0); int mid=(a+b)>>1; return Query(k<<1,l,r,a,mid,x) && Query(k<<1|1,l,r,mid+1,b,x); } int main() { scanf("%d%d",&n,&m); while(sz<n) sz<<=1; for(int i=1;i<=n;i++) { int size; ui x; scanf("%d",&size); for(int j=1;j<=size;j++) { scanf("%u",&x); Insert(t[i+sz-1],x,1); } } for(int i=sz-1;i>=1;i--) Merge(t[i<<1],t[i<<1|1],t[i]); while(m--) { int l,r; ui x; scanf("%d%d%u",&l,&r,&x); printf(Query(1,l,r,1,sz,x)?"YES\n":"NO\n"); } return 0; }
暂时感觉比较重要的内容都覆盖了?
如果遇见其他比较新颖的相关问题就继续在后面补充
(完)
原文:https://www.cnblogs.com/LiuRunky/p/Linear_Basis.html