线性基

在线性代数中,线性基是一个向量空间的基。在算法竞赛中,通常讨论两种线性基:

  1. 异或线性基:针对整数的异或(XOR)运算。
  2. 实数线性基:针对实数向量的加法和数乘运算(通常通过高斯消元实现)。

异或线性基

对于一组数 \(A = \{ a_1, a_2, \dots, a_n \}\),它们的异或线性基 \(B = \{ b_1, b_2, \dots, b_k \}\) 满足:

  1. 等价性\(A\) 中的任意元素都可以由 \(B\) 中的元素异或得到;反之,\(B\) 中的任意元素也可以由 \(A\) 中的元素异或得到。
  2. 独立性\(B\) 中任意非空子集的异或和不为 \(0\)
  3. 最小性\(B\) 是满足上述条件的最小集合。

线性基中元素的个数是唯一的,但可能有不同的具体方案。

在线性基的构造过程中,通常采用一种“从高位到低位”的贪心尝试策略。定义数组 \(d_i\) 存储最高有效位为第 \(i\) 位的基元素,用于存储已经插入线性基中的数。

对于一个要插入的非负整数 \(x\)(当前待处理的数值),从二进制最高位向第 0 位遍历。若 \(x\) 的第 \(i\) 位为 1:若线性基数组 \(d_i\) 为空,则将 \(d_i\) 赋值为 \(x\),插入成功并退出;若 \(d_i\) 不为空,则执行 \(x \gets x \oplus d_i\),利用 \(d_i\) 消去 \(x\) 的第 \(i\) 位,继续处理剩下的 \(x\)。若插入过程中 \(x\) 最终变为 0,则说明 \(x\) 无法插入线性基(它可由已有基元素异或得到)。

贪心策略保证了 \(d_i\) 的最高有效位一定是 \(i\),因此对于任意索引 \(j\)(另一位的索引),\(d_i\) 无法被其他 \(d_j \ (j \lt i)\) 异或得到,保证了基的独立性。

ll d[64];
bool insert(ll x) {
    // 假设 x 是 long long 范围内的非负整数
    for (int i = 62; i >= 0; i--) {
        if (!(x >> i)) continue;
        if (!d[i]) {
            d[i] = x;
            return true;
        }
        x ^= d[i];
    }
    return false; // x 可以由已有基异或得到
}

例题:P3812 【模板】线性基

给定 \(n\) 个整数,要求从这些数中选取任意多个数,使得它们的异或和最大。

先求出线性基数组 \(d\),利用贪心策略查询最大异或和:初始化答案为 0,从最高位到最低位遍历线性基数组 \(d\),如果将答案异或上当前的 \(d_i\) 能变得更大,就更新答案为异或后的值,否则跳过当前的 \(d_i\)

证明:由于 \(d_i\) 的最高位是 \(i\),且在遍历到第 \(i\) 位时,当前答案的高于 \(i\) 的位已经确定。如果异或 \(d_i\) 能让第 \(i\) 位从 0 变成 1,那么无论低位如何变化,最终数值一定会变大。

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

参考代码
#include <cstdio>
using ll = long long;
ll d[50];
void insert(ll x) {
    for (int i = 49; i >= 0; i--) {
        if (!(x >> i)) continue;
        if (!d[i]) {
            d[i] = x;
            return;
        }
        x ^= d[i];
    }
}
int main()
{
    int n; scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        ll x; scanf("%lld", &x);
        insert(x);
    }
    ll ans = 0;
    for (int i = 49; i >= 0; i--) {
        if ((ans ^ d[i]) > ans) {
            ans ^= d[i];
        }
    }
    printf("%lld\n", ans);
    return 0;
}

例题:P4570 [BJWC2011] 元素

给定 \(N\) 种矿石,每种矿石有一个元素序号 \(\text{Number}_i\) 和魔力值 \(\text{Magic}_i\)。如果选出的矿石子集中存在一个非空子集,其元素序号的异或和为 0,则会发生“魔法抵消”。求在不发生抵消的前提下,能够获得的魔力值之和的最大值。

题目核心要求是选出的矿石集合中,任何子集的异或和都不能为 0,这在数学上被称为线性无关。可以将每个元素序号看作二进制空间里的一个“向量”,而“异或和为 0”就对应着这些向量“线性相关”,目标是选出一个价值最大的线性无关子集。

可以直接使用贪心算法,将所有矿石按照魔力值从大到小排序。依次遍历排序后的矿石,尝试将其元素序号插入当前的线性基。如果能够成功插入线性基(即它不能被当前基中的元素异或表示),就保留它并累加魔力值。

线性基不管按什么顺序依次考虑每个数,最终线性基的大小是一定的。那么如果最优方案在某个高魔力值矿石可以选时没有将其选入,其最终方案中这个没选的矿石的序号可以被线性基异或表示,此时必定存在某一个线性基中的矿石(低魔力值)被这个未选的矿石替换掉之后依然能构成线性基。

参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
using ll = long long;
const int N = 1005;
// 矿石结构体
struct Ore {
    ll num;
    int magic;
};
Ore a[N];
// 比较函数:按魔力值从大到小排序
bool cmp(const Ore& x, const Ore& y) {
    return x.magic > y.magic;
}
// 线性基结构体
struct LinearBasis {
    ll d[64];
    LinearBasis() {
        for (int i = 0; i < 64; i++) d[i] = 0;
    }
    // 尝试插入 x,返回是否插入成功(即 x 是否与基线性无关)
    bool insert(ll x) {
        for (int i = 59; i >= 0; i--) {
            if (!(x >> i)) continue;
            if (!d[i]) {
                d[i] = x; 
                return true;
            }
            x ^= d[i];
        }
        return false;
    }
};  
int main()
{
    int n; scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%lld%d", &a[i].num, &a[i].magic);
    }
    // 贪心策略:优先选择魔力值大的矿石
    sort(a, a + n, cmp);
    LinearBasis lb;
    int ans = 0;
    for (int i = 0; i < n; i++) {
        // 如果该矿石的编号能插入线性基,说明它与之前选中的矿石不冲突
        if (lb.insert(a[i].num)) {
            ans += a[i].magic;
        }
    }
    printf("%d\n", ans);
    return 0;
}

例题:P3857 [TJOI2008] 彩灯

给定 \(N\) 盏初始不亮的灯和 \(M\) 个开关,每个开关控制一个特定的灯集合,按下开关后,该集合内所有灯的状态反转(亮变灭,灭变亮)。求最终可以组合出的不同灯光样式的总数,结果对 2008 取模。

将每盏灯的状态看作一个二进制位:0 表示灭,1 表示亮。

  • 一排 \(N\) 盏灯的状态可以用一个长度为 \(N\) 的二进制数表示。
  • 按下某个开关的操作,等价于将该开关代表的二进制掩码与当前的灯光状态进行按位异或运算。
  • 同一个开关按两次等于没按,按三次等于按一次。因此,每个开关只有“按”或“不按”两种状态。

问题转化为:给定 \(M\) 个二进制数,求它们通过异或运算能生成的不同数值的个数,这是异或线性基的典型应用。

根据线性基的性质,如果一个集合的线性基大小为 \(d\),那么该集合中的元素通过异或可以生成 \(2^d\) 个不同的值。因为线性基中的 \(d\) 个元素是线性无关的,每一个子集(共 \(2^d\) 个)对应的异或和都各不相同。

参考代码
#include <cstdio>
using ll = long long;
const int MOD = 2008;
ll d[55];
char s[55];
int cnt; // 线性基的大小
// 尝试向线性基中插入一个数 x
void insert(ll x) {
    for (int i = 49; i >= 0; i--) {
        if (!(x >> i)) continue;
        if (!d[i]) {
            d[i] = x;
            cnt++;
            return; 
        }
        x ^= d[i];
    }
}
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i++) {
        scanf("%s", s);
        ll mask = 0;
        // 将字符串转化为二进制掩码
        for (int j = 0; j < n; j++) {
            if (s[j] == 'O') {
                mask |= (1ll << j);
            }
        }
        insert(mask);
    }
    // 可变换出的样式数目为 2^cnt
    // 注意题目要求对 2008 取模
    int ans = 1;
    for (int i = 0; i < cnt; i++) ans = ans * 2 % MOD;
    printf("%d\n", ans);
    return 0;
}

例题:P4839 P 哥的桶

维护一组序列(桶),支持两种操作:

  1. 单点修改(插入):向某个桶中加入一个具有特定价值的球。
  2. 区间查询:在指定范围 \([l,r]\) 内的所有球中,选取若干个球使得它们的价值异或和最大。

最大异或和问题通常使用异或线性基解决,而涉及到区间查询,可以联想到使用线段树

建立一棵线段树,树上的每个节点都维护一个线性基,这个线性基代表了该节点所覆盖的区间 \([L,R]\) 内所有球的价值所能构成的异或空间。

当在第 \(k\) 号桶放入一个价值为 \(x\) 的球时,在线段树中从根节点到叶子节点 \(k\) 的路径上,所有覆盖了 \(k\) 的节点对应的线性基都需要插入 \(x\)。由于题目中球是“只增不减”的,不需要删除操作。线性基支持动态插入,插入的复杂度为 \(O(\log V)\),其中 \(V\) 是价值范围,那么单次更新的复杂度是 \(O(\log m \cdot \log V)\)

查询区间 \([l,r]\) 时,线段树会拆分为 \(O(\log m)\) 个节点,需要将这些节点中的线性基合并成一个临时的线性基。将一个大小为 \(d\) 的线性基合并到另一个线性基中,只需要将其中的每一个非零元素依次插入目标线性基,时间复杂度为 \(O(d^2)\),即 \(O(\log^2 V)\),那么单次查询的复杂度是 \(O(\log m \cdot \log^2 V)\)

参考代码
#include <cstdio>
const int N = 50005;
// 线性基结构体,用于维护异或空间并查询最大异或和
struct LinearBasis {
    int d[31]; // 存储基向量,2^31-1 对应 31 位 (0-30)
    LinearBasis() {
        for (int i = 0; i < 31; i++) d[i] = 0;
    }
    // 插入一个数值到线性基中
    void insert(int x) {
        for (int i = 30; i >= 0; i--) {
            if (!(x >> i)) continue;
            if (!d[i]) {
                d[i] = x;
                return;
            }
            x ^= d[i];
        }
    }
    // 查询当前线性基能异或出的最大值
    int query() {
        int res = 0;
        for (int i = 30; i >= 0; i--) {
            if ((res ^ d[i]) > res) {
                res ^= d[i];
            }
        }
        return res;
    }
};
LinearBasis tr[N * 4];
// 线段树更新:向包含位置 k 的所有区间节点插入值 x
void update(int p, int l, int r, int k, int x) {
    tr[p].insert(x);
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (k <= mid) update(p * 2, l, mid, k, x);
    else update(p * 2 + 1, mid + 1, r, k, x);
}
// 线段树查询:合并 [ql, qr] 区间内的所有线性基
void query(int p, int l, int r, int ql, int qr, LinearBasis& res) {
    if (ql <= l && r <= qr) {
        for (int i = 30; i >= 0; i--) {
            if (tr[p].d[i]) res.insert(tr[p].d[i]);
        }
        return;
    }
    int mid = (l + r) >> 1;
    if (ql <= mid) query(p * 2, l, mid, ql, qr, res);
    if (qr > mid) query(p * 2 + 1, mid + 1, r, ql, qr, res);
}
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    while (n--) {
        int op, a, b; scanf("%d%d%d", &op, &a, &b);
        if (op == 1) {
            // 在 a 号桶丢进价值为 b 的球
            update(1, 1, m, a, b);
        } else {
            // 查询 l 到 r 号桶之间的最大异或和
            LinearBasis res;
            query(1, 1, m, a, b, res);
            printf("%d\n", res.query());
        }
    }
    return 0;
}

实数线性基

\(m\) 为一个正整数,表示空间的维度。定义一个 \(m\) 维实数向量 \(\mathbf{v}\) 为一个由 \(m\) 个实数组成的序列,记作 \(\mathbf{v} = (a_1, a_2, \dots, a_m)\),其中 \(a_j \in \mathbb{R}\)\(\mathbb{R}\) 表示全体实数集,\(j \in \{1, 2, \dots, m\}\) 是维度的索引)。

由所有此类向量构成的集合称为 \(m\) 维实向量空间,记作 \(\mathbb{R}^m\)。在这个空间中,可以定义:

  • 向量加法\(\mathbf{u} + \mathbf{v} = (u_1+v_1, u_2+v_2, \dots, u_m+v_m)\)
  • 数乘运算\(k\mathbf{v} = (ka_1, ka_2, \dots, ka_m)\),其中 \(k \in \mathbb{R}\) 是一个标量。

给定一组向量集合 \(S = \{\mathbf{v}_1, \mathbf{v}_2, \dots, \mathbf{v}_n\}\)(其中 \(n\) 为向量的个数)。
如果存在一组不全为零的实数 \(c_1, c_2, \dots, c_n\)(称为系数),使得:

\[\sum_{i=1}^n c_i \mathbf{v}_i = \mathbf{0} \]

(其中 \(\mathbf{0} = (0, 0, \dots, 0)\) 是零向量),则称集合 \(S\)线性相关的。反之,若上述等式仅在 \(c_1=c_2=\dots=c_n=0\) 时成立,则称 \(S\)线性无关的。

任何一个向量 \(\mathbf{u}\) 如果能表示为 \(\mathbf{u} = \sum_{i=1}^n c_i \mathbf{v}_i\),则称 \(\mathbf{u}\)\(S\) 的一个线性组合

集合 \(S\) 的一个线性基 \(B = \{\mathbf{b}_1, \mathbf{b}_2, \dots, \mathbf{b}_r\}\)\(S\) 的一个子集,满足:

  1. \(B\) 中的向量是线性无关的。
  2. \(S\) 中的任何向量都可以由 \(B\) 中的向量线性组合得到。

这里 \(r\) 称为集合 \(S\)秩 (Rank),且 \(r \le \min(n, m)\)

在算法竞赛中,通常使用类似于异或线性基的动态维护方式,但底层逻辑是高斯消元。

维护一个数组 \(d_{1 \dots m}\),其中 \(d_j\) 存储一个向量,该向量的第一个非零分量出现在第 \(j\) 维(即主元位置)。

对于待插入的向量 \(\mathbf{x} = (x_1, x_2, \dots, x_m)\)

  1. 遍历 \(j\) 从 1 到 \(m\)
    • \(|x_j| \lt \epsilon\)(其中 \(\epsilon\) 是预定义的精度极小值),则跳过该维度。
    • \(d[j]\) 为空:
      • \(d[j] \gets \mathbf{x}\),插入成功并返回。
    • \(d[j]\) 已存在:
      • 计算系数 \(f = x_j / d_{j,j}\)\(d_{j,j}\) 表示向量 \(d_j\) 的第 \(j\) 维分量)。
      • 执行向量消元:\(\mathbf{x} \gets \mathbf{x} - f \cdot d[j]\)
  2. 若遍历结束 \(\mathbf{x}\) 变为 \(\mathbf{0}\),则说明原向量线性相关,插入失败。

插入一个向量需 \(O(m^2)\)(遍历 \(m\) 维,每维消元需 \(O(m)\)),处理 \(n\) 个向量的总时间复杂度为 \(O(nm^2)\)

例题:P3265 [JLOI2015] 装备购买

给定 \(n\) 件装备,每件装备有 \(m\) 个属性(可看作 \(m\) 维向量 \(\mathbf{z_i}\))和一个花费 \(c_i\)。如果一件装备的属性可以由已购买的装备属性线性组合得到,则这件装备是多余的。目标是在保证购买装备数量最多的前提下,使总花费最小。

将所有装备按照花费 \(c_i\) 从小到大进行排序,遍历排序后的装备,尝试将其属性向量插入线性基。本题精度要求较高,使用 long double 类型存储。

参考代码
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
using ld = long double;
const int N = 505;
const ld EPS = 1e-6;
struct Item {
    ld a[N];
    int c;
};
Item s[N];
bool used[N];
ld d[N][N];
bool cmp(const Item& x, const Item& y) {
    return x.c < y.c;
}
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    // 读取装备属性
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            scanf("%Lf", &s[i].a[j]);
        }
    }
    // 读取装备花费
    for (int i = 0; i < n; i++) {
        scanf("%d", &s[i].c);
    }
    // 贪心策略:按花费从小到大排序
    sort(s, s + n, cmp);
    int cnt = 0, cost = 0;
    // 线性基维护(高斯消元)
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (fabs(s[i].a[j]) > EPS) {
                if (!used[j]) {
                    // 线性基维护(高斯消元)
                    used[j] = true;
                    cnt++;
                    cost += s[i].c;
                    for (int k = j; k < m; k++) {
                        d[j][k] = s[i].a[k];
                    }
                    break;
                } else {
                    // 利用已有的基向量进行消元
                    ld f = s[i].a[j] / d[j][j];
                    for (int k = j; k < m; k++) {
                        s[i].a[k] -= f * d[j][k];
                    }
                }
            } 
        }
    }
    // 输出最多购买数量和最小花费
    printf("%d %d\n", cnt, cost);
    return 0;
}
posted @ 2026-03-21 12:02  RonChen  阅读(1)  评论(0)    收藏  举报