Java教程

[算法入门]线性基

本文主要是介绍[算法入门]线性基,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

#0.0 前置知识

下文中所说的集合除特殊说明,均指“无符号整数集”。

#0.1 张成

设 \(T\subseteq S\),所有这样的子集 \(T\) 的异或和组成的集合称为 \(S\) 的张成,记作 \(\text{span}(S)\)。即在 \(S\) 中选出任意多个数,其异或和的所有可能的结果组成的集合。

#0.2 线性相关

对于一个集合 \(S\),如果存在一个元素 \(S_j\),使得 \(S\) 的在除去这个元素的集合 \(S'\) 的张成 \(\text{span}(S')\) 中包含 \(S_j\),那么就说 \(S\) 线性相关

相反的,如果集合 \(S\) 中不存在这样一个元素,那么就称 \(S\) 线性无关

说的简单一些,就是如果 \(S\) 线性相关,那么存在一个元素可以由其他元素异或得到。

由此得到一个性质:对于一个线性相关的集合 \(S\),去除掉可以由其他元素异或得到的元素后,集合的张成不变。

#1.0 线性基

#1.1 线性基的性质

  • 线性基的元素能相互异或得到原集合的元素的所有相互异或得到的值。
  • 线性基是满足性质 1 的最小的集合。
  • 线性基没有异或和为 0 的子集。
  • 线性基中每个元素的异或方案唯一,也就是说,线性基中不同的异或组合异或出的数都是不一样的。
  • 线性基中每个元素的二进制最高位互不相同。

#1.2 线性基的构造

线性基一般采用动态构造的方式,即从一个空的线性基开始,逐个插入某个数 \(t\)。

设 \(S\) 中用二进制表示下有 \(L\) 位,那么我们用一个长为 \(L\) 的数组 \(a\) 来保存该集合的线性基。

对于每一个 \(i\),\(a_i\) 只有以下两种可能:

  1. \(a_i=0\),且只有满足 \(j>i\) 的 \(a_j\) 的第 \(i\) 个二进制位上可能为 \(1\);
  2. \(a_i\ne0\),且
    • 整个 \(a\) 数组中只有 \(a_i\) 的第 \(i\) 个二进制位上为 \(1\);
    • \(a_i\) 更高的二进制位一定为 \(0\);
    • \(a_i\) 更低的二进制位可能为 \(1\);

注意,“整个 \(a\) 数组中只有 \(a_i\) 的第 \(i\) 个二进制位上为 \(1\)”这个性质是这个构造方案所特有的,并不是线性基必须具有的性质。

那么构造方案据显而易见了:对于一个原数组中的数 \(t\),从 \(t\) 最高位的 \(1\) 开始考虑,假设这是第 \(j\) 位,且 \(a_j\ne0\),那么就将 \(t\) 异或上 \(a_j\),也就是将 \(t\) 的第 \(j\) 个二进制位上的 \(1\) 消掉,直到找到一个二进制位 \(i\),满足当前 \(t\) 的最高位 \(1\) 为第 \(i\) 位,且 \(a_i=0\),那么我们就可以将 \(t\) 插入到 \(a_i\) 这个位置,插入时需要满足:

  • \(t\) 比 \(i\) 更高的二进制位一定为 \(0\);这一点不必考虑,在上面的过程中已经消掉了;
  • \(t\) 比 \(i\) 更低的二进制位 \(j\),若 \(a_j\ne0\),那么 \(t\) 的第 \(j\) 位必须为 \(0\);对于这个要求,我们可以枚举 \(t\) 的更低的为一的二进制位 \(k\),如果 \(a_k\ne0\),那么就让 \(t\) 异或上 \(a_k\);
  • 整个 \(a\) 数组中只有 \(a_i\) 的第 \(i\) 个二进制位上为 \(1\);对于 \(j<i\) 的情况无需考虑,我们来看 \(j>i\) 的情况,对于每一个这样的情况,我们让 \(a_j\) 异或上 \(t\) 即可;

那么不难写出上面构造的代码。

#1.3 代码实现

void insert(ll t) {
    for (int i = 51; i >= 0; i --) {
        if (!(t & (1ll << i))) continue;
        if (a[i]) t ^= a[i];
        else {
            /*注意,这里两个循环的顺序不能交换*/
            for (int j = 0; j < i; j ++)
              if (t & (1ll << j)) t ^= a[j];
            for (int j = 51; j > i; j --)
              if (a[j] & (1ll << i)) a[j] ^= t;
            a[i] = t; break;
        }
    }
}

时间复杂度为 \(O(\log n)\) 的。

#1.4 线性基的应用

注意到上面的构造方式,对任意的 \(a_i\ne0\),\(a_i\) 都是与线性基中其他的数或原集合中的数异或得来,而异或这个操作显然是可逆的,所以线性基中的任意一个数都是可以由原集合中的数异或得来,同样可以用 \(a\) 中的数异或得到原集合中的数。

#1.4.1 最大异或值

按上面的构造方案,直接将所有的 \(a\) 异或得到的值便是整个数组的最大异或值。

这一点并不难理解,因为在上面我们保证了对于任意的 \(a_i\ne0\),仅有 \(a_i\) 的第 \(i\) 个二进制位不为 \(0\),所以可以保证将每异或一个不为零的 \(a_k\),二进制下第 \(k\) 位会变为 \(1\),且永远不会再变回 \(0\);

至于 \(a_k=0\) 的情况,意味着不存在一种方案使得第 \(k\) 位可以在不被 \(a_j(j>k)\) 控制的情况下单独为 \(1\),如果强行让第 \(j\) 位为 \(1\),得到的答案不会更优,因为可能会导致更高位的 \(1\) 消失。

#1.4.2 最小非零异或值

显然,线性基中最小的、不为零的 \(a_k\) 即为答案。

#1.4.3 查询异或可行性

对于一个数 \(x\),我们想要知道它能否由 \(S\) 中的数异或得到,我们可以先构建出 \(S\) 的线性基 \(a\),再去尝试将 \(x\) 插入 \(a\),如果最终 \(x\) 没有被插入,即被消为 \(0\),那么意味着可以被异或得到。相反则不能。

#2.0 另一种构造

我们还有另一种构造方式,但是会失去“整个 \(a\) 数组中只有 \(a_i\) 的第 \(i\) 个二进制位上为 \(1\)”这个特殊性质,但相对来说构造要更简洁一些。与上面的构造方案唯一的区别就是不需要维护该性质,这里不多赘述。

void insert(ll t) {
    for (int i = 51; i >= 0; i --) {
        if (!(t & (1ll << i))) continue;
        if (a[i]) t ^= a[i];
        else {a[i] = t; break;}
    }
}

要注意,对于这种构造方式,若我们要求最大异或值,在做异或时需要判断异或后得到的值是否更大。

#3.0 更多操作

#3.1 线性基合并

两个集合的线性基合并后得到的线性基为两个集合的并的线性基。我们只需要将其中一个线性基中的数全部插入另一个线性基即可。

参考资料

[1] 线性基学习笔记 - Menci

[2] 线性基小记 - command_block

[3] 线性基 - OI Wiki

这篇关于[算法入门]线性基的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!