对于给出多个数,求异或的最大值时,通常需要用线性基来处理
什么是线性基呢?
对于原数组,我们试图用最少个数作为基础,用这些数之间的异或 就可以 表示所以子集合的异或和,而这些数即基础,称之为线性基,类似于用10个数来表示1~1023的所有数的道理
所有基线性相关
插入
我们用数组a来表示线性基,a[i]表示基当中最高位为第i位的基
基的插入过程为
- 假如当前数v的最高位i所在的基a[i]已经存在(a[i]!=0),那么这个数通过基a[i]转化成低位的数:v^=a[i]
- 假设不存在,就拿当前数作为基a[i]
eg:
假设给出下列二进制数
1100 ,1011 ,1110 ,1101 ,0001 ,0101
- 1100,因为a[4]==0 所以a[4]=1100
- 1011,a[4]存在,所以1011->0111,故a[3]=0111
- 1110,a[4]存在,->0010,a[2]=0010
- 1101,a[4]存在,->0001,a[1]=0001
- 0001,a[1]存在,->0000
- 0101,a[3]存在,->0010,a[2]存在,->0000
很容易发现,只要满基了,就可以表示所有的数了,因为相当于二进制每一位都可以控制0或1
本质:低位基都是由数经过高位基转化而来
void insert(int val){
for(int i=31;i>=1;i--){
int j=i-1;
if(val&(1<<j)){
if(!a[i]){
a[i]=val;break;
}
val^=a[i];
}
}
if(val==0)a[0]=1;//可以通过异或变成0,用于找最小值
}
验证存在性
和插入的过程类似,从高到低,只要同位的最高位基存在,就异或,到最后成功由这些基转化成0就说明存在
int fin(int val){
if(val==0&&a[0])return 1;
for(int i=31;i>=1;i--){
int j=i-1;
if(val&(1<<j)){
val^=a[i];
if(!val)return 1;
}
}
return 0;
}
合并
因为基的性质,只要把另一个线性基里面的基当成数插入就可以了(相当于把其他的基通过当前线性基内的基转化一下)
void merge(int b[]){
for(int i=31;i>=1;i--){
if(!b[i])continue;
insert(b[i]);
}
}
查找最值
所有原数之间的异或和等价于基之间的异或和,所以找最大值,只需要判断所有基的取与不取即可
找最大值时,从高位基开始找,取某个基时使答案变大,那么就是需要取的
int finmax(){
int ans=0;
for(int i=31;i>=1;i--){
if(ans^a[i]>ans)ans^=a[i];
}
return ans;
}
找最小值时,因为基代表了最高位是哪一位,所以最小的那个基就是答案(当然,要考虑一个基都没有的情况)
int finmin(){
if(a[0])return 0;
for(int i=1;i<=31;i++){
if(a[i])return a[i];
}
}
模板
#include<bits/stdc++.h>
using namespace std;
struct linear_Bace;
typedef linear_Bace LB;
#define N 100009
struct linear_Bace{
int a[33];
void insert(int val){
for(int i=31;i>=1;i--){
int j=i-1;
if(val&(1<<j)){
if(!a[i]){
a[i]=val;break;
}
val^=a[i];
}
}
if(val==0)a[0]=1;//可以通过异或变成0
}
int fin(int val){
if(val==0&&a[0])return 1;
for(int i=31;i>=1;i--){
int j=i-1;
if(val&(1<<j)){
val^=a[i];
if(!val)return 1;
}
}
return 0;
}
int finmax(){
int ans=0;
for(int i=31;i>=1;i--){
if((ans^a[i])>ans)ans^=a[i];
}
return ans;
}
int finmin(){
if(a[0])return 0;
for(int i=1;i<=31;i++){
if(a[i])return a[i];
}
return 0;
}
}e[N];
void merge(LB &a,LB &b){//a和b都变成a+b
for(int i=31;i>=1;i--){
if(b.a[i]==0)continue;
a.insert(b.a[i]);
}
b=a;
}
例题
题意:
给出n个数,会依次删除所有数,求每次删除前剩下数组ans
定义ans=从任意一个完整区间(连续的没有数被删除)内找任意个数,使异或和最大
解析:
异或和最大值,明显的线性基,但是线性基只要insert,而没有erase,所有我们离线处理删除操作,正向删除相当于反向插入,这样基本思路就有了
接下来要处理完整区间的问题
把每个位置都放一个线性基,每插入一个点,如果左右连着一段已经处理出来的有效区间,那么就可以合并到一起
怎么合并,和哪个合并?
每插入一个点,那个点接壤的点如果是已有区间的一部分,那么就一定是端点。
所以就可以用两个端点的相关值来表示这个区间的相关值
相关值有:左端点和右端点位置,一个区间的线性基
那么就用l[i]表示i所在区间的左端点位置,r[i]同理
insert 位置p时,如果连着p-1,就可以通过l[p-1]知道这个区间的左端点位置
代码:
#include<bits/stdc++.h>
using namespace std;
struct linear_Bace;
typedef linear_Bace LB;
#define N 100009
struct linear_Bace{
int a[33];
void insert(int val){
for(int i=31;i>=1;i--){
int j=i-1;
if(val&(1<<j)){
if(!a[i]){
a[i]=val;break;
}
val^=a[i];
}
}
if(val==0)a[0]=1;//可以通过异或变成0
}
int fin(int val){
if(val==0&&a[0])return 1;
for(int i=31;i>=1;i--){
int j=i-1;
if(val&(1<<j)){
val^=a[i];
if(!val)return 1;
}
}
return 0;
}
int finmax(){
int ans=0;
for(int i=31;i>=1;i--){
if((ans^a[i])>ans)ans^=a[i];
}
return ans;
}
int finmin(){
if(a[0])return 0;
for(int i=1;i<=31;i++){
if(a[i])return a[i];
}
return 0;
}
}e[N];
void merge(LB &a,LB &b){//a和b都变成a+b
for(int i=31;i>=1;i--){
if(b.a[i]==0)continue;
a.insert(b.a[i]);
}
b=a;
}
int nu[N];
int ord[N];
int f[N],l[N],r[N];
int main(){
int n;scanf("%d",&n);
for(int i=1;i<=n;i++)l[i]=r[i]=i,f[i]=0;
for(int i=1;i<=n;i++)scanf("%d",nu+i);
for(int i=1;i<=n;i++)scanf("%d",ord+i);
int ans=0;
stack<int>A;
for(int i=n;i>=1;i--){
int p=ord[i],y=nu[p];
f[p]=1;
if(f[p-1]==0&&f[p+1]==0){//孤立点
e[p].insert(y);
ans=max(ans,e[p].finmax());
}
else if(f[p-1]&&f[p+1]){//连接两个区间的点
int u=l[p-1],v=r[p+1];
r[u]=v,l[v]=u;
e[v].insert(y);
merge(e[v],e[u]);
ans=max(ans,e[v].finmax());
}
else if(f[p-1]){//连接左区间
int u=l[p-1];
e[u].insert(y);
r[u]=p,l[p]=u;
e[p]=e[u];
ans=max(ans,e[u].finmax());
}
else{//连接右区间
int u=r[p+1];
e[u].insert(y);
r[p]=u,l[u]=p;
e[p]=e[u];
ans=max(ans,e[u].finmax());
}
A.push(ans);
}
while(!A.empty()){
printf("%d\n",A.top());A.pop();
}
}