线性基
本文仅发布于此博客和作者的洛谷博客,不允许任何人以任何形式转载,无论是否标明出处及作者。
线性基虽然是基于线性代数的算法,但是不通过线性代数理解也可以。
因为算法比较奇怪,下面的内容部分来自洛谷线性基题解。
线性基简介
线性基是一种擅长处理异或问题的数据结构.设值域为,就可以用一个长度为的数组来描述一个线性基。特别地,线性基第位上的数二进制下最高位也为第位。
一个线性基满足,对于它所表示的所有数的集合,中任意多个数异或所得的结果均能表示为线性基中的元素互相异或的结果,即意,线性基能使用异或运算来表示原数集使用异或运算能表示的所有数。运用这个性质,我们可以极大地缩小异或操作所需的查询次数。
前置芝士
对于集合,任选元素和,使,得。能通过相同的异或操作把自己还原回,称和等价。扩展一下可得,对进行任意次上述操作得到的,都与等价。
记中任意一或多个元素的异或和组成的集合为,相应定义为的异或和集合。因为上面的定义,能够通过异或操作把自己复原回,因此。
这个芝士并不会运用在线性基的构造和使用上,但是是线性基正确性得以保证的原因。
插入
我们考虑插入的操作,令插入的数为,考虑的二进制最高位,
若线性基的第位为,则直接在该位插入,退出;
若线性基的第位已经有值,则 ,重复以上操作直到.
如果退出时,则此时线性基已经可以表示原先的了;反之,则说明为了表示,往线性基中加入了一个新元素.
复杂度为.
判断
判断一个数是否能被原数列异或出来。
模仿插入的方法,如果最终可以插入成功就说明不能被异或出来,否则就可以。
当然,即使可以插入成功也不要插入。
复杂度同为.
查询异或最值
查询最小值相对比较简单。考虑插入的过程,因为每一次跳转操作,的二进制最高位必定单调降低,所以不可能插入两个二进制最高位相同的数。而此时,线性基中最小值异或上其他数,必定会增大。所以,直接输出线性基中的最小值即可。
考虑异或最大值,从高到低遍历线性基,考虑到第位时,如果当前的答案第位为,就将异或上;否则不做任何操作。显然,每次操作后答案不会变劣,最终的即为答案。
p.s. 同样,我们考虑对于一个数,它与原数列中的数异或的最值如何获得。用与序列异或最大值类似的贪心即可解决。
最小值,最大值,数与数列异或最大值也一样。
查询第k小值
这个东西比较麻烦。
先假设原序列无法异或出。
先考虑一个简化的情况,若线性基,尝试解决这个弱化版问题。
显然可以得到,和对应的第小值的图表,列在下方。
第小值 | |
---|---|
容易发现规律,直接对二进制分解并选取相应元素即可。
下面给出原因,本质上是一个贪心的过程。
考虑从到选取元素。设已经选出来的元素集合,现在要决定选不选.
如果选了,即使后面什么也不选了,也一定严格大于任何一种不选的情况。
所以,对进行二进制分解,就可以得到第小值的构造方案了。
考虑加强:仅保证线性基中的元素均为的正整数次方。
做法也易得,设表示中最小值,表示中第二小值,依此类推。这时不选取,而换为即可。例如时结果为。原因同上。
再次加强,仅保证“对于线性基中的每个元素,设其二进制最高位为,则线性基中的所有其他元素的第位均为”。
比如,下面这个就满足这个条件。
100000010000110
010000000000111
001000010001011
000100000001100
000010010001101
000001000000111
000000100001010
000000001000111
000000000100100
000000000010100
这种情况仍然可以用上面的方法,开个数组解决。原因是,“选某一元素以后的异或和一定严格大于不选某一元素以后的异或和”这一关键性质仍然可以满足。
到了这里,这个事情我们就可以做了。
我们把线性基进行一个简化,强制其满足上面的条件。
对于线性基中的每个数,仍设其最高位为,如果其他某个数的第位为,就异或上,把其第位消去。满足以上条件后,就可以按照上面的方法处理了。
另外,因为线性基的特点,只根据基中元素没有办法判断能不能异或出零。如果在最开始插入元素时,某个元素没有插入成功,就代表原数列可以异或出零。
如果可以异或出零,则原算法求出的第小值实为第小值。算法开始前就可以解决问题。
简化,贪心。
查询第k大值
记线性基内元素数量为,根据排列组合,一共能异或出(如果可以异或出,就是)个值。于是,第大值即为第(或)小值。done.
复杂度同上。
代码贴在下面,每个操作都挺简短的。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int base[64];//线性基
bool Zero_Compound=false;//能不能异或出0
//注意,位运算的优先级很奇怪,最好把位运算有关的全加上括号不然能给你WA到崩溃
bool insert(int x){//插入元素
for(int i=62;i>=0;i--){
if((x&(1ll<<i)) != 0){//如果当前位有地方,插入
if(base[i]==0){
base[i]=x;
return true;
}else{//异或再找地方
x^=base[i];
}
}
}
return false;//已经能被线性基内元素异或出来,插入失败
}
bool check(int x){//x能不能够被线性基中的数表表示出来
for(int i=62;i>=0;i--){
if((x&(1ll<<i)) != 0){
if(base[i]==0){
return false;
}else{
x^=base[i];
}
}
}
return true;
}
void ease(){//简化线性基
for(int i=0;i<=62;i++){
if(base[i]==0){
continue;
}
for(int j=i+1;j<=62;j++){
if((base[j]&(1ll<<i)) != 0){//如果j的第i位是1,清成0
base[j]^=base[i];
}
}
}
}
int minrk(int k){//k小数
if(Zero_Compound){//如果能合成0,k--
k--;
if(k==0){//如果k=0了,要找的就是0
return 0;
}
}
int ans=0;
ease();//简化
vector<int> tmp;//d数组,拿vector会比较方便
for(int i=0;i<=62;i++){//一定是从小往大!
if(base[i]){
tmp.push_back(base[i]);
}
}
if(k>=pow(2,tmp.size())){//超过限度,根本没有k小值
return -1;//ERR
}
for(int i=62;i>=0;i--){//贪心
if((k&(1ll<<i)) != 0){
ans^=tmp[i];
}
}
return ans;
}
int maxrk(int k){//k大值
int size=0;
for(int i=0;i<=62;i++){
if(base[i]){
size++;
}
}
return minrk(pow(2,size)-k+(Zero_Compound?1:0));
}
int max_(){//最大值,也是一个贪心
int ans=0;
for(int i=62;i>=0;i--){
if((base[i]!=0) && ((ans^base[i])>ans)){
ans^=base[i];
}
}
return ans;
}
int min_(){//最小值
if(Zero_Compound){//如果能合成出0肯定是0
return 0;
}
for(int i=0;i<=62;i++){//不然就是线性基中的最小值
if(base[i]!=0){
return base[i];
}
}
return 114514;
}
int nummax(int x){//数和数列异或最大值,和max_唯一的区别就是ans变成了非0的x
for(int i=62;i>=0;i--){
if((base[i]!=0) && ((x^base[i])>x)){
x^=base[i];
}
}
return x;
}
signed main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
int opt;
cin>>opt;
if(opt==1){
int x;
cin>>x;
bool flag=insert(x);
if(!flag){
Zero_Compound=true;
}
}
if(opt==2){
int x;
cin>>x;
cout<<(check(x)?"true":"false")<<endl;
}
if(opt==3){
int x;
cin>>x;
cout<<nummax(x)<<endl;
}
if(opt==4){
int x;
cin>>x;
cout<<minrk(x)<<endl;
}
if(opt==5){
int x;
cin>>x;
cout<<maxrk(x)<<endl;
}
if(opt==6){
cout<<min_()<<endl;
}
if(opt==7){
cout<<max_()<<endl;
}
}
}
例题
两个可爱的例题/se
P3857 [TJOI2008]彩灯 板子,不作任何提示。
P3292 [SCOI2016]幸运数字 需要知道线性基是可合并,可重复贡献的。暴力合并即可,方法不作提示。另外,题目tag里有一个倍增。倍增要求什么是显然的。
题解
简单说一下思路。
对于第一题,可以对题面换一种表述方式:
给出个数,可以随便选任意个数(可以为),得到它们的异或和。求可以得到多少值不同的异或和。
把所有数插到线性基里面,答案为,为线性基里元素的个数。这里不需考虑能不能异或出,因为可以一个数都不选。
第二题有两个解法。
解法1:
在维护求LCA的倍增数组f[i][j]
(意为的级祖先)的同时,维护“从到它的级祖先的这条路径上所有点的线性基”,记为base[i][j]
。维护方法是把base[i][j-1]
和base[i+f[i][j-1]][j-1]
合并在一起。另外维护点的深度,询问时会用。
对于每次询问的,,先找到它们的LCA,把LCA到和两条路径拆开看。可以使用类似ST表的方法,找到满足比LCA深的最大的,合并base[x][k]
和base[x的dep[x]-dep[LCA]+1-pow(2,k)级祖先][k]
即可得到路径线性基。对于同理,最后把两条路径的线性基再合并,即可得到到的线性基。查询最大值即可。
时间复杂度,但是时限宽,卡卡常应该能过。
前缀线性基
这里再介绍一种维护序列中区间异或线性基的方法,即解法2.
我们把插入线性基的逻辑进行修改:额外维护线性基中的每个数在原序列中的位置。设当前需要在线性基处插入,但是非,而且,就把和交换(同时交换id,即把拿出来换成),继续往下插入拿出来的。
初始化先得到序列的前缀线性基。设询问区间的线性基,我们就找到包含区间中所有元素的线性基,删掉里面所有的元素,即可得到的线性基。
正确性的证明是,对于的线性基中的任意元素,在插入线性基中的时候只和的元素异或过,所以对于的元素组成的集合,其中的每个元素都可以和集合内的元素异或得到原序列中相应的元素,所以此挑选出的集合即为区间的线性基。
初始化,询问区间每次。
解法2:树上前缀线性基,维护出每个点到根的前缀线性基,求LCA后分别得到LCA到和到的区间线性基,再合并,查询,异或最大值即可。时间复杂度,稳过。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】