线性基

本文仅发布于此博客和作者的洛谷博客,不允许任何人以任何形式转载,无论是否标明出处及作者。


线性基虽然是基于线性代数的算法,但是不通过线性代数理解也可以。

因为算法比较奇怪,下面的内容部分来自洛谷线性基题解。

线性基简介

线性基是一种擅长处理异或问题的数据结构.设值域为[1,N],就可以用一个长度为log2N的数组来描述一个线性基。特别地,线性基第i位上的数二进制下最高位也为第i位。

一个线性基满足,对于它所表示的所有数的集合SS中任意多个数异或所得的结果均能表示为线性基中的元素互相异或的结果,即意,线性基能使用异或运算来表示原数集使用异或运算能表示的所有数。运用这个性质,我们可以极大地缩小异或操作所需的查询次数。

前置芝士

对于集合S,任选元素ab,使aab,得SS能通过相同的异或操作把自己还原回S,称SS等价。扩展一下可得,对S进行任意次上述操作得到的S,都与S等价。

S中任意一或多个元素的异或和组成的集合为T,相应定义TS的异或和集合。因为上面的定义,S能够通过异或操作把自己复原回S,因此T=T

这个芝士并不会运用在线性基的构造和使用上,但是是线性基正确性得以保证的原因。

插入

我们考虑插入的操作,令插入的数为x,考虑x的二进制最高位i

若线性基的第i位为0,则直接在该位插入x,退出;

若线性基的第i位已经有值ai,则x=xai ,重复以上操作直到x=0.

如果退出时x=0,则此时线性基已经可以表示原先的x了;反之,则说明为了表示x,往线性基中加入了一个新元素.

复杂度为O(logN).

判断

判断一个数x是否能被原数列异或出来。

模仿插入的方法,如果最终可以插入成功就说明不能被异或出来,否则就可以。

当然,即使可以插入成功也不要插入。

复杂度同为O(logN).

查询异或最值

查询最小值相对比较简单。考虑插入的过程,因为每一次跳转操作,x的二进制最高位必定单调降低,所以不可能插入两个二进制最高位相同的数。而此时,线性基中最小值异或上其他数,必定会增大。所以,直接输出线性基中的最小值即可。

考虑异或最大值,从高到低遍历线性基,考虑到第i位时,如果当前的答案xi位为0,就将x异或上ai;否则不做任何操作。显然,每次操作后答案不会变劣,最终的x即为答案。

p.s. 同样,我们考虑对于一个数x,它与原数列中的数异或的最值如何获得。用与序列异或最大值类似的贪心即可解决。

最小值O(1),最大值O(logN),数与数列异或最大值也一样。

查询第k小值

这个东西比较麻烦。

先假设原序列无法异或出0

先考虑一个简化的情况,若线性基b={20,21,,2x},尝试解决这个弱化版问题。

显然可以得到,k和对应的第k小值的图表,列在下方。

k k小值
1=(001)2 20
2=(010)2 21
3=(011)2 2120
4=(100)2 22
5=(101)2 2220
6=(110)2 2221
7=(111)2 222120

容易发现规律,直接对k二进制分解并选取相应元素即可。

下面给出原因,本质上是一个贪心的过程。


考虑从2x20选取元素。设已经选出来的元素集合S,现在要决定选不选2k.

如果选了2k,即使后面什么也不选了,也一定严格大于任何一种不选2k的情况。

所以,对k进行二进制分解,就可以得到第k小值的构造方案了。


考虑加强:仅保证线性基中的元素均为2的正整数次方。

做法也易得,设d0表示b中最小值,d1表示b中第二小值,依此类推。这时不选取2k,而换为dk即可。例如k=5时结果为d2d0。原因同上。

再次加强,仅保证“对于线性基中的每个元素,设其二进制最高位为k,则线性基中的所有其他元素的第k位均为0”。

比如,下面这个就满足这个条件。

100000010000110
010000000000111
001000010001011
000100000001100
000010010001101
000001000000111
000000100001010
000000001000111
000000000100100
000000000010100

这种情况仍然可以用上面的方法,开个d数组解决。原因是,“选某一元素以后的异或和一定严格大于不选某一元素以后的异或和”这一关键性质仍然可以满足。

到了这里,这个事情我们就可以做了。


我们把线性基进行一个简化,强制其满足上面的条件。

对于线性基中的每个数i,仍设其最高位为k,如果其他某个数j的第k位为1j就异或上i,把其第k位消去。满足以上条件后,就可以按照上面的方法处理了。


另外,因为线性基的特点,只根据基中元素没有办法判断能不能异或出零。如果在最开始插入元素时,某个元素没有插入成功,就代表原数列可以异或出零。

如果可以异或出零,则原算法求出的第k小值实为第k+1小值。算法开始前k就可以解决问题。

简化O(log2N),贪心O(logN)

查询第k大值

记线性基内元素数量为m,根据排列组合,一共能异或出2m1(如果可以异或出0,就是2m)个值。于是,第k大值即为第2mk(或2mk+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),得到它们的异或和。求可以得到多少值不同的异或和。

把所有数插到线性基里面,答案为2sizesize为线性基里元素的个数。这里不需考虑能不能异或出0,因为可以一个数都不选。

第二题有两个解法。

解法1:

在维护求LCA的倍增数组f[i][j](意为i2j级祖先)的同时,维护“从i到它的2j级祖先的这条路径上所有点的线性基”,记为base[i][j]。维护方法是把base[i][j-1]base[i+f[i][j-1]][j-1]合并在一起。另外维护点的深度dep,询问时会用。

对于每次询问的x,y,先找到它们的LCA,把LCA到xy两条路径拆开看。可以使用类似ST表的方法,找到满足f[x][k]比LCA深的最大的k,合并base[x][k]base[x的dep[x]-dep[LCA]+1-pow(2,k)级祖先][k]即可得到路径线性基。对于y同理,最后把两条路径的线性基再合并,即可得到xy的线性基。查询最大值即可。

时间复杂度O(nlognlog2G+q(logn+log2G)),但是时限宽,卡卡常应该能过。

前缀线性基

这里再介绍一种维护序列中区间异或线性基的方法,即解法2.

我们把插入线性基的逻辑进行修改:额外维护线性基中的每个数在原序列中的位置id。设当前需要在线性基i处插入x,但是basei0,而且idx>idbasei,就把xbasei交换(同时交换id,即把basei拿出来换成x),继续往下插入拿出来的basei

初始化先得到序列的前缀线性基。设询问区间[l,r]的线性基,我们就找到包含区间[1,r]中所有元素的线性基,删掉里面所有id<l的元素,即可得到[l,r]的线性基。

正确性的证明是,对于[1,r]的线性基base中的任意元素xx在插入线性基中的时候只和id>idx的元素异或过,所以对于idl的元素组成的集合,其中的每个元素都可以和集合内的元素异或得到原序列a中相应的元素,所以此挑选出的集合即为区间[l,r]的线性基。

初始化nlogk,询问区间每次logk

解法2:树上前缀线性基,维护出每个点到根的前缀线性基,求LCA后分别得到LCA到x和到y的区间线性基,再合并,查询,异或最大值即可。时间复杂度O(nlognlogG+q(logn+log2G)),稳过。

posted @   Cerebral_fissure  阅读(97)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示