线性基(极值、区间极值、第k大小)
线性基的性质:
1.
1.
1.线性基能相互异或得到原集合的所有相互异或得到的值。
2.
2.
2.线性基是满足性质
1
1
1 的最小的集合
3.
3.
3.线性基没有异或和为
0
0
0 的子集。
先引入
p
[
i
]
p[i]
p[i] 的概念和用法:
①
①
①
p
[
i
]
p[i]
p[i] 记录 原集合的数字,该数字的二进制下从小到大第
i
i
i 位为1。
②
②
② 每个数字最多被记录一次。
③
③
③ 数字优先从高位
"
1
"
"1"
"1"到低位
"
1
"
"1"
"1"记录!(即从该数的二进制的最高位的
"
1
"
"1"
"1" 开始记录,一旦发现
p
[
i
]
=
0
p[i] = 0
p[i]=0即存入
p
[
i
]
=
x
p[i] = x
p[i]=x ,然后枚举下一个数字。)
构造线性基的方法:
对于原集合中的每一个数:
①
①
① 若其二进制下最高位第
i
i
i 位的
“
1
”
“1”
“1”未被记录过,即
(
p
[
i
]
=
0
)
(p[i] = 0)
(p[i]=0),则更新令
p
[
i
]
=
x
p[i]=x
p[i]=x。
②
②
② 否则在寻找其次高位下的
“
1
”
“1”
“1”,直到枚举到其第
0
0
0 位
给定
n
n
n 个整数(数字可能重复)。
求在这些数中选取任意个,使得他们的异或和最大。
选择数字的方式显然贪心,二进制下 高位的
1
1
1 显然比 低位的
1
1
1 优先选择。
那么考虑当一个数字对答案的贡献时,我们用
p
[
i
]
p[i]
p[i] 数组记录 其 二进制下的最大贡献,即贡献至少为
2
i
2^i
2i。但如果 另出现一个数字,与之前
p
[
i
]
p[i]
p[i] 中记录的数字的最高位 均为
1
1
1 的时候,我们只能二选一,所以我们需要将后来的数字异或
p
[
i
]
p[i]
p[i],得到新数字,然后继续向下枚举新数字的次高位
1
1
1 。能记录则记录。
void seperate(long long x)
{
for(int i = 50; i >= 0; i--)
{
if(x >> (long long)i == 0) continue;
if(!p[i])
{
p[i] = x;
return;
}
x ^= p[i];
}
}
可能初学者会产生一个问题,若出现若干个数字,其最高位都是同一个位置,我们应该如何选择呢?
若论我们的选法,我们的 最高位的
i
i
i ,
p
[
i
]
p[i]
p[i] 只会记录第一个出现最高位的数字。尽管后面出现的数字的最高位均与
p
[
i
]
p[i]
p[i] 相同,但是没法选入
p
[
i
]
p[i]
p[i] 。我们的算法是这些数字 依次 与
p
[
i
]
p[i]
p[i] 异或,然后寻找次高位的
"
1
"
"1"
"1",但会出现两种情况:
我们不妨设
p
[
i
]
=
x
p[i] = x
p[i]=x , 与
x
x
x 最高位冲突的数字设为
y
y
y
①
①
① 原来记录的数字
x
x
x 可以贡献 最高位 和 次高位的答案,即
x
x
x 的次高位要比
y
y
y 的次高位要高,那么显然 记录
x
x
x 答案会更大一些。算法没有问题
②
②
② 原来记录的数字
x
x
x 的次高位不如
y
y
y 的次高位高, 那么显然当
y
y
y 异或
x
x
x 的的时候,
y
y
y 的次高位
"
1
"
"1"
"1" 与 对应位下的
x
x
x 的
"
0
"
"0"
"0" 一异或,答案为
1
1
1,那么我们对应的
p
[
j
]
p[j]
p[j] 就可以记录
x
x
x 异或
y
y
y 的答案了。 那么我们答案会统计 两个
p
[
i
]
p[i]
p[i] 数组的值,这两个数组的贡献仍然是最大的,可以理解为代替
①
①
① 的一个
p
[
i
]
p[i]
p[i] 答案,并且没有冲突,我们仍然遵循贪心的侧罗。那么显然算法也不会出现问题。
完整代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <cstdlib>
using namespace std;
int n;
long long ans, p[60];
long long read()
{
long long rt = 0, in = 1; char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') in = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') {rt = rt * 10 + ch - '0'; ch = getchar();}
return rt * in;
}
void seperate(long long x)
{
for(int i = 50; i >= 0; i--)
{
if(x >> (long long)i == 0) continue;
if(!p[i])
{
p[i] = x;
return;
}
x ^= p[i];
}
}
int main()
{
n = read();
for(int i = 1; i <= n; i++)
{
long long x = read();
seperate(x);
}
for(int i = 50; i >= 0; i--) ans = max(ans, ans ^ p[i]);
printf("%lld",ans);
system("pause");
return 0;
}
例题:
[模版]线性基
WC 2011 最大异或和
线性基变形
在洛谷提供的模版上,我们只写了
n
n
n 个数里 任意挑选数,使得其中异或值最大。
若规定好区间,询问区间内的最大值或者最小值,或者能否通过异或得到数字
K
K
K ,我们就需要对原来的线性基模版进行变形推广。
有 N N N 个数字,有 Q Q Q 次询问:
询问 1 1 1: 格式: 1 1 1 L L L R R R 。求区间 [ L , R ] [L,R] [L,R]中任意数字的最大异或值
询问 2 2 2: 格式: 2 2 2 L L L R R R 。求区间 [ L , R ] [L,R] [L,R]中任意数字的最小异或值
询问 3 3 3: 格式: 3 3 3 L L L R R R K K K 。如果在 [ L , R ] [L,R] [L,R] 中能否通过异或得到数字 K K K,则输出 Y E S YES YES,否则输出 N O NO NO。
对于这道题,又对于之前线性基的模版:
p
[
i
]
p[i]
p[i] 数组不再适用,因为其保存的数字可能不是由区间
[
L
,
R
]
[L,R]
[L,R] 中异或得来的。
我们需要另辟蹊径,将
p
[
i
]
p[i]
p[i] 开成 二维
p
[
j
]
[
i
]
p[j][i]
p[j][i] ,这个二维数组表示 在前
j
j
j 个数中二进制下第
i
i
i 位 存的是 哪个数字(这个数字可能是由 原数 之间 异或得来的, 大小并不一定是 原来的数字)。再换句话说,二维的
p
[
j
]
[
i
]
p[j][i]
p[j][i] 比
p
[
i
]
p[i]
p[i] 多记录了
N
−
1
N-1
N−1 个状态。开这个数组的意义就是为了存状态的。
我们还需要确定, 对于 每个 p [ j ] [ i ] = x p[j][i] = x p[j][i]=x, x x x 这个数字是插入哪个数字得到的,我们需要确定这个数字是否在区间 [ L , R ] [L,R] [L,R] 中。因此还需要在一个同 p [ j ] [ i ] p[j][i] p[j][i] 大小空间相同的 数组 p o s [ j ] [ i ] pos[j][i] pos[j][i] 。记录 p [ j ] [ i ] p[j][i] p[j][i] 的 数字 是由 第几个数异或 得来的。
在插入一个数字前,先用递推式 获得并记录 前
(
i
d
−
1
)
(id-1)
(id−1) (
i
d
id
id 表示这个数字是第几个) 个数字时的
p
[
j
]
[
i
]
p[j][i]
p[j][i] 和
p
o
s
[
j
]
[
i
]
pos[j][i]
pos[j][i] 状态。 然后对于插入的数字
X
X
X ,找其二进制下的从高位到低位的
1
1
1 。
对于数字
X
X
X 其二进制下的从高位到低位的
1
1
1 (设对应在第
i
i
i 位):
1.
1.
1. 若
p
[
j
]
[
i
]
p[j][i]
p[j][i] 未存入数字,那把当前数字放入,然后更新
p
o
s
[
j
]
[
i
]
pos[j][i]
pos[j][i] 数组,结束此流程。
2.
2.
2. 若
p
[
j
]
[
i
]
p[j][i]
p[j][i] 已经存入了其他数字,并且放入的这个数字是在
i
d
id
id 之前。那么我们的选择是,替换掉这个数字,然后
p
o
s
[
j
]
[
i
]
pos[j][i]
pos[j][i] 也被当前
i
d
id
id 替换掉(注意此时
i
d
id
id 的值被更新)。然后 令
X
=
X
X = X
X=X ^
p
[
j
]
[
i
]
p[j][i]
p[j][i] 。 然后重复此流程(但
1
1
1是从当前位向低位继续枚举)。
此时初次学习 可能会有几个小问题,我列举两个典型问题。
Q 1. Q1. Q1.: 为什么当满足上述条件 2 2 2 时,数字一定要被替换呢?
A 1. A1. A1.: 因为每次询问都是固定区间 [ L , R ] [L,R] [L,R]。我们要保证区间里的数字一定是最新的,只有这样才能保证区间 [ L , R ] [L,R] [L,R] 正确使用。(因为对于每一个 i i i ,我们都要用最新的数字来更新它,同时也解释了为什么还要有“放入的这个数字是在 i d id id 之前”这个限制条件)
Q 2. Q2. Q2.: 为什么取代之后,还要令 X = X X = X X=X ^ p [ j ] [ i ] p[j][i] p[j][i] 呢?
A
2.
A2.
A2.: 二进制的 第
i
i
i 位被操作
2
2
2 更新了,那么我们需要取 这两个数次高位的
1
1
1 , 那么用什么方法 能确定这两个数 哪个数的次高位
1
1
1 靠前呢,我们只需要异或一下,因为
①如果两个数在同一个位置次高位都是
1
1
1。那么替换后的答案不变,无事发生。
②如果两个数次高位
1
1
1 不在同一位,那么显然 高次的
1
1
1 对答案贡献会大,因此我们异或是为了取 下一个高次
1
1
1 的。用下一个高次
1
1
1 来更新后边的
p
[
j
]
[
i
]
p[j][i]
p[j][i] 和
p
o
s
[
j
]
[
i
]
pos[j][i]
pos[j][i] 数组。
上述内容的代码如图所示:
void seperate(int x, int id)
{
int t = id;
for(int i = 30; i >= 0; i--)
{
p[id][i] = p[id-1][i];
pos[id][i] = pos[id-1][i];
}
for(int i = 30; i >= 0; i--)
{
if(x >> i == 0) continue;
if(!p[id][i])
{
p[id][i] = x;
pos[id][i] = t;
return;
}
else if(pos[id][i] < t)
{
swap(p[id][i], x);
swap(pos[id][i], t);
}
x ^= p[id][i];
}
}
在查询给定的区间 [ L , R ] [L,R] [L,R] 异或最大或最小值时,显然我们要从 p [ r ] [ i ] p[r][i] p[r][i] 中查询给定的区间的数字。只有这样才能把所有数字都用上,但是用上的数字还必须属于 [ L , R ] [L,R] [L,R],因此再加一个限定条件 p o s [ r ] [ i ] ≥ l pos[r][i] \geq l pos[r][i]≥l。
一、查询最大值还是原来的求最大值方法,贪心取高位的 1 1 1 ,再用大小判断 取得的高位 1 1 1 不会被下一个 p [ j ] [ i ] p[j][i] p[j][i] 通过异或抵消掉。
算法正确性证明:
高位的
1
1
1 大于 低位所有
1
1
1 。那么贪心从高位
i
i
i 选择
p
[
j
]
[
i
]
p[j][i]
p[j][i]进行异或。
高位的
1
1
1 在选择的同时,低位的
1
1
1 可能会与之一同选上。但枚举到低位
1
1
1 的时候,我们选择它不一定会令答案变大,因此需要比较一下。同时,我们选择低位的
p
[
j
]
[
i
]
p[j][i]
p[j][i] 时,它一定不会影响高位的
1
1
1,因为在预处理
p
[
j
]
[
i
]
p[j][i]
p[j][i]的时候,如果同为
1
1
1, 我们令其与原来的
p
[
j
]
[
i
]
p[j][i]
p[j][i] 进行异或了,高位
1
1
1 相抵消成
0
0
0 了。
二、查询最小值,直接正序查找就好了,当 i i i 最小且 p [ j ] [ i ] p[j][i] p[j][i] 存在即可。
算法正确性证明:
设
X
X
X 为最小值答案,再设 存在一个
p
[
j
]
[
i
]
p[j][i]
p[j][i] 比
X
X
X 小,若
p
[
j
]
[
i
]
p[j][i]
p[j][i] 比
X
X
X 小,(二进制下 即
p
[
j
]
[
i
]
p[j][i]
p[j][i] 最高位的
1
1
1 一定不会早于
X
X
X)那么用反证法证明:
①假设 我们的答案
X
X
X 二进制下的 高位
1
1
1 比
p
[
j
]
[
i
]
p[j][i]
p[j][i] 二进制下的高位
1
1
1 要靠前!但假设不成立,因为我们
i
i
i 从
0
0
0开始枚举的,
p
[
j
]
[
i
]
p[j][i]
p[j][i] 会比
X
X
X 更早被枚举到,因此答案不会被记为
X
X
X。
②假设 我们的答案
X
X
X 二进制下的 高位
1
1
1 比
p
[
j
]
[
i
]
p[j][i]
p[j][i] 二进制下的高位
1
1
1 相同。那么既然我们的答案由
p
[
j
]
[
i
]
p[j][i]
p[j][i] 得到, 又存在一个新的
p
[
j
]
[
i
]
p[j][i]
p[j][i] 比当前答案小,那么显然这两个答案异或一下,得到的答案更小,这与假设的答案
p
[
j
]
[
i
]
p[j][i]
p[j][i]最小矛盾,所以假设不成立。
代码如图所示:
int query_max(int l, int r)
{
int ans = 0;
for(int i = 30; i >= 0; i--)
if(pos[r][i] >= l && ans^p[r][i] > ans)
ans ^= p[r][i];
return ans;
}
int query_min(int l, int r)
{
for(int i = 0; i <= 30; i++)
if(pos[r][i] >= l && p[r][i])
return p[r][i];
return 0;
}
对于操作
3
3
3 ,我们要查找 在区间
[
L
,
R
]
[L,R]
[L,R] 里能否通过异或得到数字
k
k
k 。
那么把
k
k
k 自行看成二进制数,我们只要在符合范围条件的 线性基中,通过异或,把 二进制下的
k
k
k 的
1
1
1 都通过异或消掉,最后把
k
k
k 消成
0
0
0 就好。
那么
k
k
k 二进制里每一个 为
1
1
1 的
i
i
i 位,我们必须令
p
[
j
]
[
i
]
p[j][i]
p[j][i] 与其异或,才能消掉,那么一路枚举下来,逢
1
1
1 便异或,如果
k
k
k 最后消成
0
0
0 ,那么就说明可以 通过区间里的数字异或得到
k
k
k。
算法正确性证明:
在构造
p
[
j
]
[
i
]
p[j][i]
p[j][i] 的时候,我们通过 冲突时 进行异或的方式,找下一个 高位
1
1
1 填入下一个低位
p
[
j
]
[
i
]
p[j][i]
p[j][i] 。因此我们这种处理方式 是将 存入值的
p
[
j
]
[
i
]
p[j][i]
p[j][i] 的数量最多化。同时冲突时,我们进行了异或,高位
1
1
1 便转化成了
0
0
0。也就是说
p
[
j
]
[
i
]
p[j][i]
p[j][i] 的数的大小 超不过
2
i
+
1
2^{i+1}
2i+1。也就是二进制下 比
i
i
i 次高的二进制位上都是
0
0
0 。因此我们顺着枚举下来就好且顺序从高位到地位,因为选高位的时候,会影响低位的答案!
代码如下:
int query_min(int l, int r)
{
for(int i = 0; i <= 30; i++)
if(pos[r][i] >= l && p[r][i])
return p[r][i];
return 0;
}
完整代码如下:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <cstdlib>
using namespace std;
int n,q;
int pos[101010][40],p[101010][40];
int read()
{
int rt = 0, in = 1; char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') in = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') {rt = rt * 10 + ch - '0'; ch = getchar();}
return rt * in;
}
int query_max(int l, int r)
{
int ans = 0;
for(int i = 30; i >= 0; i--)
if(pos[r][i] >= l && ans^p[r][i] > ans)
ans ^= p[r][i];
return ans;
}
int query_min(int l, int r)
{
for(int i = 0; i <= 30; i++)
if(pos[r][i] >= l && p[r][i])
return p[r][i];
return 0;
}
bool exist(int l, int r, int k)
{
for(int i = 30; i >= 0; i--)
if(pos[r][i] >= l && (k>>i))
k ^= p[r][i];
return (k == 0) ? true : false;
}
void seperate(int x, int id)
{
int t = id;
for(int i = 30; i >= 0; i--)
{
p[id][i] = p[id-1][i];
pos[id][i] = pos[id-1][i];
}
for(int i = 30; i >= 0; i--)
{
if(x >> i == 0) continue;
if(!p[id][i])
{
p[id][i] = x;
pos[id][i] = t;
return;
}
else if(pos[id][i] < t)
{
swap(p[id][i], x);
swap(pos[id][i], t);
}
x ^= p[id][i];
}
}
int main()
{
n = read(), q = read();
for(int i = 1; i <= n; i++)
{
int x = read();
seperate(x, i);
}
for(int i = 1; i <= q; i++)
{
int ins = read(), l = read(), r = read();
if(ins == 1) printf("%d\n",query_max(l, r));
else if(ins == 2) printf("%d\n",query_min(l, r));
else if(ins == 3)
{
int k = read();
printf(exist(l, r, k) ? "YES\n" : "NO\n");
}
}
system("pause");
return 0;
}
例题:暂无
给定
n
n
n 个整数(数字可能重复)。
输出异或第
k
k
k 小的数。
p
[
i
]
p[i]
p[i] 数组与最初的异或求最大值代码中的一样。
只是多了一个
r
e
b
u
i
l
d
rebuild
rebuild 函数。
r
e
b
u
i
l
d
rebuild
rebuild 操作 类似于化学的提纯,每一个
p
[
i
]
p[i]
p[i] 尽量只保留最高第
i
i
i 位的
1
1
1。其他位的
1
1
1 通过异或消掉。
然后记录重构的
p
[
i
]
p[i]
p[i] 数组就好啦。
对于查询:
先判断新的
p
[
i
]
p[i]
p[i] 数量是否与
n
n
n 相同,如果不相同则说明,存在两个数异或值为
0
0
0 ,所以
k
k
k 应该 自减
1
1
1。
把
k
k
k 用二进制考虑,考虑选
p
[
i
]
p[i]
p[i] 。
从最大位开始选择,选和不选都 第k大异或值都是
2
i
2^i
2i 大小的影响。
所以这就是把
k
k
k 用二进制考虑的原因。
完整代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <cstdlib>
using namespace std;
int n,k,cnt;
int p[40];
int read()
{
int rt = 0, in = 1; char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') in = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') {rt = rt * 10 + ch - '0'; ch = getchar();}
return rt * in;
}
void seperate(int x)
{
for(int i = 30; i >= 1; i--)
{
if(x >> i == 0) continue;
if(!p[i])
{
p[i] = x;
return;
}
x ^= p[i];
}
}
void rebuild()
{
for(int i = 30; i >= 0; i--)
for(int j = i-1; j >= 0; j--)
if( (p[i] >> j) & 1)
p[i] ^= p[j];
for(int i = 0; i <= 30; i++)
if(p[i]) p[cnt++] = p[i];
}
int search()
{
if(n != cnt) k--;
int ans = 0;
if(k >= (1 << cnt)) return -1;
for(int i = 30; i >= 1; i--)
if( (k >> i) & 1 ) ans ^= p[i];
return ans;
}
int main()
{
n = read(), k = read();
for(int i = 1; i <= n; i++)
{
int x = read();
seperate(x);
}
rebuild();
printf("%d",search());
system("pause");
return 0;
}
例题:暂无。