OI中的线性代数

向量和矩阵


Part 1:向量 (Vector) —— 是“坐标”,也是“状态”

想象一下你在玩一个2D游戏,你的角色在地图上。

  • “你在哪?” → 你需要一个坐标来表示,比如 (10, 5)
  • “你要往哪走一步?” → 你需要一个方向和距离,比如“向右3步,向上4步”。

这两种信息,我们都可以用一个东西来表示,它就是向量

在编程里,向量本质上就是一个数组

  • vector_pos = [10, 5] 可以表示你当前的位置。
  • vector_move = [3, 4] 可以表示你下一步的移动。

小结一下,对我们Oier来说,向量就是:

  1. 一个点/坐标 (Position): 描述一个东西在空间里的位置。可以是二维 [x, y]、三维 [x, y, z],甚至更高维(比如一个英雄有多个属性:[血量, 攻击, 防御, 敏捷],这也是一个四维向量!)。
  2. 一个位移/状态变化 (Displacement / State Change): 描述一个“操作”,比如“x增加3,y增加4”。

所以,别把向量想得太复杂,它就是一排有序的数字,用来打包表示一个位置或者一个状态


Part 2:矩阵 (Matrix) —— 超级的“变换器”

如果说向量是一个“状态”,那矩阵就是一个“状态转移的规则”。它是一个“变换器”或者叫“函数”。你给它一个旧的状态(一个向量),它能给你算出新的状态(另一个向量)。

在编程里,矩阵就是一个二维数组

比如我们有一个2x2的矩阵:

M = [[a, b],
     [c, d]]

这玩意儿有什么用?它最核心的作用就是“变换”向量。

想象一个机器,你把一个向量 v_old = [x, y] 从左边扔进去,这个机器(矩阵M)对它进行一通操作,然后从右边吐出一个新的向量 v_new = [x', y']

这个过程,就叫矩阵乘以向量

举几个直观的例子:

  • 旋转变换器: 有一个特定的矩阵,你把角色的坐标向量 [x, y] 扔进去,出来的就是旋转了90度之后的新坐标。
  • 缩放变换器: 另一个特定的矩阵,可以把所有坐标都放大2倍。
  • 镜像变换器: 还有一个矩阵,可以把角色关于y轴做个对称。

小结一下,矩阵是什么:

  1. 一个二维的数字表格。
  2. 它最牛的地方在于,它不只是一个数据表格,它是一个操作,一个变换器
  3. 它的工作方式是:矩阵 * 向量 = 新向量

Part 3:矩阵乘法 —— “变换”的叠加

好了,最关键的地方来了。

如果一个矩阵 M1 代表“旋转90度”,另一个矩阵 M2 代表“放大2倍”。

我现在想对一个角色先旋转90度,再放大2倍,该怎么做?

  • 笨办法: 先用 M1 乘以角色的向量 v,得到一个中间向量 v_temp。再用 M2 乘以 v_temp 得到最终结果 v_final
    v_final = M2 * (M1 * v)

  • 聪明办法: 我能不能造一个“超级变换器” M_super,它本身就等于“先旋转90度再放大2倍”这个复合操作?

可以!这个超级变换器就是 M1M2 的乘积:
M_super = M2 * M1 (注意顺序!先做的变换在右边)

这就是矩阵乘法的精髓:它是变换的叠加/复合。两个矩阵相乘,会得到一个新的矩阵,这个新矩阵代表了前两个矩阵依次施加的变换效果的总和。


Part 4:为什么OI需要它?—— 矩阵快速幂

现在进入正题,这东西在OI里到底有什么用?

想象一个递推问题,比如最经典的斐波那契数列:
f(n) = f(n-1) + f(n-2)

我们每次的状态是什么?是 [f(n), f(n-1)] 这一个向量。
我们想从 [f(n-1), f(n-2)] 这个旧状态,得到 [f(n), f(n-1)] 这个新状态。

能不能找到一个“变换器”(矩阵),让这件事发生?
[新状态] = 矩阵 * [旧状态]

也就是:
[ f(n) ] = [ ? ? ] * [ f(n-1) ]
[f(n-1)] [ ? ? ] [ f(n-2) ]

根据递推公式 f(n) = 1*f(n-1) + 1*f(n-2)f(n-1) = 1*f(n-1) + 0*f(n-2),我们很容易就能构造出这个神奇的矩阵:

[ f(n) ] = [ 1 1 ] * [ f(n-1) ]
[f(n-1)] [ 1 0 ] [ f(n-2) ]

我们把这个 [[1, 1], [1, 0]] 矩阵叫做 T (Transition Matrix, 转移矩阵)。

你看,每一次递推,就等于乘以一次矩阵 T

那么,要求 f(n),就相当于从初始状态 [f(1), f(0)] = [1, 0] 开始,连续乘以 n-1 次这个矩阵 T
[f(n), f(n-1)] = T * T * ... * T (n-1次) * [f(1), f(0)]

也就是:
[f(n), f(n-1)] = T^(n-1) * [f(1), f(0)]

现在问题来了:如果 n 非常大,比如 10^18,你循环 n-1 次肯定超时。
但是,你对快速幂一定很熟吧?求 a^n 可以在 O(log n) 时间内搞定。

矩阵的乘法也满足结合律 (A*B)*C = A*(B*C),所以矩阵也可以用快速幂!

我们可以用 O(k^3 * log n) 的时间计算出 T^(n-1)(其中k是矩阵的边长,这里是2)。然后再用这个最终的“超级无敌变换矩阵”乘以初始状态向量,一步到位,直接算出 f(n)

总结一下给Oier的最终版:

  1. 向量:一个一维数组,用来表示一个状态(比如 [f(n), f(n-1)])。
  2. 矩阵:一个二维数组,用来表示一次状态转移(从 n-1n 的变换规则)。
  3. 矩阵乘法:将多次连续的状态转移合并成一次等效的转移。
  4. 矩阵快速幂:当你要执行海量(比如 10^18 次)相同的状态转移时,用快速幂算法把这些转移合并成最终的“终极转移”,然后一次性作用在初始状态上,实现 O(log n) 级别的加速。

所有可以用矩阵快速幂优化的DP或递推问题,本质都是在做这件事。希望这个解释对你有帮助!去刷题试试吧,你会发现新世界的!

矩阵的初等变换

好的,各位Oier同学,我们继续用大白话聊聊矩阵。

上次我们把矩阵看作一个“变换器”,它通过乘法来改变一个状态(向量)。今天我们来聊点别的,不把它当“变换器”,而是把它当成一个“谜题板”。这个谜题板上写满了各种方程,我们的任务是解开它。


Part 1: 矩阵的核心谜题 —— 解方程组

大家在初中就学过解二元一次方程组,比如:

2x + 3y = 8
x  -  y = 1

我们用加减消元法或者代入消元法就能解出 x 和 y。

如果方程有10个,未知数也有10个呢?手算就太痛苦了。在计算机里,我们用矩阵来表示和解决这个问题。

上面的方程组可以写成矩阵的形式: A * x = b

  • A (系数矩阵): 就是所有未知数前面的系数组成的“表格”。
    A = [[2, 3],
         [1,-1]]
    
  • x (未知数向量): 就是我们要解的未知数。
    x = [[x],
         [y]]
    
  • b (常数向量): 就是等号右边的结果。
    b = [[8],
         [1]]
    

所以,解线性方程组,在计算机眼中,就是已知矩阵 A 和向量 b,求向量 x 的过程。

怎么求呢?这就需要我们对这个“谜题板”(矩阵A)进行一些操作,让它变得简单,直到答案一目了然。这些操作,就是初等行变换


Part 2: 解谜的三大神器 —— 初等行变换

想象一下,你在解方程组时会做什么?无非三件事:

  1. 把两个方程交换一下位置。 (这显然不影响最终答案)
  2. 给某个方程的两边同时乘以一个非零的数。 (比如 x - y = 1 变成 2x - 2y = 2
  3. 把一个方程的k倍加到另一个方程上。 (这是加减消元法的精髓)

这三个操作,对应到矩阵里,就是大名鼎鼎的初等行变换 (Elementary Row Operations),它们是我们解谜的瑞士军刀,只有这三招,但足够解万题。

  1. 交换两行 (Swap): swap(row_i, row_j)
  2. 给某一行乘以一个非零常数 (Scale): multiply(row_i, k)
  3. 把第 j 行的 k 倍加到第 i 行上 (Add): add(row_i, row_j, k)

核心思想: 对矩阵A进行这些操作,只要我们同时对向量b也进行同样的操作,方程组的解是不会变的!


Part 3: 算法登场 —— 高斯消元法 (Gaussian Elimination)

高斯消元法,就是一套标准化的、利用上述三大神器来解方程组的算法流程。它的目标非常明确:把系数矩阵 A 变成一个 “上三角矩阵”(或者叫“阶梯形矩阵”)。

啥是上三角矩阵?就是长这样的,左下角全都是0:

[[a, b, c],
 [0, d, e],
 [0, 0, f]]

为什么变成这样就好解了?你看最后一行,它对应的方程是 f * z = ...z 一下就解出来了。然后把 z 的值代入倒数第二行的方程 d * y + e * z = ...,又能解出 y。最后把 yz 都代入第一行,解出 x。这个过程叫回代 (Back Substitution)

高斯消元的算法步骤 (就像一份菜谱):

为了方便,我们通常把矩阵 A 和向量 b 拼在一起,形成一个增广矩阵 [A | b],然后对整个增广矩阵进行操作。

目标:A 的部分变成上三角。

流程: (假设我们有一个 n x n 的矩阵)

  1. 枚举主元列: 从第 c = 0 列开始,到第 n-1 列。
  2. 找到当前列的“主元”(Pivot): 在当前第 c 列,从第 r 行(r 初始为0)往下,找到一个绝对值最大的元素,把它所在的那一行和第 r 行交换。
    • 为啥要找最大的? 为了减少计算中的浮点数误差,让结果更精确。这个技巧叫“列主元高斯消去法”。
    • 如果这一列从第 r 行往下全都是0,说明这一列搞不定了,直接跳到下一列(c++),行 r 不变。
  3. 归一化 (可选,但推荐): 将新的第 r 行整体除以 A[r][c] 的值。这样主元就变成了1,方便后续计算。
  4. 消元: 对于第 r 行以外的所有其他行 i(从0到n-1,i != r),我们要把它们第 c 列的元素变成0。怎么做?很简单:
    • add(row_i, row_r, -A[i][c])
    • 也就是,把第 r 行(主元行)乘以 -A[i][c],然后加到第 i 行上。这样第 i 行的第 c 列就变成 A[i][c] - A[i][c] = 0 了。
  5. 前进: 当前行和当前列都处理完了,让 r++,然后回到步骤1处理下一列。

等所有列都处理完,你的增广矩阵 [A | b] 就变成了 [上三角 | 新的b]。然后从最后一行开始回代,就能求出所有未知数。

高斯-若尔当消元法 (Gauss-Jordan Elimination):
这是高斯消元的“豪华版”。它在第4步消元时,不仅仅是把主元下面的元素变成0,而是把主元所在列的所有其他元素(包括上面的)都变成0。这样搞完之后,矩阵A的部分会直接变成一个单位矩阵(对角线全是1,其他全是0)。

[[1, 0, 0],
 [0, 1, 0],
 [0, 0, 1]]

这样做的好处是,你连回代都省了!最后增广矩阵右边的b向量,直接就是你的答案x向量!代码写起来更无脑。


Part 4: 在OI中的应用

高斯消元法这套东西,在OI里非常有用:

  1. 解线性方程组: 这是它的本职工作。有些看起来是图论、DP或者网络流的问题,最后可以抽象成一个n元一次方程组,然后用高斯消元一波带走。
  2. 求逆矩阵: 如果你想求矩阵 A 的逆矩阵 A⁻¹。你可以构造一个增广矩阵 [A | I],其中 I 是单位矩阵。然后对这个大家伙进行高斯-若尔当消元,目标是把左边的 A 变成 I。当左边成功变成 I 时,右边剩下的部分,就神奇地变成了 A⁻¹!即 [I | A⁻¹]
  3. 求行列式: 初等行变换对行列式的值有明确的影响:
    • 交换两行,行列式值取反。
    • 某一行乘以k,行列式值也乘以k。
    • 把一行加到另一行上,行列式值不变。
      利用这个性质,我们可以用高斯消元把矩阵变成上三角矩阵。上三角矩阵的行列式就是对角线上所有元素的乘积。所以,在消元过程中记录下交换次数和乘法因子,就能轻松求出原矩阵的行列式。
  4. 线性基 (Advanced): 在异或空间中,求一组数的线性基的过程,本质上就是在一个只有0和1的矩阵上做高斯消元。每一位可以看作一个维度,每个数可以看作一个向量,目标也是把矩阵变成“阶梯型”。

总结

  • 初等行变换是解方程组时那三种最基本、最合法的操作。
  • 高斯消元是把这些操作流程化、算法化,专门用来把矩阵变成简单的上三角/阶梯型,从而方便求解。
  • 代码实现时,通常用一个二维数组模拟增广矩阵,然后按照高斯(-若尔当)消元的“菜谱”一步步循环操作即可。

希望这个解释能帮你把抽象的数学概念,和你熟悉的算法思维联系起来!

高斯消元法

好的,各位Oier,咱们今天来把“高斯消元法”这个听起来很高端的东西,拆解成一个你闭着眼睛都能写出来的算法。

忘了线性代数,忘了什么向量空间。咱们就把它当成一个解谜游戏

游戏背景:解方程

你一定解过这种初中级别的方程组:

2x + 3y = 8   (方程①)
 x -  y = 1   (方程②)

你是怎么解的?通常是“加减消元法”:

  1. 把方程②乘以2,得到 2x - 2y = 2 (方程③)。
  2. 用方程①减去方程③,(2x - 2x) + (3y - (-2y)) = 8 - 2,得到 5y = 6
  3. 瞬间解出 y = 1.2
  4. 再把 y 代回任意一个方程,解出 x

发现了吗?核心操作就是:通过把一个方程乘以某个数,然后加到另一个方程上,来“干掉”一个未知数。

高斯消元,就是把这个过程标准化、流程化,让计算机也能轻松执行。


游戏目标:把谜题变简单

想象一下,我们有n个方程,n个未知数。在程序里,我们用一个二维数组(矩阵) A 来存所有系数,用一个一维数组 b 来存等号右边的结果。

比如上面的例子,就存成:
A = [[2, 3], [1, -1]]
b = [8, 1]

高斯消元的目标非常纯粹:通过一系列合法的变换,把系数矩阵 A 变成一个 “上三角矩阵”

啥是上三角矩阵?就是长这样的,主对角线(左上到右下)的左下方全都是0

[[a, b, c],
 [0, d, e],
 [0, 0, f]]

为什么要变成这样?因为一旦变成这样,谜题就解开了!

  • 最后一行对应的方程是:0*x + 0*y + f*z = ...z 一下就算出来了。
  • 知道了 z,看倒数第二行:0*x + d*y + e*z = ...y 也算出来了。
  • 一路往上代,所有未知数都迎刃而解。这个过程叫回代

游戏规则:三大神器(初等行变换)

你在解谜过程中,只有三种合法的操作:

  1. 交换两行: 相当于交换两个方程的位置,答案不变。
  2. 给某一行整体乘以一个非零数: 相当于给一个方程两边同乘以一个数,答案不变。
  3. 把一行的k倍加到另一行上: 这就是我们“消元”的核心武器。

游戏攻略:高斯消元算法“菜谱”

好了,上菜谱!这是一个可以被完美翻译成代码的流程。为了方便,我们把 Ab 拼在一起,叫增广矩阵 [A | b],所有操作对一整行生效。

目标:[A | b]A 部分变成上三角。

流程 (假设有 n 行 n+1 列的增广矩阵):

第一步:枚举“主心骨”
我们一列一列地处理。从第 c = 0 列开始。我们的目标是,利用第 r 行(r 初始为0),把下面所有行的第 c 列都变成0。

for (int c = 0, r = 0; c < n; ++c) {
    // c 是当前处理的列
    // r 是当前处理的行 (也叫主元行)

第二步:找到这一列的“老大” (选主元)
在第 c 列,从第 r 行往下,找到绝对值最大的那个元素。比如它在第 max_r 行。然后,交换第 r 行和第 max_r

  • 为什么要找最大的? 防止除以一个很小的数,导致精度爆炸。这是OI中写高斯消元必须有的好习惯!
  • 如果这一列从第 r 行往下全都是0怎么办? 说明这个未知数无法被确定(或者有无穷解/无解),我们搞不定它,就跳过这一列 (continue),让 c 增加,但 r 不变,继续处理下一列。
    int max_r = r;
    for (int i = r + 1; i < n; ++i) {
        if (abs(matrix[i][c]) > abs(matrix[max_r][c])) {
            max_r = i;
        }
    }
    swap(matrix[r], matrix[max_r]); // 交换两行

第三步:清理门户 (消元)
现在,第 r 行的第 c 个元素 matrix[r][c] 就是我们的“老大”(主元)了。我们要用它来“干掉”它下面所有的“小弟”——也就是把第 r+1 行到第 n-1 行的第 c 列元素全部变成0。

怎么做?对于 r 下面的每一行 i

  1. 算出比例 factor = matrix[i][c] / matrix[r][c]
  2. 让第 i 行的所有元素,都减去第 r 行对应元素乘以这个比例 factor
    matrix[i][j] -= matrix[r][j] * factor (对 jcn 循环)

这样操作完,matrix[i][c] 就变成了 matrix[i][c] - matrix[r][c] * (matrix[i][c] / matrix[r][c]),正好等于0!

    // 消元
    for (int i = r + 1; i < n; ++i) {
        double factor = matrix[i][c] / matrix[r][c];
        for (int j = c; j <= n; ++j) { // 注意要到最后一列(b)
            matrix[i][j] -= matrix[r][j] * factor;
        }
    }
    r++; // 当前行处理完毕,主元行下移
}

第四步:收割胜利果实 (回代)
经过上面的一通操作,矩阵 A 的部分已经变成了漂亮的上三角。现在从最后一行开始,把答案一个个算出来。

  • 最后一行 n-1A[n-1][n-1] * x[n-1] = b[n-1],所以 x[n-1] = b[n-1] / A[n-1][n-1]
  • 倒数第二行 n-2A[n-2][n-2] * x[n-2] + A[n-2][n-1] * x[n-1] = b[n-2]。因为 x[n-1] 已经知道了,代入就能求出 x[n-2]
  • ...依此类推,从下往上,直到解出所有未知数。

总结一下给你的代码模板思路:

  1. 外层循环 c0n-1,表示当前要消掉哪一列。r 表示当前用哪一行做主元。
  2. 找主元:在 c 列的 r 行及以下,找到绝对值最大的,换到 r 行。
  3. 判断无解/多解:如果找到的主元是0,说明有问题。
  4. 消元:用一个内层循环 ir+1n-1,遍历 r 行下面的所有行。
  5. 在内层循环里,再用一个最内层循环 jcn,更新这一行 i 的所有元素。
  6. 所有循环结束后,矩阵变成上三角。
  7. 回代:再写一个从 n-10 的循环,依次求解。

高斯消元法就是这么一个朴实无华但极其强大的算法,它把人类的直觉(加减消元)变成了一套计算机可以执行的、固定的机械化流程。搞懂它,你就能解决一大票和解方程相关的OI题了!

posted @ 2025-07-22 15:09  surprise_ying  阅读(48)  评论(0)    收藏  举报