线性基学习笔记

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

定义

异或和

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

张成

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

线性相关

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

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

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

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

线性基

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

  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;
    }
};