线性基
本文仅发布于此博客和作者的洛谷博客,不允许任何人以任何形式转载,无论是否标明出处及作者。
线性基虽然是基于线性代数的算法,但是不通过线性代数理解也可以。
因为算法比较奇怪,下面的内容部分来自洛谷线性基题解。
线性基简介
线性基是一种擅长处理异或问题的数据结构.设值域为\([1,N]\),就可以用一个长度为\(\lceil \log_2N \rceil\)的数组来描述一个线性基。特别地,线性基第\(i\)位上的数二进制下最高位也为第\(i\)位。
一个线性基满足,对于它所表示的所有数的集合\(S\),\(S\)中任意多个数异或所得的结果均能表示为线性基中的元素互相异或的结果,即意,线性基能使用异或运算来表示原数集使用异或运算能表示的所有数。运用这个性质,我们可以极大地缩小异或操作所需的查询次数。
前置芝士
对于集合\(S\),任选元素\(a\)和\(b\),使\(a\leftarrow a\oplus b\),得\(S'\)。\(S'\)能通过相同的异或操作把自己还原回\(S\),称\(S\)和\(S'\)等价。扩展一下可得,对\(S\)进行任意次上述操作得到的\(S'\),都与\(S\)等价。
记\(S\)中任意一或多个元素的异或和组成的集合为\(T\),相应定义\(T'\)为\(S'\)的异或和集合。因为上面的定义,\(S'\)能够通过异或操作把自己复原回\(S\),因此\(T'=T\)。
这个芝士并不会运用在线性基的构造和使用上,但是是线性基正确性得以保证的原因。
插入
我们考虑插入的操作,令插入的数为\(x\),考虑\(x\)的二进制最高位\(i\),
若线性基的第\(i\)位为\(0\),则直接在该位插入\(x\),退出;
若线性基的第\(i\)位已经有值\(a_i\),则\(x = x\oplus a_i\) ,重复以上操作直到\(x=0\).
如果退出时\(x=0\),则此时线性基已经可以表示原先的\(x\)了;反之,则说明为了表示\(x\),往线性基中加入了一个新元素.
复杂度为\(O(\log N)\).
判断
判断一个数\(x\)是否能被原数列异或出来。
模仿插入的方法,如果最终可以插入成功就说明不能被异或出来,否则就可以。
当然,即使可以插入成功也不要插入。
复杂度同为\(O(\log N)\).
查询异或最值
查询最小值相对比较简单。考虑插入的过程,因为每一次跳转操作,\(x\)的二进制最高位必定单调降低,所以不可能插入两个二进制最高位相同的数。而此时,线性基中最小值异或上其他数,必定会增大。所以,直接输出线性基中的最小值即可。
考虑异或最大值,从高到低遍历线性基,考虑到第\(i\)位时,如果当前的答案\(x\)第\(i\)位为\(0\),就将\(x\)异或上\(a_i\);否则不做任何操作。显然,每次操作后答案不会变劣,最终的\(x\)即为答案。
p.s. 同样,我们考虑对于一个数\(x\),它与原数列中的数异或的最值如何获得。用与序列异或最大值类似的贪心即可解决。
最小值\(O(1)\),最大值\(O(\log N)\),数与数列异或最大值也一样。
查询第k小值
这个东西比较麻烦。
先假设原序列无法异或出\(0\)。
先考虑一个简化的情况,若线性基\(b=\{2^0,2^1,\cdots,2^x\}\),尝试解决这个弱化版问题。
显然可以得到,\(k\)和对应的第\(k\)小值的图表,列在下方。
| \(k\) | 第\(k\)小值 |
|---|---|
| \(1=(001)_2\) | \(2^0\) |
| \(2=(010)_2\) | \(2^1\) |
| \(3=(011)_2\) | \(2^1\oplus2^0\) |
| \(4=(100)_2\) | \(2^2\) |
| \(5=(101)_2\) | \(2^2\oplus2^0\) |
| \(6=(110)_2\) | \(2^2\oplus2^1\) |
| \(7=(111)_2\) | \(2^2\oplus2^1\oplus2^0\) |
容易发现规律,直接对\(k\)二进制分解并选取相应元素即可。
下面给出原因,本质上是一个贪心的过程。
考虑从\(2^x\)到\(2^0\)选取元素。设已经选出来的元素集合\(S\),现在要决定选不选\(2^k\).
如果选了\(2^k\),即使后面什么也不选了,也一定严格大于任何一种不选\(2^k\)的情况。
所以,对\(k\)进行二进制分解,就可以得到第\(k\)小值的构造方案了。
考虑加强:仅保证线性基中的元素均为\(2\)的正整数次方。
做法也易得,设\(d_0\)表示\(b\)中最小值,\(d_1\)表示\(b\)中第二小值,依此类推。这时不选取\(2^k\),而换为\(d_k\)即可。例如\(k=5\)时结果为\(d_2\oplus d_0\)。原因同上。
再次加强,仅保证“对于线性基中的每个元素,设其二进制最高位为\(k\),则线性基中的所有其他元素的第\(k\)位均为\(0\)”。
比如,下面这个就满足这个条件。
100000010000110
010000000000111
001000010001011
000100000001100
000010010001101
000001000000111
000000100001010
000000001000111
000000000100100
000000000010100
这种情况仍然可以用上面的方法,开个\(d\)数组解决。原因是,“选某一元素以后的异或和一定严格大于不选某一元素以后的异或和”这一关键性质仍然可以满足。
到了这里,这个事情我们就可以做了。
我们把线性基进行一个简化,强制其满足上面的条件。
对于线性基中的每个数\(i\),仍设其最高位为\(k\),如果其他某个数\(j\)的第\(k\)位为\(1\),\(j\)就异或上\(i\),把其第\(k\)位消去。满足以上条件后,就可以按照上面的方法处理了。
另外,因为线性基的特点,只根据基中元素没有办法判断能不能异或出零。如果在最开始插入元素时,某个元素没有插入成功,就代表原数列可以异或出零。
如果可以异或出零,则原算法求出的第\(k\)小值实为第\(k+1\)小值。算法开始前\(k--\)就可以解决问题。
简化\(O(\log^2 N)\),贪心\(O(\log N)\)。
查询第k大值
记线性基内元素数量为\(m\),根据排列组合,一共能异或出\(2^m-1\)(如果可以异或出\(0\),就是\(2^m\))个值。于是,第\(k\)大值即为第\(2^m-k\)(或\(2^m-k+1\))小值。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里有一个倍增。倍增要求什么是显然的。
题解
简单说一下思路。
对于第一题,可以对题面换一种表述方式:
给出\(n\)个数,可以随便选任意个数(可以为\(0\)),得到它们的异或和。求可以得到多少值不同的异或和。
把所有数插到线性基里面,答案为\(2^{size}\),\(size\)为线性基里元素的个数。这里不需考虑能不能异或出\(0\),因为可以一个数都不选。
第二题有两个解法。
解法1:
在维护求LCA的倍增数组f[i][j](意为\(i\)的\(2^j\)级祖先)的同时,维护“从\(i\)到它的\(2^j\)级祖先的这条路径上所有点的线性基”,记为base[i][j]。维护方法是把base[i][j-1]和base[i+f[i][j-1]][j-1]合并在一起。另外维护点的深度\(dep\),询问时会用。
对于每次询问的\(x\),\(y\),先找到它们的LCA,把LCA到\(x\)和\(y\)两条路径拆开看。可以使用类似ST表的方法,找到满足\(f[x][k]\)比LCA深的最大的\(k\),合并base[x][k]和base[x的dep[x]-dep[LCA]+1-pow(2,k)级祖先][k]即可得到路径线性基。对于\(y\)同理,最后把两条路径的线性基再合并,即可得到\(x\)到\(y\)的线性基。查询最大值即可。
时间复杂度\(O(n\log n\log^2 G+q(\log n+\log^2 G))\),但是时限宽,卡卡常应该能过。
前缀线性基
这里再介绍一种维护序列中区间异或线性基的方法,即解法2.
我们把插入线性基的逻辑进行修改:额外维护线性基中的每个数在原序列中的位置\(id\)。设当前需要在线性基\(i\)处插入\(x\),但是\(base_i\)非\(0\),而且\(id_x>id_{base_i}\),就把\(x\)和\(base_i\)交换(同时交换id,即把\(base_i\)拿出来换成\(x\)),继续往下插入拿出来的\(base_i\)。
初始化先得到序列的前缀线性基。设询问区间\([l,r]\)的线性基,我们就找到包含区间\([1,r]\)中所有元素的线性基,删掉里面所有\(id<l\)的元素,即可得到\([l,r]\)的线性基。
正确性的证明是,对于\([1,r]\)的线性基\(base\)中的任意元素\(x\),\(x\)在插入线性基中的时候只和\(id>id_x\)的元素异或过,所以对于\(id\geq l\)的元素组成的集合,其中的每个元素都可以和集合内的元素异或得到原序列\(a\)中相应的元素,所以此挑选出的集合即为区间\([l,r]\)的线性基。
初始化\(n\log k\),询问区间每次\(\log k\)。
解法2:树上前缀线性基,维护出每个点到根的前缀线性基,求LCA后分别得到LCA到\(x\)和到\(y\)的区间线性基,再合并,查询,异或最大值即可。时间复杂度\(O(n\log n\log G+q(\log n+\log^2 G))\),稳过。