跳到正文

【学习笔记】线性基

被卡2026牛客多校2的B,得知线性基秒杀,遂学习

简介

ACM中,“线性基”通常特指异或线性基

线性基可以理解为:从一组向量中去掉冗余部分,保留一组线性无关的向量,并且这些向量仍能表示原向量组张成的整个空间。

异或线性基是一种擅长处理异或问题的数据结构.设值域为 \([1,N]\),就可以用一个长度为 \([log_2N]\) 的数组来描述一个线性基。

线性基可以高效解决:

  • 一组数的子集异或最大、最小、第 \(k\) 大、第 \(k\) 小值;
  • 某个数能否由若干元素异或得到;
  • 不同子集异或值的数量;

理解

考虑二维向量:

\[{ v_1=(1,0),\quad v_2=(0,1),\quad v_3=(1,1) } \]

因为:$ v3 \ = \ v1 \ + \ v2 $ ,即 \(v3\) 可以被空间中其他向量线性表示;

所以 \(v3\) 是冗余的。只保留:$ {v1, \ v2} $;

他们是线性无关的,即不能相互表示,但他们仍然可以表示原来三个向量张成的整个二维空间,因此它们构成一组基。

异或线性空间是同样的思想,只是:

  • 向量的每个分量只能是 0 或 1;
  • 加法变成按位异或;
  • 线性组合的系数只能是 0 或 1。

例如:$ 3 = 011, \ 5 = 101,\ 6 = 110 $

由于:$ 3 \oplus 5 = 6 $

所以 6 可以由 35 表示。集合 {3,5,6} 的一组线性基可以是 {3,5}

整数的每一个二进制位,都是线性基中向量的一个维度。

存储

设整数的二进制位数为 \(B\),使用数组 base[B] 存储线性基,其中 base[i] 表示最高位的 \(1\) 位于第 \(i\) 位的基向量。

每个最高位至多对应一个基向量。这与高斯消元中的主元类似:不同基向量拥有不同的最高位,因此它们线性无关。

插入 + 判断表示

插入整数 x 时,从最高位向最低位扫描:

  1. 如果 x 的第 \(i\) 位为 \(0\),则跳过该位;
  2. 如果第 \(i\) 位为 \(1\)basis[i] 为空,则令 basis[i] = x,插入成功;
  3. 如果 basis[i] 已存在,则执行 x ^= basis[i],消去 x 的第 \(i\) 位;
  4. 如果最终 x 变为 \(0\),说明它可以被原有线性基表示,无需插入。

插入过程可以理解为:

  • 分离x中当前未存在于线性基中的维度 basis[i],同时由于在异或空间中,去掉这一维度的 x 会变为 x ^= basis[i]

判断的过程也很简单,是插入的逆过程:

  • 在线性基中遍历所有维度;
  • 如果线性基中没有 x 中为 1 的一维,显然无法表示 x ,因为线性基无法消去这一维;
  • 否则与这一维的基 basis[i] 作异或;
void insert(ll x) {
  for (ll i = n; i >= 0; --i) {
    if (((x >> i) & 1ll) == 0) continue;
    if (base[i] == 0) {
      base[i] = x;
      tot++;
      return;
    }
    x ^= base[i];
  }
  uninsed = true;
}
bool check(ll x) {
  for (int i = n; i >= 0; --i) {
    if (((x >> i) & 1ll) == 0) continue;
    if (base[i] == 0) return false;
    x ^= base[i];
  }
  return true;
}

查询最值

查最大值的思路很简单:

  • 从高位到低位遍历线性基。如果异或当前基向量能使答案变大,就选择该向量;

这一贪心成立,是因为二进制数的高位比所有低位一起对答案的贡献更大。

查最小值同理:

  • 从低位到高位遍历线性基,如果出现了一个向量立刻停止;

这一贪心也成立,因为越后方的向量维护权值更大的维度;

注意,如果在插入时,当前需要插入的数 x 没有被插入,则最小值显然为 \(0\)

由于当前值 \(x\) 未被插入,则可知线性基中有一组 \(x\) 的线性表示,且 \(x\) 不在这组表示中,那么显然对于整个原数列来说最小值是0

ll qmx(ll x) {
  ll res = 0;
  for (int i = n; i >= 0;--i) {
    res = max(res, (res ^ base[i]));
  }
  return res;
}
ll qmn(ll x) {
  if (uninsed) return 0ll;
  for (ll i = 0; i <= n;++i) {
    if (base[i]) return base[i];
  }
}

查询第k小

首先,根据查询最小值中的特判,我们也可以知道答案为 \(0\) 的特殊情况;

随后,这一步需要我们进一步简化线性基;

原因是原线性基的高位向量仍然携带着低位,因此按二进制枚举之后不会按单调顺序排列,不便于我们查询。

所以很自然想到要用低位向量进一步消掉高位中的低位主元,且显然从低向高简化;

经过这一步操作后,设线性基内共有 cnt 个数,则它们共可以表示出 \(2^{cnt}\) 个数。

随后根据 \(k\) 进行二进制枚举即可。

ll query(ll k) {
  ll res = 0, cnt = 0;
  if (uninsed && --k == 0) return 0;
  for (ll i = 0; i <= n; ++i) {
    for (ll j = i - 1; j >= 0; --j)
      if (base[i] & (1ll << j)) base[i] ^= base[j];
    if (base[i]) t[cnt++] = t[i];
  }
  if (k >= (1ll << cnt)) return -1;
  for (ll i = 0;i < cnt;i++)
    if (k & (1ll << i)) res ^= t[i];
  return res;
}

合并线性基

合并两个集合时,只需把一个线性基中的所有非零向量插入另一个线性基:

void merge(const Basis& other) {
    for (int i = 0; i < 64; ++i) {
        if (other.basis[i] != 0) {
            insert(other.basis[i]);
        }
    }
}

这一性质使线性基可以与线段树、树上倍增等结构结合,处理区间或树上路径的异或查询。

完整代码

struct Basis {
  ll n, tot = 0;
  bool uninsed = false;
  vector<ll> base, t;
  Basis(ll _n) : n(_n), base(n + 1), t(n + 1) {}
  void insert(ll x) {
    for (ll i = n; i >= 0; --i) {
      if (((x >> i) & 1ll) == 0) continue;
      if (base[i] == 0) {
        base[i] = x;
        tot++;
        return;
      }
      x ^= base[i];
    }
    uninsed = true;
  }
  bool check(ll x) {
    for (int i = n; i >= 0; --i) {
      if (((x >> i) & 1ll) == 0) continue;
      if (base[i] == 0) return false;
      x ^= base[i];
    }
    return true;
  }

  ll qmx() {
    ll res = 0;
    for (int i = n; i >= 0;--i) {
      res = max(res, (res ^ base[i]));
    }
    return res;
  }
  ll qmn() {
    if (uninsed) return 0ll;
    for (ll i = 0; i <= n;++i) {
      if (base[i]) return base[i];
    }
    return 0ll;
  }
  ll query(ll k) {
    ll res = 0, cnt = 0;
    if (uninsed && --k == 0) return 0;
    for (ll i = 0; i <= n; ++i) {
      for (ll j = i - 1; j >= 0; --j)
        if (base[i] & (1ll << j)) base[i] ^= base[j];
      if (base[i]) t[cnt++] = base[i];
    }
    if (k >= (1ll << cnt)) return -1;
    for (ll i = 0;i < cnt;i++)
      if (k & (1ll << i)) res ^= t[i];
    return res;
  }// k-th smallest
  void merge(const Basis& other) {
    for (int i = 0; i <= n; ++i) {
      if (other.base[i] != 0) {
        insert(other.base[i]);
      }
    }
  }
};


posted @ 2026-07-22 20:24  MisaUdon  阅读(16)  评论(0)    收藏  举报