线性基
类似于立体空间向量,只要我们在空间中找到一组基向量i,j,k,以后空间中任何向量都可以用它们表示。
三维欧氏空间是特殊的线性空间,三维欧氏空间的基向量在线性空间中就被推广为了线性基。
称线性空间 V 的一个极大线性无关组为 V 的一组 Hamel 基 或 线性基,简称 基。
(好的以上概念纯扯淡)
我们从实用角度去理解线性基。线性基分为实数线性基和异或线性基,异或线性基更常用。以下均以异或线性基为例。
性质
1.线性基存储的是一个数的集合 \(S\) 的信息,满足 \(S\) 中任意多个数异或所得的结果均能表示为线性基中元素相互异或的结果。且线性基是能保存数集的情况下,其中存储元素最少的。
2.线性基第 \(i\) 位上的数二进制下最高位也为第 \(i\) 位。
3.设一个数集的最大值为 \(M\),则就可以用一个长度为 \(log_2 M\) 的数组来描述一个线性基。
插入
插入 \(x\),令 \(x\) 的二进制最高位为 \(i\)。
-
若线性基的第 \(i\) 位为0,则直接在该位插入 \(x\) ,\(break\) ;
-
若线性基的第 \(i\) 位已经有 \(a_i\) ,则 \(x=x \oplus a_i\),继续寻找 \(x\) 的最高位重复以上操作。
知道了线性基的实现后,我们就可以简单伪证下它的性质:
1.考虑对于每个 \(x\) ,如果它首次就插入成功,则显然线性基中各元素异或结果能表示原数组中各元素相互异或的结果。只考虑没有一下插入成功的,则它会在插入途中异或上一些数,变为 \(x\oplus a_1\oplus a_3\oplus a_7...\) (例子随便举的)。
而我们又知道,它插入之前,集合中各元素异或结果就能表示了。则只需要考虑插入它后能否表示它和不同的数的异或和。一定是可以的。给它异或如 \(a_1,a_3,a_7\) 这样的数会异或相消,异或另一些 \(a_2,a_4,a_5...\) ,就会给它增加异或的不同的数。
同时我们考虑,如果当前数本来就能被异或集合表示出来,则它一定会插入失败,因为一路走来都被抵消了。这也是判断它们能不能表示一个数的方法。
再思考一个问题,如果一个长为 \(n\) 的线性基每一位都有值,则它不可能再被插入值了,那么它能表示所有 \(2^n\) 种结果吗?由于线性基内存的各个数的异或和一定不同,所以就能表示 \(C_n^0+C_n^1+C_n^2+...+C_n^n=2^n\) 种情况。
2、3直接根据插入方式显然。
void add(int x){
for(int i=50;i>=0;i--){
if((x>>i)&1){
if(!a[i]){
a[i]=x;
break;
}
x^=a[i];
}
}
}
线性基合并
显然,根据线性基的定义,它有可合并性。只需要把一个线性基里面的东西插入另一个就行了,复杂度 \(O(log^2V)\)
求异或和最值
max:从高位开始贪心。由于线性基第 \(i\) 位上的数二进制下最高位也为第 \(i\) 位,不妨直接按位异或判断。如果异或当前位变大了,就直接异或。显然,如果异或当前位变大,则异或和的当前位一定是由0变1,而往后异或不可能再改变当前位的值,故贪心得证。
int solve(){
int ans=0;
for(int i=50;i>=0;i--){
if((ans^a[i])>ans) ans^=a[i];
}
return ans;
}
min:如果出现插入失败,最小异或和就是0。否则,实际上就是线性基最低位的元素(它前面没有元素,且它的最高位最小)。
求排名
求一个数x的排名(这里要求要在线性基内出现过),即把它按位拆解下来,若在遇到一个1,设线性基当前位之前有元素的位的个数为 \(i\),就给它的排名加上 \(2^i\) 。
考虑前面只要有元素的位就可以两两组合共有 \(2^i\) 种情况,而当前这个含1的位 \(j\) 代表了 \(2^j\) ,必然大于它们。(这里直接加 \(2^i\) ,实际上相当于把 0 的情况先不考虑,-1,然后对比它小的元素+1才是它的排名)
另外,对于可重排名,每一个重复元素会对它的排名产生 \(\times 2\) 的效果。
albus就是要第一个出场
#include<bits/stdc++.h>
using namespace std;
const int mod=10086;
int n,a[105],q,d[105];
void add(int x){
int fl=1;
for(int i=31;i>=0;i--){
if((x>>i)&1){
if(!a[i]){
a[i]=x;
d[i]++,fl=0;
break;
}
x^=a[i];
}
}
if(fl) d[0]++;
}
int qpow(int x,int y){
int res=1;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
int solve(int x){
int ans=0;
for(int i=0;i<=31;i++){
ans+=d[i];
d[i]=ans;
}
ans=0;
for(int i=31;i>=0;i--){
if((x>>i)&1){
if(a[i]) ans+=qpow(2,d[i]-1);
}
}
return ans+1;
}
int main(){
scanf("%d",&n);
for(int i=1,x;i<=n;i++){
scanf("%d",&x);
add(x);
}
scanf("%d",&q);
printf("%d",solve(q)%mod);
return 0;
}
求第k大/小异或和
以第k小为例。对于线性基中第i个有值的位置,比它小的i-1个数无论如何异或和都小于它,且它们最多异或得 \(2^{i-1}-1\) 个数(不考虑0)。那么当查询的排名大于 \(2^{i-1}-1\) 时,我们肯定要异或上这个值。
那么我们就可以将k拆分二进制,从高位枚举,找到每个1所在的位置,并给答案异或上第i个有值的二进制位。
但问题来了,当答案异或了一个值x时,比x小的数应该按异或x后的值排序,有的数最高位可能改变。问题的根源是,对于一个i最高位的1,它并不是唯一一个在这一位存在1的元素。
线性基不是唯一的。考虑一种构造,使对于 \(i\), \(a_i\) 是数组中唯一一个在 \(i\) 位上有1的数。这样它即使前面需要被异或,也不会影响。构造过程就是从高到低枚举数,如果这一位为1,就异或它。(见代码
注意是查询之前先重构,特判0的情况。复杂度 \(O(log^2 V)\) 。
void rebuild(){
for(int i=0;i<=30;i++){
for(int j=i-1;j>=0;j--){
if((a[i]>>j)&1) a[i]^=a[j];
}
}
cnt=0;
for(int i=0;i<=30;i++){
if(a[i]) c[++cnt]=a[i];
}
}
int kth(int k){
if(cnt<n) k--;//含0
if((1ll<<cnt)<=k) return -1;//线性基总数不够
rebuild();
int res=0,pos=1;
while(k){
if(k&1) res^=c[pos];
pos++;
k>>=1;
}
return res;
}
前缀线性基
奇技淫巧罢了。
考虑从左往右建立线性基,需要多记录一个pos表示该元素何时被插入。我们查询的时候只需要找到 \(pos_i\geq l\) 的位置即可。
建立时,每次都在前一次的基础上,如果插入当前位置有数,就交换pos,使位置上记录的尽量是靠后的pos,这样能保证在查询[l,r]时尽可能包含较多的数。
这个需要离线查询。
void insert(ll x,int p){
for(int i=61;i>=0;i--){
if((x>>i)&1){
if(!a[i]){
a[i]=x,pos[i]=p;
break;
}
else if(pos[i]<p){
swap(a[i],x),swap(pos[i],p);
}//还要继续插入他俩的异或和,保证严谨性
x^=a[i];
}
}
}
图上线性基
经典例题:[WC2011] 最大XOR和路径
经典手法:发现如果走过去再走回来,异或和一定会抵消,因此答案一定由一条简单路径和若干环的异或和构成。
那么不妨直接通过跑dfs树,得到环,把环放到线性基里面。然后在线性基里查询每条简单路径能达到的最大异或和。
例题
[bzoj3811] 玛里苟斯
有转化为组合意义的写法,这里提供另一种思路。
难以观察到,最终答案小于 \(2^{63}\) ,又要求 \(k\) 次方,则在 \(k>2\) 时有线性基元素个数 \(n<22\) ,暴力 \(O(2^n)\) 即可。剩下逐一考虑。
\(k=1\) ,手玩几组数据发现,把所有数拆成二进制后,答案的第i位有可能为1当且仅当原数组的第i位有1。在这种情况下,答案第i位为1的概率为 \(\frac{1}{2}\) 。这个概率就等价于选到奇数个1的概率。证明不妨考虑:一共有k个1,取k-1个1生成 \(2^{k-1}\) 个不同取1的方案,对第k个1,如果前面为偶数个1就不选,奇数个1就选,这样会有 \(2^{k-1}\) 种情况是有1的贡献的,再除以总情况数,就是 \(\frac{1}{2}\) 的概率了。
这种情况下,过程中计算结果最多达到 \(2^{64}-1\) ,需要开ull。
\(k=2\) ,考虑先把二进制的每一位拆出来,令 \(x_i\) 表示每一位为1或0,然后化简 \((\sum_{i=0}^{n-1} x_i \times 2^i)^2=\sum_{i=0}^{n-1} (x_i\times 2^i)^2+\sum_{i=0}^{n-1}\sum_{j=i+1}^{n-1}2\times x_ix_j\times 2^{i+j}\)
平方项有贡献的概率其实就是 \(x_i\) 为1的概率,就是 \(\frac{1}{2}\) ;
形如 \(x_ix_j\) 的项有贡献的概率是 \(\frac{1}{2}\times \frac{1}{2}=\frac{1}{4}\) 。
但没想到吧,那样直接计算 \(x_ix_j\) 的概率是错误的。如果对于每个 \(a_i\) ,它的第 \(i,j\) 位不是同时为0就是同时为1,那么相当于 \(i,j\) 的选择也是同时的,这种情况下它有贡献的概率是 \(\frac{1}{2}\) ,特判。
注意 \(k>2\) 的情况,只保证了答案在 \(2^{63}\) 次方以内,但计算时很可能超过这个值,不妨直接在计算时给它除掉总情况数,不能整除时维护余数。
即,设线性基元素有 \(m\) 个,把答案记录为 \(ans=\lfloor \frac{y}{2^m} \rfloor\) , \(y=ans*2^m+y \mod 2^m\)
对于输出格式,由于异或和最后一位为1的概率是 \(\frac{1}{2}\) ,所以结果要么为整数,要么多0.5。
//一定要注意过程中有没有炸ll,炸ull的。。
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
using namespace std;
const int maxn=1e5+5;
int n,k,vis[maxn],b[maxn],t[maxn],cnt;
ull a[maxn],r,ans,mod;
void solve1(){
ull ans=0;
for(int i=1;i<=n;i++) ans|=a[i];
if(ans&1) printf("%llu.5",ans/2);
else printf("%llu",ans/2);
}
bool check(int x,int y){
for(int i=1;i<=n;i++){
if(((a[i]>>x)&1)&&!((a[i]>>y)&1)) return 0;
if(((a[i]>>y)&1)&&!((a[i]>>x)&1)) return 0;
}
return 1;
}
void solve2(){
for(int i=1;i<=n;i++){
for(int j=0;j<63;j++){
if((a[i]>>j)&1) vis[j]=1;
}
}
for(int i=0;i<33;i++){
if(vis[i]) ans+=(1llu<<(i+i));
}
for(int i=0;i<33;i++){
for(int j=i+1;j<33;j++){
if(vis[i]&&vis[j]){
ans+=(1llu<<(i+j));
if(check(i,j)) ans+=(1llu<<(i+j));
}
}
}
if(ans&1) printf("%llu.5",ans/2);
else printf("%llu",ans/2);
}
void add(int x){
for(int i=22;i>=0;i--){
if((x>>i)&1){
if(!b[i]){
b[i]=x;
break;
}
else x^=b[i];
}
}
}
void calc(int x){
ull tx=x/mod,tr=x%mod;
for(int i=1;i<k;i++){
tx*=x,tr*=x;
tx+=tr/mod;
tr%=mod;
}
// printf("Res: %llu\n",res);
ans+=tx,r=tr+r;
ans+=r/mod,r%=mod;
}
void dfs(int x,int sum){
if(x==cnt+1){
calc(sum);
return ;
}
dfs(x+1,sum);
dfs(x+1,sum^t[x]);
}
void solve3(){
for(int i=1;i<=n;i++) add(a[i]);
for(int i=22;i>=0;i--) if(b[i]) t[++cnt]=b[i];
// printf("%d\n",cnt);
mod=(1<<cnt);
dfs(1,0);
if(r) printf("%llu.5",ans);
else printf("%llu",ans);
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++){
scanf("%llu",&a[i]);
}
if(k==1) solve1();
else if(k==2) solve2();
else solve3();
return 0;
}
[bzoj3569] DZY Loves Chinese II
如果不强制在线,可以线段树分治,但奈何时间复杂度过高。
正解是奇妙构造。(类似于这种多组询问图的连通性的问题好像都是hash构造,比如 [CSP-S 2022] 星战。)
考虑若一个图不联通,则必然会被分割成两个块且其间没有相连的边。如果这两个块之间相连的边集为 \(E\) ,询问边集为 \(A\) ,则如果 \(E\in A\) ,该图不联通。
不妨考虑随机异或值哈希,如果构造出边的权值,使得 \(E\) 中的异或和为0,则只用在线性基上查找询问边集中有没有异或和为0的组合就行。(这里必须使得 \(E\) 中异或和为0,否则就成了询问边集的子集要和原图中某个边集异或和为0,复杂度爆炸)
考虑随便对图跑一个dfs树,若要断成两个块,则必须至少断掉一个树边,且穿过该树边的非树边也要断了。不妨直接给非树边随便赋权值,树边权值为穿过它的非树边的权值异或和。最后在线性基上判断是否有一个组合能使异或和为0。
操作的时候直接给当前边赋值为它所有儿子的边权异或和,如果为非树边,则正是我们所需要的;如果为树边,它的边权也为穿过它的非树边的异或和,若有穿过它但没穿过当前边的非树边,会在异或时抵消。(画几个图看看就出来了)
[bzoj4184] shallot
听说线性基好像可以处理删除操作,但要记录一堆神仙玩意,没太看懂。
这个题因为可以离线,故使用线段树分治即可。太卡空间。
[BeiJing2011] 梦想封印
仔细想想好像也不难,但是死活没意识到线性基最多插入log次,可以暴力重构。
总体就是套路的叠加。删边改加边,线性基图上路径长问题,分别维护链和环,然后线性基插入成功最多log次,可以暴力重构。
我不会的点在于如何维护路径的不同值?这里用set维护对于每条路经,它和环的线性基的异或最小值。这里就用到线性基的不唯一性,不妨固定一个统一标准,都把路径权值搞成最小值,就能比较出来有没有重复的。
[SCOI2016] 幸运数字
线性基+树剖+线段树的维护看起来很炸裂,至少得4个log吧。
如果倍增就能3个log,差不多能过。
还有更优解,是双log的,就是用线性基维护树上前缀和的做法。就是前缀线性基,然后每次判断dep和pos的关系去操作。

浙公网安备 33010602011771号