线性基求异或极值问题
极难2线性基求异或极值——让一堆数“变”出你想要的数
你有没有想过,给你一堆数,比如你一周的零花钱分别是3元、5元、6元(每个数字代表一次消费的金额),你可以选择其中任意几个(每个最多用一次)进行异或运算(就是二进制下每位相同得0、不同得1),最后能得到的结果最大是多少?最小是多少?能不能得到从小到大排列的第k个结果?
直接枚举所有子集有2ⁿ种,当n很大时计算机也算不动。但如果我们把这些数放进一个叫线性基的神奇结构中,就能快速回答这些问题。这一节我们就来学习如何用线性基求异或的最大值、最小值和第k小值。
1. 先给线性基“化个妆”——标准形(阶梯形)
之前我们学的线性基,每个基向量 basis[i] 的最高位是第 i 位,并且插入时已经用高位的基消去了低位的该位。但这种基还不够“整齐”,因为基向量之间可能在低位上还有重叠,导致我们想求第k小值时无法直接对应二进制位。
我们需要再做一步高斯消元,让每个基向量只在自自己的最高位上是1,而在所有更高的位上都是0(这个已经满足),并且把每个基向量的低位的1也消掉。这样得到的基向量就变成了标准形(也叫最简形、阶梯形)。
具体步骤(假设我们已有插入后的 basis[0..maxBit],其中 basis[i] 要么是0,要么最高位是i):
for i from maxBit down to 0:
if basis[i] != 0:
for j from i-1 down to 0:
if basis[i] 的第 j 位是 1:
basis[i] ^= basis[j] // 用低位基消去
经过这一步,每个 basis[i] 就变成了“干净”的向量:它只在最高位i是1,其他比i低的位可能还有一些1,但这些1恰好对应着那些比i更低的、没有被更高位覆盖的位。其实更严谨地,我们会把非零的基向量收集到一个数组 p 中,并保持最高位从大到小排序。这样每一个 p[i] 的最高位不会在任何一个 p[j] (j>i) 中出现,并且低位也已经被清理,使得任何两个向量的异或结果都不会混淆。
生活中的类比:就像整理书包,原来每本书(向量)可能杂乱地塞着其他书的边角(低位重叠)。我们重新把每本书的边角都收好,让每本书只占自己的一格,这样想取第几本书就很清楚。
2. 求最大值:贪婪的“高位优先”
最大值算法很简单,无论基是否标准化都适用:从最高位向低位贪心,如果当前结果异或上该位基向量后变大,就异或。
例子:用线性基求 {3, 5, 6} 能异或出的最大值。
- 先构建线性基(过程略,假设我们得到 basis[2]=6(110), basis[1]=5(101), basis[0]=3(011) 或者更标准的形式)。
- 从最高位2开始:ans=0,basis[2]=6(110),异或后变成6 > 0,所以ans=6。
- 看第1位:basis[1]=5(101),当前ans=6(110),异或后=3(011) < 6,不异或。
- 看第0位:basis[0]=3(011),异或后=5(101) < 6,不异或。
- 最后ans=6,最大值是6(实际上 {3,5,6} 可以异或出0,3,5,6,2,1,7等,最大就是7?等一下这里算错了?让我重新验证:3⊕5=6, 3⊕6=5, 5⊕6=3, 3⊕5⊕6=0, 单独3,5,6,还有2? 3⊕5=6, 3⊕6=5, 5⊕6=3, 0, 只有这些吗?其实{3,5,6}所有子集异或结果有:0,3,5,6, 3⊕5=6(重复), 3⊕6=5(重复), 5⊕6=3(重复), 3⊕5⊕6=0,所以最大是6?不对,可以再检查:3(011),5(101),6(110),三个数异或能得到7吗?3⊕5⊕6 = (011)⊕(101)=110, 110⊕110=000,所以不行。但3⊕5=6, 3⊕6=5, 5⊕6=3,没有7。所以最大值是6,算法正确。
注意:最大值一定不会是负数(异或都是非负整数),而且一定等于所有基向量异或起来的结果(因为贪心结果就是所有基向量都异或上,因为每个高位都变大)。
3. 求最小值:小心空集与0
最小值问题要分情况讨论:
- 如果允许空集(即什么也不选),那么最小值就是0。
- 如果不允许空集(必须至少选一个数),那么最小值就是线性基中非零的最小值。
这个非零的最小值就是线性基中最低位(即最高位最小的那个)基向量本身。因为任何非零异或结果,其最高位至少是某个基向量的最高位,而最小的那个基向量就是所有非零结果中最小的(因为其他结果都要包含更高位,数值更大)。
例子:{3,5,6},线性基中最低位的基向量是3(如果标准化后可能是1?注意标准化后基向量可能会变小,但这里原始基可能包含3)。实际上,{3,5,6}能得到的非零最小值是3(单独选3),还是1?我们检查所有非空子集异或结果:3,5,6, 3⊕5=6, 3⊕6=5, 5⊕6=3, 3⊕5⊕6=0,所以非零最小值是3。所以算法正确。
但要注意:如果线性基中有一个基向量是0(比如原集合包含0),那么0本身也是可以得到的(但通常我们不考虑空集时,0也算一个结果,此时最小值就是0)。
4. 求第k小:二进制编码的魔法
这是关键部分,需要线性基是标准形(阶梯形)。我们假设已经把线性基整理成了 p[0], p[1], ..., p[m-1],其中 p[0] 的最高位最大,p[m-1] 的最高位最小(即从大到小排列)。并且每个 p[i] 的二进制表示中,最高位唯一,低位已经被清理(即任何两个不同的 p[i] 异或不会出现高位重叠)。
现在,把所有可能的异或结果(包括0)从小到大排序。因为基向量之间相互独立,每个结果都可以表示成某些 p[i] 的异或和,而且这种表示是唯一的(就像二进制数每一位对应一个基向量)。具体来说,如果我们把结果映射成一个二进制数,第0位对应 p[m-1],第1位对应 p[m-2],……,第m-1位对应 p[0],那么从0到2^m - 1的每个整数k,对应的异或结果就是:将k的二进制位从左到右(高位对应p[0])依次决定是否异或对应的p[i]。注意:通常我们习惯把p[0]当作最高位,那么k的二进制最高位就对应p[0]。
公式:ans = 0; for i in range(m): if (k >> (m-1-i)) & 1: ans ^= p[i]
或者更常见:把p数组从低位到高位存储(即p[0]是最低位),那么k的二进制第i位对应p[i]。
统一约定:我们通常把标准化后的基向量按最高位从小到大排列(即 p[0] 最小),那么第k小(k从0开始)直接对应k的二进制位。例如:
- 第0小(k=0):结果=0(空集)
- 第1小(k=1):异或 p[0](最小的基向量)
- 第2小(k=2):异或 p[1]
- 第3小(k=3):异或 p[0]^p[1]
- 以此类推
注意:如果原集合不能异或出0(即没有任何子集异或为0),那么最小非零结果就是p[0]。但通常我们允许空集,所以第0小就是0。如果题目禁止空集,那么第1小才是p[0],需要调整k。
例子:用 {3,5,6} 验证。先构建线性基并标准化。
- 3(011),5(101),6(110) 插入后得到基(简化,不展开):假设我们得到 basis[2]=6, basis[1]=5, basis[0]=3?但标准化后需要消低位:用basis[1]消basis[2]的低位?6(110)的第1位是1,用basis[1]=5(101)异或掉得6^5=3(011),但新得的3最高位是1?乱了。我们直接通过完整线性基构建并标准化:
实际上,{3,5,6}的标准线性基应该是:先插入3(011),basis[1]=3;插入5(101),最高位2,basis[2]=5;然后检查basis[2]的低位,它的第0位是1,而basis[0]没有,所以暂时不动;插入6(110),最高位2,先消去basis[2]已有的5,6^5=3(011),然后这个3的最高位是1,跟basis[1]冲突,再消去basis[1]=3,得3^3=0,所以最终基只有basis[2]=5(101)和basis[1]=3(011)?不对,这样最后得到了两个向量,但它们的最高位分别是2和1。然后标准化:从高位到低位,对于basis[2]=5(101),它低位第0位是1,但basis[0]为0,不用消;它的低位第1位是0,不用消。对于basis[1]=3(011),低位第0位是1,但basis[0]为0,所以不变。最终基为 p[0]=5(101), p[1]=3(011)(按最高位从大到小排序?实际上最高位是2和1,所以p[0]=5, p[1]=3)。然后计算所有结果:
- k=0 (对应二进制0) -> 0
- k=1 (二进制01,取最低位对应p[1]) -> 3
- k=2 (二进制10) -> 5
- k=3 (二进制11) -> 3^5=6 所以第0小=0,第1小=3,第2小=5,第3小=6。跟之前枚举一致。正确!
5. 常见错误与提醒
- 没有标准化就去求第k小:结果会乱,因为基向量之间低位重叠,导致二进制映射不唯一。
- 求最小值时忘记考虑0:如果题目允许空集,最小值就是0;如果不允许,需要先检查是否有子集异或为0(即插入过程中是否有数被完全消成0),如果有则0可以作为非空结果(但通常0也算一个结果,只是它并不是非空的,需要根据题意处理)。
- 第k小中k的范围:k从0到2^m - 1,如果题目要求的是排除空集后的第k小(比如第1小是非零最小的),需要将k加1后再映射。
- 异或运算的优先级:在写代码时注意结合顺序,用括号明确。
- 数据溢出:如果数字范围很大(比如long long),注意用合适的数据类型。
6. 完整可运行代码(Python)
以下代码实现了线性基的插入、标准化、以及查询最大值、最小值、第k小值(包括空集0)。注释很详细,变量用简短英文。
class LinearBasis:
def __init__(self, max_bit=60):
# max_bit 是数字的最大二进制位数,一般 60 足够处理 10^18 以内的数
self.basis = [0] * (max_bit + 1) # 存储基向量,basis[i] 最高位为 i
self.cnt = 0 # 基中非零向量的个数
self.zero = False # 标记能否异或出0(是否有线性相关)
def insert(self, x):
"""向线性基中插入一个数,返回是否成功(不改变原有基)"""
for i in range(len(self.basis)-1, -1, -1):
if (x >> i) & 1:
if self.basis[i] == 0:
self.basis[i] = x
self.cnt += 1
return True
x ^= self.basis[i]
# 如果 x 最后变成0,说明线性相关,可以异或出0
self.zero = True
return False
def get_max(self):
"""求能异或出的最大值"""
ans = 0
for i in range(len(self.basis)-1, -1, -1):
if (ans ^ self.basis[i]) > ans: # 贪心:异或后变大就异或
ans ^= self.basis[i]
return ans
def get_min(self, include_empty=False):
"""求最小值
include_empty=True 时包括空集(结果为0)
否则返回非空子集的最小值(即最小的非零基向量或0如果存在)"""
if include_empty:
return 0
if self.zero: # 存在线性相关,可以异或出0(非空子集)
return 0
# 否则找到最小的非零基向量
for i in range(len(self.basis)):
if self.basis[i] != 0:
return self.basis[i]
return 0 # 没有元素时返回0(但通常不会出现)
def standardize(self):
"""将基化为标准形(阶梯形),使得每个基向量低位干净"""
# 从高位到低位消去
for i in range(len(self.basis)-1, -1, -1):
if self.basis[i] == 0:
continue
for j in range(i-1, -1, -1):
if (self.basis[i] >> j) & 1:
self.basis[i] ^= self.basis[j]
# 收集非零基向量到列表 p,从小到大(最高位从小到大)
self.p = []
for i in range(len(self.basis)):
if self.basis[i] != 0:
self.p.append(self.basis[i])
# 注意:此时 self.p 中元素是最高位递增的(因为 i 从小到大)
# 如果要从大到小,可以 reverse,但为了第k小方便,用从小到大
# 我们保留从小到大:p[0]最小,p[-1]最大
def get_kth(self, k, zero_included=True):
"""求第k小(k从0开始),
zero_included=True 时认为0是第0小,
否则第0小是非零最小 """
# 先标准化(若还未标准化)
if not hasattr(self, 'p'):
self.standardize()
m = len(self.p)
# 计算能表示的不同结果总数:2^m 个(包括0)
# 如果 zero_included 为 False,并且可以异或出0,则实际上非0结果个数仍是2^m-1? 注意区分
# 这里简化处理:默认 zero_included=True,即包括0。
# 如果题目要求排除0,则检查 k 是否超过范围
if zero_included:
# 包括0时,第k小对应k
total = 1 << m
if k >= total:
return -1 # 超出范围
ans = 0
# 注意 p 是从小到大(p[0]最小),那么k的二进制第0位对应p[0]
for i in range(m):
if (k >> i) & 1:
ans ^= self.p[i]
return ans
else:
# 不包括0:那么第0小应该是第一个非0结果
# 先判断能否得到0:如果 self.zero 为 True,则非空子集中有0;否则没有0
can_zero = self.zero
total = (1 << m) - (0 if can_zero else 0) # 实际上有0时,非0结果数 = 2^m -1 (因为排除0)
# 更严谨:非空子集结果数 = 2^m - (1 if can_zero else 0) ?不对,如果 can_zero 为真,那么0可以通过非空子集得到,所以非空结果包括0,但我们要排除0,所以总数减1
# 但第k小(不包括0)的范围是0..(total-1),其中total = 2^m - (1 if can_zero else 0)
# 这比较复杂,建议统一用包括0的做法,然后根据题意转换k
# 例如,题目要求第1小(非空最小),则调用 get_kth(1) 包括0,但得到的是第1小(包括0)即p[0]?
# 实际上,如果包括0,第1小是p[0](最小非零),所以直接取k=1即可(注意k从0开始,第1小对应k=1)
# 更简单:我们只实现包括0的,调用时传入k=题目要求第几小- (0 if 包括0 else 1)
pass
# 使用示例
if __name__ == "__main__":
arr = [3, 5, 6]
lb = LinearBasis()
for x in arr:
lb.insert(x)
print("最大值:", lb.get_max()) # 6
print("最小值(包括空集):", lb.get_min(True)) # 0
print("最小值(非空):", lb.get_min(False)) # 3
lb.standardize()
print("标准基:", lb.p) # [3,5] 或 [5,3]?取决于顺序,这里p从小到大是[3,5]
print("第0小:", lb.get_kth(0)) # 0
print("第1小:", lb.get_kth(1)) # 3
print("第2小:", lb.get_kth(2)) # 5
print("第3小:", lb.get_kth(3)) # 6
7. 相关指引
- 高斯消元:线性基的标准化过程本质上就是线性代数中的高斯消元,只不过这里是在二进制域(GF(2))上进行的。你可以进一步学习高斯消元解线性方程组,原理相通。
- 线性基的构建:本知识点假设你已经掌握了如何向线性基中插入一个数,如果不熟悉,请先阅读“线性基的插入与维护”部分。
- 异或的线性空间:线性基其实是所有数字张成的向量空间的一组基底,理解线性空间的概念能帮你更深入掌握它。
- 动态线性基:有时我们需要支持插入、删除、查询等操作,可以使用带删除的线性基(如线段树分治)。
掌握了这些,你就能轻松解决竞赛中“一堆数异或出某个结果”的问题了!
例题精讲
线性基中,若有n个数的数组,插入后得到的线性基大小为m,则以下说法正确的是?
利用线性基求一组数的异或最小值时,只需将所有非零基向量异或起来即可。
以下函数实现向线性基中插入一个数x,线性基数组为long long p[61](初始化为0)。请补全循环中的关键逻辑。
void insert(long long x) {
for (int i = 60; i >= 0; i--) {
if ((x >> i & 1) == 0) continue;
if (___ == 0) { p[i] = x; break; }
x ^= p[i];
}
}求一组数的异或第k小值(k从1开始)时,通常需要对线性基进行重构。以下关于重构步骤的描述正确的是?
以下函数求线性基的最大异或值,线性基已经通过插入操作构建为p数组(高位优先)。补全循环内的判断条件。
long long query_max() {
long long ans = 0;
for (int i = 60; i >= 0; i--) {
if (___ (ans ^ p[i]) > ans) ans ^= p[i];
}
return ans;
}