前言
线性基是向量空间的一组基,通常可以解决有关异或的一些题目。──OI Wiki
另外,为了描述方便,我们不严谨的约定下文中出现的所有“子集”都非空
线性基的作用
求解一个数集中 集合内异或和最大/最小的子集中元素的异或和(即从集合中挑选任意多个数,求异或和的最值)
- 在线性基的帮助下,一个无序的异或和问题可以转化为贪心问题
线性基的性质
虽然我不知道为什么所有有关线性基的文章里都会把这个放在前面,但是我还是放一下吧
- ① 线性基所有子集的异或和形成的集合 与 原数集所有子集的异或和形成的集合 相同
- (线性基内所有的元素都是通过原数集中的元素做异或运算得到的)
- ② 线性基是满足①性质的最小集合
- ③ 线性基不存在异或和为的子集
- (线性基所有元素的得到不存在与异或的过程)
- ④ 线性基所有子集的异或和形成的集合为无重集合
- (本质上和性质③一样)
- ⑤ 线性基内元素二进制下最高位都在不同的位置
- (因为线性基的实现就是以最高位位置为索引存的一维数组)
线性基的构造
似乎所有题解都是在摆出一大堆说明后直接开始讲怎么构造(虽然似乎这样更有效率),但是我其实一开始看得一头雾水也挺悲(虽然其实真的直接看会快很多),但我还是决定把分析推导过程谈一谈(当然只是我自己的)。
考虑异或和最大问题
首先有关异或这个运算的特性,不再详细说明,提出关键词:可逆
想让一个数最大,在二进制下我们肯定希望它的最高位的位置能更大,一个最高位在第8位的肯定比最高位在第4位的大。
然后,比如说我们现在已经有一个二进制数_2#card=math&code=%2810100%29_2&id=iR84S),我们想让他更大,可以让它与_2#card=math&code=%281xxxxx%29_2&id=eAafe)异或,不管怎么样它都能变大。所以我们考虑能不能维护一个数组,下标是最高位在第位的数。
考虑一个最高位位置各不相同的集合,可以直接按照位置从高到低贪心求解。
然而实际问题里最高位位置不一定都不同。所以我们的线性基不能直接简单存储。我们现在考虑最高位在同一个位置的两个数怎么处理。
比如有:_2#card=math&code=x%3D%281010%29_2&id=sRf6p),_2#card=math&code=y%3D%281001%29_2&id=syyTs),它们的最高位都是第四位。考虑处理顺序,我们先将塞入了(因为第四位实际上是用(1<<3)获得的,所以我们用表示第位),然后发现的最高位也是4。如果这两个数都选择的话,就会导致第四位的消失,所以我们考虑把,虽然存的是,但是除了第四位的信息,都可以由新的和得到,这时候我们把再塞入线性基就可以了。如果遇到冲突处理类似。
当我们在贪心处理的时候,如果本来就比大,那我们直接就不用了。如果比大,那我们可以用来表示原来的。
简单且不严谨的归纳一下,由于异或运算的可逆性,存和存是一样的,但是可以造成其中一个数最高位的变化。
简单的理解是,线性基维护的是能够对第位产生贡献的元素中的其中一个,并通过之后的部分元素与第位元素共同维护原集合中最高位在的元素。
线性基的实现
对于一个数:
- 找到它最高位的位置(满足%3D1#card=math&code=x%20%5C%26%20%281%3C%3Cp%29%3D1&id=ud5pq)的最大的)
- 检查
b[p]
是否为空- 如果为空:令
b[p]
为 - 如果非空:令为,重复跳回最初
- 如果为空:令
最终得到线性基数组b[n]
//find函数返回p
rep(i,1,n){
while(1){
int p = find(a[i]);
if(!b[p]){
b[p] = a[i];
break;
} else {
if(p == 0) break;
a[i] ^= b[p];
}
}
}
线性基的应用
求异或和最小
直接返回b[n]
中的最小元素(即从小往大遍历第一个非元素)
求异或和最大
从大往小贪心,如果b[i]
能使变大,那就异或b[i]
,反之不异或。
相关题目
#include<map>
#include<set>
#include<cmath>
#include<queue>
#include<bitset>
#include<vector>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define rep(i,a,b) for(register int i = (a);i <= (b);++i)
#define per(i,a,b) for(register int i = (a);i >= (b);--i)
#define mkp std::make_pair
typedef long long ll;
typedef unsigned long long ull;
using std::string;using std::cin;using std::cout;
inline bool cmp(int x,int y){return x < y;}
const int N = 1e6+9;
const int inf = 1e9+9;
const double eps = 1e-7;
int _,n,m;
ll a[2*N],b[110];
inline int find(ll x){
int p = 55;
while((1ll<<p) > x && p >= 0) --p;
while((1ll<<p) & x == 0 && p >= 0) --p;
return p;
}
int main(){
std::ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
//freopen("in.in", "r", stdin);
cin >> n;
rep(i,1,n) cin >> a[i];
rep(i,1,n){
while(1){
int p = find(a[i]);
if(!b[p]){
b[p] = a[i];
break;
} else {
if(p == 0) break;
a[i] ^= b[p];
}
}
}
ll ans = 0;
per(i,55,0) ans = ans > (ans ^ b[i]) ? ans : (ans ^ b[i]);
cout << ans << "\n";
return 0;
}
这道题也是一道板子题。我们先二进制状压一下每个开关的效果,然后求这些数的线性基。由于线性基的性质,所以可以得到答案就是,其中表示线性基中的元素个数。
#include<map>
#include<set>
#include<cmath>
#include<queue>
#include<bitset>
#include<vector>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define rep(i,a,b) for(register int i = (a);i <= (b);++i)
#define per(i,a,b) for(register int i = (a);i >= (b);--i)
#define mkp std::make_pair
typedef long long ll;
typedef unsigned long long ull;
using std::string;using std::cin;using std::cout;
inline bool cmp(int x,int y){return x < y;}
const int N = 1e6+9;
const int inf = 1e9+9;
const double eps = 1e-7;
int _,n,m;
ll a[2*N],b[2*N],ans;
char ch[100];
inline int find(ll x){
int p = 55;
while(!(x & (1ll<<p)) && p >= 1) --p;
return p;
}
int main(){
std::ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#ifdef LOCAL //use "g++ xxx.cpp -DLOCAL"
freopen("in.in", "r", stdin);
#endif
cin >> n >> m;
rep(i,1,m){
cin >> ch+1;
rep(j,1,n) a[i] = (a[i] << 1) + (ch[j] == 'O');
}
rep(i,1,m){
int p;
while(1){
p = find(a[i]);
if(!b[p]){
b[p] = a[i];
break;
} else {
a[i] ^= b[p];
if(p == 0) break;
continue;
}
}
}
ans = 1;
per(i,55,0) if(b[i]){ans <<= 1 , ans %= 2008;}
if(ans == 1) ans = 0;
cout << ans%2008 << "\n";
return 0;
}