一、异或线性基
现在对于n个数字(下面称这n个数为原数集): ,找到一组数 ,可以用 中数的异或组合来表示 中的数(即 集合和 集合通过异或出来的值域是一样的),那么称 就是 的一个线性基。
例如:
对于集合
,它的线性基就是
。
因为
,
^
=
。
所以通过 中的元素异或,就可以直接得到 中的所有元素。
先说一说线性基有什么性质:
1.原数集中的任意一个数字都能够通过线性基中的元素异或出来(这与线性基的构建方式有关),同时也说明线性基不唯一。
2.原数集中的数字异或出来的值域与线性基中的元素以后出来的值域相等(通过上一条性质可知)。
3.线性基中没有异或和为零的非空子集:现在假设存在这样一个子集使得
的异或和为零,那么根据异或的性质能得出:
,既然
已经能用除他之外的线性基元素表示出来,我们便没有必要再将
放在线性基中。
4.线性基中的选取元素的每一种方案,都对应一个异或值,不存在多种选取方案对应同一个异或值的情况:现在假设存在这种情况,那么我们就会存在一个非空子集的异或值为零,这与上一条性质矛盾。
5.线性基是满足以上性质的最小集合,即线性基中不存在任何一个多余的元素。
构造线性基
首先定义两个数组:
是原数集 ,
是在二进制中二进制位为
最高位为第
位的数。
对于 可能不好理解,举个例子: ,在二进制下 11,其二进制位为 1 最高位为第 1 位,所以
;
我们主要通过插入操作来实现线性基的构造如下:
令插入的数为
,考虑
的二进制最高位
,
- 若线性基的第 位为 ( ),则直接在该位插入 ,退出;
- 若线性基的第 位已经有值 ,则 ,重复以上操作直到
插入顺序的不同会导致线性基的不同。
举个例子,原数集
既然要异或,先将原数集变为二进制表示:
直接观察,我们可以发现,
^
,
^
所以我们可以用
来表示
,也可以用
来表示
。
故
都是原数集的线性基。
下面使用插入操作来模拟:
-
先插入 ,其第三位是最高位,且 故 ,本次插入结束。
-
再插入 ,其第三位是最高位,且 ,故令 ^ ;
此时本次插入还未结束,这个 是上一个 ^ 来的,其第一位是最高位,且 ,故 。 -
最后插入最后一个元素 ,其第一位是最高位,且 ,故令 ^ ,插入结束;
到此求出一个线性基 ,如果将 的插入顺序交换就是另一个线性基 。
//插入线性基操作 p[i]是最高位为i的数 Maxbit是二进制位的最大值,正常设为63
void insert_lb(ll x) {
for(int j = Maxbit - 1; j >= 0; j--) {
if(x & (1ll << j)) {
if(!p[j]) {
p[j] = x;
break;
} else {
x ^= p[j];
}
}
}
}
线性基应用
1.询问数字x是否在当前线性基异或集合中
bool check(ll x){
for(int i=Maxbit - 1;i>=0;--i)
if(x & (1ll << i)){
if(!p[i]) return 0;
x^=p[i];
if(!x) return 1;
}
}
2.求异或最大值
ll max_xor() {
ll ans=0;
for(int i=Maxbit-1; i >= 0; i--)
if(ans^p[i]>ans)
ans=ans^p[i];
return ans;
}
因为我们求最大值,所以,我们从二进制高位开始枚举,因为先枚举高位,异或后 只可能 后面的低位会变大,所以比较一下异或后低位可能变大的值,维护一下最大值即可。
例题:luogu P3812 【模板】线性基
3.求异或最小值
int min_xor()
{
int cnt=0;//cnt代表线性基的个数,n代表原序列个数
for(int i=0; i < Maxbit; i++)
if(p[i])
cnt++;
if(cnt<n)
return 0;
else
{
for(int i=0; i<=61; i++)
if(p[i])
return p[i];
}
}
线性基最小的非零数就是异或的最小值,因为异或其他数都会使结果变大。
还要注意的一点就是
- 如果线性基的个数和原数集的个数不相同的话,那么原数集中元素异或结果必定有 ,
- 如果线性基的个数和原数集的个数相等,则异或后没有
4.求异或第 k 小的值
方法:先将原线性基中,除了 ,其他所有数的第 位全部变成 。
举个例子:
线性基为
将除
之外,其他数的第
位全变为
将除
之外,其他数的第
位全变为
然后从小到大把所有线性基中的元素,存在一个新的数组
中。
我们这样处理排序后,任意两个数进行异或后 就只会变大,不会变小,因为只有
上的第
位是
其他都是
,异或后必然变大,这样就了一定的单调性。
对于 的二进制中的数位 ,若 的二进制在 位为1,我们就把 数组中不为零的且排名为 的数异或到ans上。
为什么可以这样? 其实这和二进制的原理十分相似,我们来分析一下。(首先抛开异或和为 情况不说,一会单独考虑)。
以
为例:
的二进制为
。
二进制位的第零位和第二位是
,那么第五小的异或和就是
^
的二进制为
。
二进制位的第一位和第二位是
,那么第六小的异或和就是
^
通过 我们上边红字部分的单调性,导致了这样一条规律。
下面主要是解决异或和 有没有 的问题,这个问题在求异或最小值时已经提过了。
结论:
- 如果线性基的个数和原数集的个数不相等的话,那么原数集中元素异或结果必定有 ,
- 如果线性基的个数和原数集的个数相等,则异或后没有
可以简单地举例证明一下:
原数集 ,取一个线性基 ,因为 ^ ,所以 ^ ^ 。
而 就不会产生这种情况。
HDU 3949 XOR
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ll long long
using namespace std;
const int Max = 57;
const int Maxbit = 63;
ll p[Maxbit]; //保存线性基
ll s[Maxbit]; //重构后的线性基
ll k,x;
int cas,icas = 0;
int n,cnt = 0,Q;
//将 x 插入线性基
void insert_lb(ll x) {
for(int j = Maxbit - 1; j >= 0; j--) {
if(x & (1ll << j)) {//如果 x的第 j 位为 1
if(!p[j]) {//如果之前没有数插入过
p[j] = x;
break;
} else {//如果已经有数插入过
x ^= p[j];
}
}
}
}
//重构线性基
void rebuild(){
for(int i = 0; i < Maxbit; i++) {
for(int j = Maxbit - 1; j > i; j--) {
if(p[j] & (1ll << i)) {
p[j] ^= p[i];
}
}
}
//从小到大排序
for(int i = 0 ; i < Maxbit; i++) {
if(p[i]) s[cnt++] = p[i];
}
}
int main() {
scanf("%d",&cas);
while(cas--) {
//初始化
printf("Case #%d:\n",++icas);
memset(p,0,sizeof(p));
cnt = 0;
scanf("%d",&n);
for(int i = 1; i <= n; i++) {
scanf("%lld",&x);
insert_lb(x);
}
rebuild();
//开始查询
scanf("%d",&Q);
while(Q--) {
ll ans = 0;
scanf("%lld",&k);
if(n != cnt) k--;// 如果线性基 和 原数集个数不同 含有 0
if(k >= (1ll << cnt)){
printf("-1\n");
continue;
}
for(int i = Maxbit-1; i >= 0; i--)
if(k & (1ll << i)) ans ^= s[i];
printf("%lld\n",ans);
}
}
return 0;
}
/*
2
2
1 2
4
1 2 3 4
3
1 2 3
5
1 2 3 4 5
2
5
21 11 6 3 16
16
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
*/
5.有向图找最大异或路径
主要通过 luogu P4151 [WC2011]最大XOR和路径 这道题来理解
二、整数线性基
线性空间是一个关于以下两个运算封闭的向量集合:
- 向量加法 ,其中 均为向量。
- 标量乘法 ,也称为数乘运算,其中 是向量, 是常数(标量)。
给定若干向量 ,若向量 能由 经过向量加法和标量乘法运算得出,则称向量 能被向量 表出。显然, 能表出的所有向量构成一个线性空间, 被称为这个线性空间的生成子集。
任意选出线性空间中的若干个向量,如果其中存在一个向量能被其他向量表出,则称这些向量线性相关,否则称这些向量线性无关。
线性无关的生成子集被称为线性空间的基底,简称基。基的另一种定义是线性空间的极大线性无关子集。一个线性空间的所有基包含的向量个数都相等,这个数被称为线性空间的维数。
例如,平面直角坐标系中的所有向量构成一个二维线性空间,它的一个基就是单位向量集合
。平面直角坐标系的
轴上的所有向量构成了一个一维线性空间,它的一个基就是
。
对于一个 行 列的矩阵,我们可以把它的每一行看作一个长度为 的向量,称为“行向量”。矩阵的 个行向量能够表出所有向量构成的一个一维线性空间,这个线性空间的维数被成为矩阵的"行秩"。类似地,我们可以定义列向量和列秩。实际上,矩阵的行秩一定等于列秩,它们都被称为矩阵的秩。
把这个 的矩阵看作"系数矩阵"进行高斯消元(增广矩阵的最后一列全看作零),得到一个简化阶梯矩阵。显然,简化阶梯矩阵的所有非零行向量线性无关。因为初等行变换就是行向量之间进行的向量加法与标量乘法运算,所以高斯消元不改变行向量表出的线性空间。于是,简化阶梯形矩阵的所有非零行向量就是该线性空间的一个基,非零行向量的个数就是矩阵的秩。
【例题1】luogu P3265 [JLOI2015]装备购买
题意:
个装备,每个装备 个属性,每个装备还有个价格 。如果手里有的装备的每一项属性为它们分配系数(实数)后可以相加得到某件装备,则不必要买这件装备。求最多装备下的最小花费。
思路
每个装备就是一个向量,要求最多装备的数量,就是求出
个向量的秩。
还要求花最少的钱,我们只需要在选出在相同作用下,价格更小的那个向量即可。比如
和
都可以表出
,但是
的价格更低,所以我们优先选择
。
这样我们先给向量提前排个序,将价格小的放在前面即可。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ld long double
#define eps 1e-6
using namespace std;
const int N = 507;
int n,m,cnt = 0,sum = 0,p[N] = {0};
struct Node{
int cost;
ld a[N];
}z[N];
bool cmp(const Node &a,const Node &b){
return a.cost < b.cost;
}
void debug(){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
printf("%Lf ",z[i].a[j]);
}
puts("");
}
}
void Gauss(){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(fabs(z[i].a[j]) > eps){
if(!p[j]){
p[j] = i;
cnt++;
sum += z[i].cost;
break;
}else {
ld tmp = z[i].a[j] / z[p[j]].a[j];
for(int k = j; k <= m ; k++)
z[i].a[k] -= z[p[j]].a[k] * tmp;
}
debug();
}
}
}
}
int main(){
scanf("%d %d",&n,&m);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
scanf("%Lf",&z[i].a[j]);
}
}
for(int i = 1; i <= m; i++) scanf("%d",&z[i].cost);
sort(z,z + n,cmp);
Gauss();
printf("%d %d\n",cnt,sum);
return 0;
}