线性基

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

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

例题:P4151 [WC2011] 最大 XOR 和路径

给定一个 \(N\) 个点 \(M\) 条边的带权无向连通图,点编号为 \(1\)\(N\)。要求找一条从 \(1\) 号点到 \(N\) 号点的路径,使得路径上经过的所有边权的异或和最大。路径可以重复经过点和边,重复经过的边权需要重复计算异或和。

在一个无向连通图中,从 \(1\)\(N\) 的任意一条路径的异或和,都可以看作是由以下两部分组成的:

  • 一条基础路径:从 \(1\)\(N\) 的任意一条简单路径。
  • 若干个环:图中存在的任意环。

根据异或运算的性质 \(x \oplus x = 0\),可以从主路径上的某个点出发,绕过一个环后再原路返回。这样,往返路径上的边权会被异或两次从而抵消,最终的效果等同于在原路径的异或和基础上,异或了一个环的权值和。

因此,问题的核心转化为:在图中找到一条从 \(1\)\(N\) 的路径,并选择若干个环,使得它们的异或总和最大。

为了处理环的异或贡献,可以使用线性基

  • 通过 DFS 构建一棵 DFS 树,并记录每个节点 \(u\) 到根节点 \(1\) 的异或距离 \(d_u\)
  • 在 DFS 过程中,如果遇到一条非树边(返祖边)连接 \(u\)\(v\),其权值为 \(w\),则说明发现了一个环,该环的异或和为 \(d_u \oplus d_v \oplus w\)
  • 图中任何复杂的环都可以由这些基本环(由返祖边构成的环)异或组合得到,因此,只需要将所有基本环的异或和插入线性基即可。

选定 \(d_N\) 作为基础异或和(这代表了 DFS 树上从 \(1\)\(N\) 的路径),然后利用线性基对该值进行贪心最大化即可。

时间复杂度为 \(O(M \log V)\),其中 \(M\) 是边数,\(V\) 是边权的最大值。

参考代码
#include <cstdio>
#include <vector>
using namespace std;
using ll = long long;
const int N = 50005;
struct Edge {
    int to;
    ll w;
};
struct Basis {
    ll d[64];
    void insert(ll x) {
        for (int i = 59; i >= 0; i--) {
            if (!(x >> i)) continue;
            if (!d[i]) {
                d[i] = x;
                return;
            }
            x ^= d[i];
        }
    }
    ll query(ll x) {
        ll res = x;
        for (int i = 59; i >= 0; i--) {
            if ((res ^ d[i]) > res) {
                res ^= d[i];
            }
        }
        return res;
    }
};
vector<Edge> a[N];
bool vis[N];
ll dis[N];
Basis b;
// DFS 寻找环并构建线性基
void dfs(int u, ll cur) {
    vis[u] = true;
    dis[u] = cur;
    for (Edge e : a[u]) {
        int v = e.to;
        if (!vis[v]) {
            dfs(v, cur ^ e.w);
        } else { // 发现环,将环的异或和插入线性基
            b.insert(cur ^ e.w ^ dis[v]);
        }
    }
}
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i++) {
        int u, v;
        ll w;
        scanf("%d%d%lld", &u, &v, &w);
        a[u].push_back({v, w});
        a[v].push_back({u, w});
    }
    dfs(1, 0);
    // 从 1 到 n 的路径异或和 dis[n],在线性基中贪心求最大
    printf("%lld\n", b.query(dis[n]));
    return 0;
}

例题:P3292 [SCOI2016] 幸运数字

给定一棵拥有 \(n\) 个节点的树,每个节点有一个权值 \(G_i\)。有 \(q\) 次询问,每次询问给定两个点 \(x\)\(y\),要求在 \(x\)\(y\) 的唯一路径上选出若干点,使得这些点权值的异或和最大。
数据范围:\(n \le 2 \times 10^4\)\(q \le 2 \times 10^5\)\(G_i \le 2^{60}\)

本题的核心要求是“路径上的最大异或和”,这显然是线性基的典型应用场景。

对于每一个询问 \((x,y)\),可以提取路径上的所有权值,插入到一个线性基中,然后查询最大值。线性基插入的时间复杂度为 \(O(\log V)\),其中 \(V\) 是权值最大值,\(O(q \cdot n \cdot \log V)\) 的时间复杂度显然会超时。若使用倍增维护线性基,合并两个大小为 \(\log V\) 的线性基需要 \(O(\log^2 V)\) 的时间,倍增查询的总时间复杂度为 \(O(q \cdot \log n \cdot \log^2 V)\),还是会超时。

在序列上,常用“前缀线性基”来解决区间最大异或和问题。对于每个位置 \(i\),维护一个线性基,存储前 \(i\) 个数的信息,且每个基向量尽可能保留位置靠后的数。查询区间 \([L,R]\) 时,只需在第 \(R\) 个线性基中寻找位置 \(\ge L\) 的基向量即可。

本题可以将此技巧扩展到树上:从根节点开始 DFS,每个节点 \(u\) 的线性基继承自父节点。将 \(G_u\) 插入 \(u\) 的线性基时,记录其对应节点的深度,如果在某一位发生冲突,对比当前深度与原基向量的深度,保留深度较大者,并将被替换出的向量继续向低位尝试,这样可以保证线性基中存储的是该点到根路径上“深度尽可能大”的基向量。对于询问 \((x,y)\),设其最近公共祖先为 LCA,从 \(x\)\(y\) 的线性基中各自提取所有深度大于等于 LCA 的深度的基向量,将提取出的基向量(最多 \(60 \times 2\) 个)重新合并成一个临时线性基,在临时线性基中查询最大异或和。

预处理的时间复杂度为 \(O(n \log V)\),每次查询的时间复杂度为 \(O(q \cdot (\log n + \log^2 V))\),其中 \(\log n\) 是求 LCA 的时间复杂度,\(\log^2 V\) 是合并向量并构造临时线性基的时间复杂度。

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
using ll = long long;
const int N = 20005;
const int LOG = 15;
// 带有位置信息的线性基
struct Basis {
    ll d[61]; // 存储基向量
    int pos[61]; // 存储该基向量对应的节点深度
    Basis() {
        for (int i = 0; i <= 60; i++) {
            d[i] = pos[i] = 0;
        }
    }
    void insert(ll x, int p) {
        for (int i = 60; i >= 0; i--) {
            if (!(x >> i)) continue;
            if (!d[i]) {
                d[i] = x; 
                pos[i] = p;
                return;
            }
            if (p > pos[i]) { // 贪心:保留深度更大的向量
                swap(x, d[i]);
                swap(p, pos[i]);
            }
            x ^= d[i];
        }
    }
};  
ll g[N];
vector<int> a[N];
int dep[N], up[N][LOG];
Basis b[N];
// DFS 预处理深度、倍增 LCA 和位置线性基
void dfs(int u, int p, int d) {
    dep[u] = d;
    up[u][0] = p;
    b[u] = b[p];
    b[u].insert(g[u], d);
    for (int i = 1; i < LOG; i++) {
        up[u][i] = up[up[u][i - 1]][i - 1];
    }
    for (int v : a[u]) {
        if (v != p) dfs(v, u, d + 1);
    }
}
int lca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    int delta = dep[u] - dep[v];
    for (int i = LOG - 1; i >= 0; i--) {
        if ((delta >> i) & 1) u = up[u][i];
    }
    if (u == v) return u;
    for (int i = LOG - 1; i >= 0; i--) {
        if (up[u][i] != up[v][i]) {
            u = up[u][i]; v = up[v][i];
        }
    }
    return up[u][0];
}
int main()
{
    int n, q; scanf("%d%d", &n, &q);
    for (int i = 1; i <= n; i++) scanf("%lld", &g[i]);
    for (int i = 1; i < n; i++) {
        int u, v; scanf("%d%d", &u, &v);
        a[u].push_back(v); a[v].push_back(u);
    }
    dfs(1, 0, 1);
    while (q--) {
        int x, y; scanf("%d%d", &x, &y);
        int l = lca(x, y);
        int mind = dep[l];
        // 临时合并 x 和 y 路径上的有效基向量
        Basis res;
        auto add = [&](ll val) {
            for (int i = 60; i >= 0; i--) {
                if (!(val >> i)) continue;
                if (!res.d[i]) {
                    res.d[i] = val;
                    return;
                }
                val ^= res.d[i];
            }
        };
        // 只保留深度 >= LCA 深度的基向量,这覆盖了从 LCA 到节点的所有信息
        for (int i = 60; i >= 0; i--) {
            if (b[x].pos[i] >= mind) add(b[x].d[i]);
            if (b[y].pos[i] >= mind) add(b[y].d[i]); 
        }
        // 查询最大异或和
        ll ans = 0;
        for (int i = 60; i >= 0; i--) {
            if ((ans ^ res.d[i]) > ans) ans ^= res.d[i];
        }
        printf("%lld\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  阅读(30)  评论(0)    收藏  举报