线性基学习笔记

线性基是竞赛中常用来解决子集异或一类题目的算法。

定义

异或和

设 为一无符号整数集(下文中除非特殊说明,集合均指「无符号整数集」),定义其异或和 。

张成

设 ,所有这样的子集 的异或和组成的集合称为集合 的张成,记作 。即,在 中选出任意多个数,其异或和的所有可能的结果组成的集合。

线性相关

对于一个集合 ,如果存在一个元素 ,使得, 在去除这个元素后得到的集合 的张成 中包含 ,即,,则称集合 线性相关。

更形象地,可以表示为,存在一个元素 ,可以用其它若干个元素异或起来得到。

相对的,如果不存在这样的元素 ,则称集合 线性无关。

一个显然的结论是,对于这个线性相关的集合 ,去除这个元素后,集合的张成不变。

线性基

我们称集合 是集合 的线性基,当且仅当:

  1. ,即 是 的张成的子集;
  2. 是线性无关的。

集合 中元素的个数,称为线性基的长度。

线性基有以下基本性质:

  1. 是极小的满足线性基性质的集合,它的任何真子集都不可能是线性基;
  2. 中的任意元素都可以唯一表示为 中若干个元素异或起来的结果。

构造与性质

这里讲解一种线性基的构造方法,及其特殊性质,之后,我们将讨论这种构造方法的正确性。下文中「线性基」均指这种方法构造出的线性基。

设集合 中最大的数在二进制意义下有 位,我们使用一个 的数组 来储存线性基。

这种线性基的构造方法保证了一个特殊性质,对于每一个 , 有以下两种可能:

  1. ,并且
    • 只有满足 的 (即,位于 后面的所有 )的第 个二进制位可能为 ;
  2. ,并且
    • 整个 数组中只有 的第 个二进制位为 ;
    • 更高的二进制位( 的二进制位)一定为 ;
    • 更低的二进制位( 的二进制位)可能为 ;

我们称第 位存在于线性基中,当且仅当 。

举个例子,我们的构造方法可能得到一个这样的数组 (右侧为最低二进制位),其中被标为蓝色的元素为上述的第 2 种情况,绿色的元素为上述的第 1 种情况。

举两个反例:

首先,线性基是动态构造的,我们只需要从空的 开始,每次考虑在一个已存在的线性基中插入一个数 即可。

从 最高位上的 开始考虑,设这是第 位,如果这一位已经存在于线性基中,则我们需要将 中的这一位消掉(将 异或上 ),才可以继续插入(因为只有 的第 位可以为 )。 如果这一位不存在于线性基中,则可以将 插入到 的位置上,但插入时需要保证:

  1. 中比第 位更高的已经存在于线性基中的二进制位上必须为 ,而这时候 的最高位上的 一定是第 位(更高位上即使原本有,也被消掉了),所以无需考虑;
  2. 中比第 位更低的已经存在于线性基中的二进制位上必须为 ,对于这一点,我们可以枚举 中为 的这些二进制位 ()对应的元素 ,并用类似上面的方式将 异或上 ,以消掉这些位上的 ;
  3. 中必须只有 的第 位为 :
    • 中在 前面的元素的第 位必须为 ,这一点必定已经满足,假设存在一个 满足 的第 位为 ,则 在插入时就应该被插入到 的位置上,所以无需考虑;
    • 中在 后面的元素的第 位必须为 ,对于这一点,我们可以枚举 后面的元素 (),将每个第 位为 的 异或上 。

流程

  • 逆序枚举 所有为 的二进制位 ,对于每个
    • 如果 ,则令
    • 如果 ,则
      • 枚举 ,如果 的第 位为 ,则令
      • 枚举 ,如果 的第 位为 ,则令
      • 令 ,结束插入过程

代码

const int MAXL = 60;

struct LinearBasis
{
    long long a[MAXL + 1];

    LinearBasis()
    {
        std::fill(a, a + MAXL + 1, 0);
    }

    void insert(long long t)
    {
        // 逆序枚举二进制位
        for (int j = MAXL; j >= 0; j--)
        {
            // 如果 t 的第 j 位为 0,则跳过
            if (!(t & (1ll << j))) continue;

            // 如果 a[j] != 0,则用 a[j] 消去 t 的第 j 位上的 1
            if (a[j]) t ^= a[j];
            else
            {
                // 找到可以插入 a[j] 的位置

                // 用 a[0...j - 1] 消去 t 的第 [0, j) 位上的 1
                // 如果某一个 a[k] = 0 也无须担心,因为这时候第 k 位不存在于线性基中,不需要保证 t 的第 k 位为 0
                for (int k = 0; k < j; k++) if (t & (1ll << k)) t ^= a[k];

                // 用 t 消去 a[j + 1...L] 的第 j 位上的 1
                for (int k = j + 1; k <= MAXL; k++) if (a[k] & (1ll << j)) a[k] ^= t;

                // 插入到 a[j] 的位置上
                a[j] = t;

                // 不要忘记,结束插入过程
                return;
            }

            // 此时 t 的第 j 位为 0,继续寻找其最高位上的 1
        }

        // 如果没有插入到任何一个位置上,则表明 t 可以由 a 中若干个元素的异或和表示出,即 t 在 span(a) 中
    }
};

证明

首先,根据归纳法,可以得出其特殊性质是满足的。

我们枚举 中的所有元素作为 ,分别插入,并将数组 转化为一个集合 (只需要去除重复的为 的元素即可,显然不为 的元素不会重复),此时 就是 的线性基:

首先,在插入的过程中,我们每次找到 最高位上的 ,然后把它消去,如果最终全部被消去,则表示要插入的 已经可以由 中一些元素的异或和表示出,此时不需要插入。这样保证了 是线性无关的。 对于被实际插入到 中的元素,它们在实际插入之前做了一些变换,这些变换都是使它异或上 中已存在的元素,或者使 中已存在的元素异或上它 —— 这些变换都是可逆的,所以用 中一些元素的异或和可以表示出这些(实际被插入的)元素。

同时,显然该算法的时间复杂度为 单次插入,空间复杂度同样为 。

合并

两个集合的线性基可以在 的时间内进行合并,合并后得到的线性基为两个集合的并的线性基。

合并只需要将一个线性基中的所有元素插入到另一个线性基中即可。

void mergeFrom(const LinearBasis &other) {
    for (int i = 0; i <= MAXL; i++) insert(other.a[i]);
}

static LinearBasis merge(const LinearBasis &a, const LinearBasis &b) {
    LinearBasis res = a;
    for (int i = 0; i <= MAXL; i++) res.insert(b.a[i]);
    return res;
}

应用

线性基最基本的应用,就是子集最大异或和问题:

给一个集合 ,求它的一个子集 ,使得 最大,求出这个最大值。

首先,求出 的线性基 ,问题转化为选择 的一个子集。从高到低考虑在线性基中的二进制位 ,如果第 位存在于线性基中,则考虑到线性基中只有惟一一个元素的第 位为 ,所以之前加入到 中的元素的异或和的第 位一定为 ,即,将这个元素加入到 中一定会使答案更大 —— 所以,求出线性基中所有元素的异或和,即为答案。

long long queryMax()
{
    long long res = 0;
    for (int i = 0; i <= MAXL; i++) res ^= a[i];
    return res;
}

模板

一

不显式构造出集合 ,支持动态插入。

struct LinearBasis
{
    long long a[MAXL + 1];

    LinearBasis()
    {
        std::fill(a, a + MAXL + 1, 0);
    }

    LinearBasis(long long *x, int n)
    {
        build(x, n);
    }

    void insert(long long t)
    {
        for (int j = MAXL; j >= 0; j--)
        {
            if (!t) return;
            if (!(t & (1ll << j))) continue;

            if (a[j]) t ^= a[j];
            else
            {
                for (int k = 0; k < j; k++) if (t & (1ll << k)) t ^= a[k];
                for (int k = j + 1; k <= MAXL; k++) if (a[k] & (1ll << j)) a[k] ^= t;
                a[j] = t;
                break;
            }
        }
    }

    // 数组 x 表示集合 S,下标范围 [1...n]
    void build(long long *x, int n)
    {
        std::fill(a, a + MAXL + 1, 0);

        for (int i = 1; i <= n; i++)
        {
            insert(x[i]);
        }
    }

    long long queryMax()
    {
        long long res = 0;
        for (int i = 0; i <= MAXL; i++) res ^= a[i];
        return res;
    }

    void mergeFrom(const LinearBasis &other)
    {
        for (int i = 0; i <= MAXL; i++) insert(other.a[i]);
    }

    static LinearBasis merge(const LinearBasis &a, const LinearBasis &b)
    {
        LinearBasis res = a;
        for (int i = 0; i <= MAXL; i++) res.insert(b.a[i]);
        return res;
    }
};

二

显式构造出集合 (代码中的 ),不支持动态插入。

struct LinearBasis
{
    std::vector<long long> v;
    int n; // 原有集合 S 的大小

    // 数组 x 表示集合 S,下标范围 [1...n]
    void build(long long *x, int n)
    {
        this->n = n;
        std::vector<long long> a(MAXL + 1);

        for (int i = 1; i <= n; i++)
        {
            long long t = x[i];

            for (int j = MAXL; j >= 0; j--)
            {
                if ((t & (1ll << j)) == 0) continue;

                if (a[j]) t ^= a[j];
                else
                {
                    for (int k = 0; k < j; k++) if (t & (1ll << k)) t ^= a[k];
                    for (int k = j + 1; k <= MAXL; k++) if (a[k] & (1ll << j)) a[k] ^= t;

                    a[j] = t;
                    break;
                }
            }
        }

        v.clear();
        for (int i = 0; i <= MAXL; i++) if (a[i]) v.push_back(a[i]);
    }

    long long queryMax()
    {
        long long x = 0;
        for (size_t i = 0; i < v.size(); i++) x ^= v[i];
        return x;
    }
};