1598 字
8 分钟
竞赛算法(8) 线性代数
2025-12-01
无标签

线性基#

通过有限的线性基, 可以描述无限的线性空间.

算法竞赛中常用的线性基有两种, 实线性空间中的Rn\mathbb{R}^n实数线性基和布尔域线性空间Z2n\mathbb{Z}_2^n中的异或线性基. (布尔域线性空间中加法为异或, 数乘为与).

以异或线性基为例, 我们可以根据一组布尔序列X={x1,x2,...,xm}X = \{x_1, x_2, ..., x_m\}构造出一组异或线性基B={b1,b2,...,bn}B=\{b_1, b_2, ..., b_n\}. 这组基有如下性质:

  1. 任意非空子集的异或和不为0.
  2. XX中的任意元素xx, 都可以在BB中取出若干元素使其异或和为xx.
  3. BB是满足性质1和2的极小向量组.

:::[异或线性基的典型用法]

  1. 判断一个数是否能表示成某数集子集的异或和;
  2. 求一个数表示成某数集子集异或和的方案;
  3. 求某数集的最大/最小/第k大/第k小子集异或和;
  4. 求某数在某数集子集异或和中的排名. :::

矩阵快速幂/矩阵加速递推#

矩阵加速递推可以把线性递推式由O(n)O(n)优化到O(k3logn)O(k^3\log n). 其中k为递推式的阶数.

如果一个递推式是线性的, 我们可以把它写成矩阵的形式, 一次递推即相当于左乘一次转移矩阵. 我们需要n次递推, 由于矩阵有结合律, 这相当于我们左乘转移矩阵的n次幂. 我们可以先使用快速幂算法求出矩阵的n次幂, 再用初始向量左乘得到结果.

以计算斐波那契数列第n项fnf_n为例.

  1. 构造状态向量. fi=fi1+fi2f_i = f_{i - 1} + f_{i - 2}. 我们知道他的初始态有两项f0=0,f1=1f_0 = 0, f_1 = 1. 这暗示我们状态有两项:
Fn=[fnfn1]F_n = \begin{bmatrix} f_n \\ f_{n-1} \end{bmatrix}
  1. 构建转移矩阵:
fn=1fn1+1fn2f_n = 1 \cdot f_{n-1} + 1 \cdot f_{n-2}fn1=1fn1+0fn2f_{n-1} = 1 \cdot f_{n-1} + 0 \cdot f_{n-2}

于是转移矩阵:

M=[1110]M = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}

转移可以写成:

Fi=MFi1F_i = MF_{i - 1}
  1. 做快速幂即可.
// 列向量
template <class S, int N>
struct ColVector {
array<S, N> a;
ColVector(S def = S()) {
fill(a.begin(), a.end(), def);
}
S& operator[](int i) {
return a[i];
}
const S& operator[](int i) const {
return a[i];
}
};
// 矩阵
template <typename S, int N>
struct Matrix {
array<array<S, N>, N> a;
Matrix(S def = S()) {
for (int i = 0; i < N; i++) {
fill(a[i].begin(), a[i].end(), def);
}
}
S* operator[](int i) {
return a[i].data();
}
const S* operator[](int i) const {
return a[i].data();
}
// 单位矩阵
static Matrix e() {
Matrix ret;
for (int i = 0; i < N; i++) {
ret[i][i] = 1;
}
return ret;
}
// 加法
friend Matrix operator+(Matrix lt, const Matrix& rt) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
lt[i][j] += rt[i][j];
}
}
return lt;
}
// 乘法
friend Matrix operator*(const Matrix& lt, const Matrix& rt) {
Matrix ret;
for (int i = 0; i < N; i++) {
for (int k = 0; k < N; k++) {
if (lt[i][k] == S()) {
continue;
}
for (int j = 0; j < N; j++) {
ret[i][j] += lt[i][k] * rt[k][j];
}
}
}
return ret;
}
Matrix pow(ll b) const {
Matrix ret = e();
Matrix t = *this;
while (b > 0) {
if (b & 1) {
ret = ret * t;
}
t = t * t;
b >>= 1;
}
return ret;
}
ColVector<S, N> operator*(const ColVector<S, N>& vec) {
ColVector<S, N> ret;
for (int i = 0; i < N; i++) {
for (int k = 0; k < N; k++) {
if (a[i][k] == S()) {
continue;
}
ret[i] = ret[i] + (a[i][k] * vec[k]);
}
}
return ret;
}
};

矩阵快速幂还有其他trick, 这里简单介绍一些.

邻接矩阵的快速幂#

以下邻接矩阵的定义: adj[v][u]表示从u到v的边, 请注意这和常见的邻接矩阵是反过来的(方便左乘).

首先考虑这样一个问题: 对于给定的简单图, 顶点数n <= 100, 计算由k <= 1e9步组成的路径数.

我们可以很容易想到这样一个递推方法, 设dp[i][j]表示以节点i为结尾的, 由j步组成的路径数, 设c[j][i]为i到j是否有边的指示器(有边为1, 无边为0), 则:

  1. dp[1][j] = c[1][1] * dp[1][j - 1] + c[1][2] * dp[2][j - 1] + ... + c[1][n] * dp[n][j - 1].
  2. dp[2][j] = c[2][1] * dp[1][j - 1] + ...
  3. dp[n][j] = c[n][1] * dp[1][n - 1] + ...

我们可以发现这可以写成矩阵形式. 状态向量即为DP = dp[1][j], dp[2][j], ..., dp[n][j], 转移矩阵为c[i][j]组成的矩阵, 我们会发现这个矩阵正是图的邻接矩阵. 所以使用邻接矩阵建图, 再对邻接矩阵做快速幂即可.

(min, +)矩阵乘法(新定义)#

回想floyd算法的过程. dp[i][j] = min(dp[i][t] + dp[t][j]), 我们会发现dp[i][t]的第二维和dp[t][j]的第一维是相同的, 而且每次t需要遍历所有点, 这恰好和矩阵乘法有相似之处(矩阵乘法: dp[i][j] = dp[i][1] * dp[1][j] + dp[i][2] * dp[2][j] + ...), 也就是说原来的乘法相当于新的加法, 原来的加法变成了新的min操作, 可以证明这样的矩阵乘法是满足结合律的, 所以可以使用快速幂优化. 这样的矩阵乘法也可以叫最短路径矩阵乘法, 因为常用来求拥有k条边的最短路径问题.

例题: CF102644F

代码:

template <int N>
struct Matrix_mp {
array<array<ll, N>, N> a;
Matrix_mp() {
for (int i = 0; i < N; i++) {
fill(a[i].begin(), a[i].end(), INFLL);
}
}
ll* operator[](int i) {
return a[i].data();
}
const ll* operator[](int i) const {
return a[i].data();
}
static Matrix_mp e() {
Matrix_mp ret;
for (int i = 0; i < N; i++) {
ret[i][i] = 0;
}
return ret;
}
friend Matrix_mp operator+(Matrix_mp lt, const Matrix_mp& rt) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
lt[i][j] = min(lt[i][j], rt[i][j]);
}
}
return lt;
}
friend Matrix_mp operator*(const Matrix_mp& lt, const Matrix_mp& rt) {
Matrix_mp ret;
for (int i = 0; i < N; i++) {
for (int k = 0; k < N; k++) {
if (lt[i][k] >= INFLL) {
continue;
}
for (int j = 0; j < N; j++) {
ll now;
if (rt[k][j] >= INFLL) {
now = INFLL;
} else {
now = lt[i][k] + rt[k][j];
}
ret[i][j] = min(ret[i][j], now);
}
}
}
return ret;
}
Matrix_mp pow(ll b) const {
Matrix_mp ret = e();
Matrix_mp t = *this;
while (b > 0) {
if (b & 1) {
ret = ret * t;
}
t = t * t;
b >>= 1;
}
return ret;
}
ColVector<ll, N> operator*(const ColVector<ll, N>& vec) {
ColVector<ll, N> ret(INFLL);
for (int i = 0; i < N; i++) {
for (int k = 0; k < N; k++) {
if (a[i][k] >= INFLL) {
continue;
}
ll now;
if (a[i][k] >= INFLL || vec[k] >= INFLL) {
now = INFLL;
} else {
now = a[i][k] + vec[k];
}
ret[i] = min(ret[i], now);
}
}
return ret;
}
};
竞赛算法(8) 线性代数
https://fuwari.vercel.app/posts/竞赛算法8-线性代数/
作者
ykindred
发布于
2025-12-01
许可协议
CC BY-NC-SA 4.0