寒假讲课:线性代数

Written by: Joyce-Peng-GitHub

寒假讲课:线性代数

蓬蒿人

推荐3Blue1Brown《线性代数的本质》漫士沉思录《无痛线代》系列

在高中数学的基础上,有时间的话推荐阅读《线性代数导论(Introduction to Linear Algebra)》《线性代数应该这样学(Linear Algebra Done Right)》。前者是科普(?)和入门性质,偏工科应用;后者更加抽象(本义),适合第二遍学的时候看。后者的作者在网上免费发布电子版,中文版也是完全免费的。

向量和矩阵

向量

先让我们约定几个符号:

几何意义

凡学过高中物理的同学应当都理解“矢量”的概念,例如力、速度、加速度、动量都是矢量。矢量(vector)就是向量(vector)。向量在几何上通常视作一个“箭头”。

通常我们说的“向量”都是自由向量,即认为起点不同但大小、方向相同的向量是相等的,与“有向线段”相区分。

定义

既有大小(magnitude)/长度(length)又有方向(direction)的量称为向量。向量的长度也称为模(module),用符号\Vert\cdot\Vert表示。特别地,称长度为00的向量为零向量(zero vector),记作0\vec{0}。称长度为11的向量为单位向量(unit vector)

称两个向量是平行的当且仅当它们方向相同或相反,称两个向量是垂直的当且仅当它们的方向垂直,即夹角为12π\dfrac{1}{2}\pi。特别地,规定0\vec{0}与任何向量平行。

基本运算:

特别地,定义两个空间向量u,v\vec{u},\vec{v}的*外/向量/叉积*u×v\vec{u}\times\vec{v}为这样一个向量,它的模长为uvsinu,v\Vert\vec{u}\Vert\Vert\vec{v}\Vert\sin\langle\vec{u},\vec{v}\rangle,方向按照右手定则确定,即把右手做成虚握的“点赞”姿势👍,手掌沿u\vec{u}方向、四根手指向v\vec{v}方向弯曲,则竖起的大拇指即沿u×v\vec{u}\times\vec{v}方向,且与u,v\vec{u},\vec{v}均垂直。

注意叉积的别名“外积”在线性代数语境下具有歧义(列向量与其转置进行矩阵乘法的结果也称为外积),注意避免使用。

性质

运算律:

**平面向量基本定理:**若O,A,BO,A,B不共线,则C平面ABO    λ,μR (OC=λOA+μOB)C\in\text{平面}ABO\iff\exists\lambda,\mu\in\mathbb{R}\ (\overrightarrow{OC}=\lambda\overrightarrow{OA}+\mu\overrightarrow{OB})

**空间向量基本定理:**若O,A,B,CO,A,B,C不共面,则三维空间内任意一点DD都满足λ,μ,νR (OD=λOA+μOB+νOC)\exists\lambda,\mu,\nu\in\mathbb{R}\ (\overrightarrow{OD}=\lambda\overrightarrow{OA}+\mu\overrightarrow{OB}+\nu\overrightarrow{OC})

**共线向量基本定理:**设v0\vec{v}\neq\vec{0},则uv    λR (u=λv)\vec{u}\parallel\vec{v}\iff\exists\lambda\in\mathbb{R}\ (\vec{u}=\lambda\vec{v})

**推论:**设O,A,B,CO,A,B,C是空间内三点,则C直线AB    λ,μR (λ+μ=1OC=λOA+μOB)C\in\text{直线}AB\iff\exists\lambda,\mu\in\mathbb{R}\ (\lambda+\mu=1\land\overrightarrow{OC}=\lambda\overrightarrow{OA}+\mu\overrightarrow{OB})

推论:设O,A,B,C,DO,A,B,C,D是空间内三点,O,A,B,CO,A,B,C不共面,则D平面ABC    λ,μ,νR (λ+μ+ν=1OD=λOA+μOB+νOC)D\in\text{平面}ABC\iff\exists\lambda,\mu,\nu\in\mathbb{R}\ (\lambda+\mu+\nu=1\land\overrightarrow{OD}=\lambda\overrightarrow{OA}+\mu\overrightarrow{OB}+\nu\overrightarrow{OC})

**向量垂直的判定:**若u,v0\vec{u},\vec{v}\neq\vec{0},则uv    uv=0\vec{u}\perp\vec{v}\iff\vec{u}\cdot\vec{v}=0

将若干个向量数乘并相加的结果称为它们的线性组合(linear combination)。对于一组向量,如果其中任何一个向量都不能由其他的向量线性组合得到,则称这组向量互相线性无关(linearly independent),否则称为线性相关(linearly dependent)。线性无关的一组向量称为一组基(底)(basis)。利用基底,可以将向量运算分解为各分量上的标量运算。

代数意义

垂直的基底是最讨人喜欢的,因为交叉项的内积为00,避免了烦人的夹角余弦。而如果基底都是单位向量就更爽了。

以平面向量为例:设i,j\vec{i},\vec{j}是一组正交单位基,v1=x1i+y1j, v2=x2i+y2j\vec{v}_1=x_1\vec{i}+y_1\vec{j},\ \vec{v}_2=x_2\vec{i}+y_2\vec{j},则

v1v2=(x1i+y1j)(x2i+y2j)=x1x2i2+(x1y2+x2y1)(ij)+y1y2j2=x1x2+y1y2.\begin{aligned} \vec{v}_1\cdot\vec{v}_2&=(x_1\vec{i}+y_1\vec{j})\cdot(x_2\vec{i}+y_2\vec{j})\\ &=x_1x_2\vec{i}^2+(x_1y_2+x_2y_1)(\vec{i}\cdot\vec{j})+y_1y_2\vec{j}^2\\ &=x_1x_2+y_1y_2. \end{aligned}

在笛卡尔(Descartes)坐标系中,总是把向量的起点固定在原点,则两个向量不同当且仅当其终点坐标不同,于是我们可以用终点坐标来唯一描述向量,这就是向量的坐标表示。分别取+x,+y,+z+x,+y,+z方向的单位向量为i,j,k\vec{i},\vec{j},\vec{k},则终点在(x,y,z)(x,y,z)处的向量的基底表示刚好就是xi+yj+zkx\vec{i}+y\vec{j}+z\vec{k}

于是向量加法、数乘和内积都有了代数意义,即如果v1=(x1,y1,z1), v2=(x2,y2,z2), λR\vec{v}_1=(x_1,y_1,z_1),\ \vec{v}_2=(x_2,y_2,z_2),\ \lambda\in\mathbb{R},则

v1+v2=(x1+x2,y1+y2,z1+z2),λv1=(λx1,λy1,λz1),v1v2=x1x2+y1y2+z1z2,v1×v2=(y1z2y2z1,z1x2z2x1,x1y2x2y1).\vec{v}_1+\vec{v}_2=(x_1+x_2,y_1+y_2,z_1+z_2),\\ \lambda\vec{v_1}=(\lambda x_1,\lambda y_1,\lambda z_1),\\ \vec{v}_1\cdot\vec{v}_2=x_1x_2+y_1y_2+z_1z_2,\\ \vec{v}_1\times\vec{v}_2=(y_1z_2-y_2z_1,z_1x_2-z_2x_1,x_1y_2-x_2y_1).

在线性代数中,我们通常将坐标写成一列,例如

(1,1,4)=[114],(1,1,4)=\begin{bmatrix} 1\\ 1\\ 4 \end{bmatrix},

称为列向量。如果写成一行[114]\begin{bmatrix}1 & 1 & 4\end{bmatrix},则称为行向量。在不加额外说明的情况下,“向量”默认指列向量,但常写作(1,1,4)(1,1,4),因为竖着写不方便,无论是手写还是LaTeX\LaTeX

在笛卡尔坐标系下:直线对应R1\mathbb{R}^1,在其中描述一个向量,只需用一个实数,此时向量与标量不需要区分;平面对应R2\mathbb{R}^2,在其中描述一个向量需要两个实数;立体对应R3\mathbb{R}^3,在其中描述向量需要三个实数。超过三维的空间是人类难以想象的,但这启发我们用nNn\in\mathbb{N}^*个实数来描述nn维空间Rn\mathbb{R}^n中的向量,即我们可以用一种纯代数的方式定义nn维空间的向量,而无需理解高维空间的样貌。实际上,这种定义方式的向量是更让人感觉“严谨”的,因为它不要求我们对立体几何有任何理解(无需欧氏几何的五条公理),可以严格建立在仅以ZFC公理系统为基石的集合论上。


不严谨地说,Rn (nN)\mathbb{R}^n\ (n\in\mathbb{N}^*)中的元素,即nn元有序实数元组,称为nn维向量。

于是可以重新定义各运算:设u=(ui)i=1n, v=(vi)i=1n, λR\vec{u}=(u_i)_{i=1}^n,\ \vec{v}=(v_i)_{i=1}^n,\ \lambda\in\mathbb{R}

由于复数允许实数的所有运算,所以我们完全可以将上面描述向量的方式推广到Cn\mathbb{C}^n

至少对于工科学生而言,对向量的理解到这一步完全够用了。

最广义的向量

经过进一步抽象后,向量可以是满足一定条件的任何东西,不一定是箭头,也不一定是一列数。

数学家发现,无论是几何箭头、数组,还是多项式、连续函数、甚至是矩阵,它们都共享同一套运算结构(加法和数乘)。为了统一研究这些对象,数学家提出了向量空间的概念。

一个集合F\mathbb{F}如果包含至少两个不同的元素(分别记作0,10,1),且对二元运算+,×+,\times(分别称作加法、乘法)满足

则称F\mathbb{F}是一个域(field)

Q,R,C\mathbb{Q},\mathbb{R},\mathbb{C}都对普通加法和普通乘法构成域。Zm\mathbb{Z}_m对模mm加法和模mm乘法构成域;特别地,{0,1}\{0,1\}对异或\oplus和与\land构成域。

向量空间
向量空间的定义

显然对任何域F\mathbb{F}Fn (nN)\mathbb{F}^n\ (n\in\mathbb{N}^*)一定是向量空间,只需将加法定义为逐元素的F\mathbb{F}上的加法。更一般地,对任何域F\mathbb{F}和集合SSFS\mathbb{F}^S构成向量空间。Fn\mathbb{F}^n可以视作FN[1,n]\mathbb{F}^{\mathbb{N}\cap[1,n]}

向量空间的元素称为向量(vector)点(point)

可以证明一个向量空间VV必然具有唯一的加法恒等元00vV (0v=0)\forall v\in V\ (0v=0)(第一个00是数,第二个00是向量)。每个元素具有唯一的加法逆元,记作v-v。易证v=(1)v-v=(-1)v

如果向量空间VV的子集UU是与VV具有相同的加法恒等元、加法和标量乘法运算的向量空间,则称UUVV子空间(subspace)。另一个等价的定义是,若向量空间VV的子集UU满足0U0\in U且对加法和标量乘法封闭,则称UUVV的子空间。

线性组合和张成空间

v1,,vmv_1,\dots,v_mVV中的一组向量,称形如i=1maivi (a1,,amF)\sum_{i=1}^ma_iv_i\ (a_1,\dots,a_m\in\mathbb{F})的向量是v1,,vmv_1,\dots,v_m线性组合(linear combination)

向量空间VV中的一个向量组v1,,vmv_1,\dots,v_m的所有线性组合构成的集合称为这组向量的张成空间(span),记作span(v1,,vm)\operatorname{span}(v_1,\dots,v_m),即

span(v1,,vm){i=1maivi:iN[1,m] (aiF)}.\operatorname{span}(v_1,\dots,v_m)\triangleq\left\{\sum_{i=1}^ma_iv_i:\forall i\in\mathbb{N}\cap[1,m]\ (a_i\in\mathbb{F})\right\}.

可以证明这个张成空间一定是所有包含该向量组中所有向量的VV的子空间中最小的。

如果一个向量空间可以由有限个向量张成,则称为有限维向量空间,否则称为无限维向量空间Fn\mathbb{F}^n是有限维向量空间,而系数都在F\mathbb{F}中的所有多项式构成的集合是无限维向量空间。

对于向量空间VV中的一个向量组v1,,vmv_1,\dots,v_m,若i=1maivi=0 (a1,,amF)\sum_{i=1}^ma_iv_i=0\ (a_1,\dots,a_m\in\mathbb{F})当且仅当iN[1,m] (ai=0)\forall i\in\mathbb{N}\cap[1,m]\ (a_i=0),则称这组向量是*线性无关(linearly independent)的,否则称为线性相关(linearly dependent)*的。

**线性相关性引理:**如果一组向量线性相关,则其中存在某一个向量,使将其移除后,这组向量张成的空间不变。

线性空间VV中一个线性无关且张成VV的向量组称为VV的一个基(basis)。另一个等价的定义是,如果向量空间VV中的一组向量满足每个vVv\in V都可以由这组向量唯一地线性表示,则这组向量是VV的一个基。

向量空间的每个张成组都能被削减成该向量空间的一个基;有限维向量空间都有基,且其每个线性无关向量组都可被扩充成该向量空间的一个基。

一个有限维向量空间VV的基都由相同个数的向量组成,将这个数量称作VV维数(dimension),记作dimV\dim V

UUVV的子空间,则dimUdimV\dim U\leq\dim V,当且仅当U=VU=V时取等。

dimV\dim V个线性无关的向量构成VV的一个基。

内积和范数

如果向量空间VV上的运算:V2F\cdot:V^2\to\mathbb{F}满足

则称\cdotVV上的内积(inner product)。对vVv\in V,定义其*范数(norm)*为v=vv\Vert v\Vert=\sqrt{v\cdot v}

范数的基本性质:

如果两个向量的内积是00,则称这两个向量是*正交(orthogonal)*的。

**毕达哥拉斯定理(Pythagorean theorem):**若uvu\perp v,则u+v2=u2+v2\Vert u+v\Vert^2=\Vert u\Vert^2+\Vert v\Vert^2

从线性方程组到矩阵

一次方程也称为线性方程(linear equation),由线性方程组成的方程组称为线性方程组

线性方程组的行视角和列视角

以方程组

{x2y=13x+2y=11\begin{cases} x-2y=1\\ 3x+2y=11 \end{cases}

为例。

矩阵-向量乘法的行视角和列视角

将上面的方程组写作矩阵乘向量的形式:

[1232][xy]=[111].\begin{bmatrix} 1 & -2\\ 3 & 2 \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix} = \begin{bmatrix} 1\\ 11 \end{bmatrix}.

由此我们可以得到矩阵-向量乘法的两种等价定义:

矩阵乘法

并非任意两个矩阵都能相乘。两个矩阵能相乘,当且仅当第一个矩阵的列数等于第二个矩阵的行数

容易注意到矩阵乘法对两个矩阵的顺序有严格要求。可以证明,矩阵乘法不满足交换律,但满足结合律

计算矩阵乘法的朴素方法需要n3n^3次乘法。

The best at this moment is n2.376n^{2.376}. But the algorithm is so awkward that scientific computing is done the regular way: n2n^2 dot products in ABAB, and nn multiplications for each one.

矩阵的初等变换

单位矩阵

I=[11]I= \begin{bmatrix} 1\\ & \ddots\\ & & 1 \end{bmatrix}

是矩阵乘法的单位元,它与任何矩阵相乘(无论左乘还是右乘)的结果都等于被乘矩阵。

如果我们将单位矩阵的第ii行的11改为λ\lambda得到矩阵MM,则MAMA的结果是AA的第ii行统统变为原来的λ\lambda倍,而其余行不变;AMAM的结果是AA的第ii列统统变为原来的λ\lambda倍,而其余行不变。

如果我们交换单位矩阵的两行,例如

P=[111],P= \begin{bmatrix} 1\\ & & 1\\ & 1 \end{bmatrix},

PAPA的结果是交换AA的第2,32,3行,其余行不变;APAP的结果是交换AA的第2,32,3列,其余列不变。进一步地,如果我们任意排列单位矩阵的行得到PP,则PAPA是按照PP的行来重新排列AA的各行,APAP是按照PP的列重新排列AA的各列。

如果将矩阵AA左乘

E=[1111],E= \begin{bmatrix} 1\\ & 1\\ -1 & & 1 \end{bmatrix},

所得结果是AA的第三行减去第一行,其余行不变;而AEAE则是将AA11列减去第33,其余列不变。

交换两行、将某一行变为原来的非零倍、将某一行减去一行统称为矩阵的初等行变换,类似地可以定义初等列变换,初等行变换和初等列变换统称为矩阵的初等变换。这些变换都可以用矩阵来描述,称为初等矩阵。由于矩阵乘法的可结合性(也可以说初等行变换的可结合性这正是矩阵乘法可结合的原因),EPA=E(PA)=(EP)AEPA=E(PA)=(EP)A,也就是说,若干个初等行变换可以结合到一起,再一次性对被乘矩阵起作用。初等矩阵的乘积仍为初等矩阵。

如果一个矩阵AA可以经过若干次初等行变换(即左乘若干个初等矩阵;当然,由于结合性,等价于左乘一个初等矩阵)得到矩阵BB,则称AABB行等价的。类似地可以定义列等价的概念。

矩阵的其他基本运算

相同形状的矩阵可以相加,定义为对应位置元素相加。矩阵乘法对矩阵加法满足分配律,即

(A+B)C=AC+BC, A(B+C)=AB+AC.(A+B)C=AC+BC,\ A(B+C)=AB+AC.

矩阵同样可以数乘,定义为各位置元素分别乘上乘数。数乘对矩阵加法、矩阵乘法对普通加法都满足分配律。

把整个矩阵沿主对角线对称一下,这个操作称为矩阵转置(transpose)。更严谨地,设AFm×nA\in\mathbb{F}^{m\times n},则它的转置AFn×mA^\top\in\mathbb{F}^{n\times m},且

A(j,i)=A(i,j) (i,jN, im, jn).A^\top(j,i)=A(i,j)\ (i,j\in\mathbb{N},\ i\leq m,\ j\leq n).

ABA^\top B内积,称ABAB^\top外积。内积还是内积,但外积却跟叉乘完全不同。

旋转变换矩阵

矩阵-向量乘法的本质是线性变换。将向量左乘一个矩阵,本质上是按照这个矩阵所描述的规则对这个向量进行旋转和拉伸。

将一个二维向量逆时针旋转角δ\delta的矩阵为

R(δ)=[cosδsinδsinδcosδ].R(\delta)= \begin{bmatrix} \cos\delta & -\sin\delta\\ \sin\delta & \cos\delta \end{bmatrix}.

解线性方程组的高斯消元法

“消元法”实际上有更一般的描述,高斯消元法(Gaussian elimination)、克劳特分解法(Crout decomposition method)只是其中特殊的两种,它们分别对普适的消元法中的参数选取方式做出了规定。从数值分析的角度来说,消元法没有方法误差,但舍入误差不可避免,所以需要根据数据的具体情况选择合适的参数。在算法竞赛中一般不需要考虑这个问题。

设我们要解的线性方程组是

{a1,1x1+a1,2x2++a1,nxn=b1a2,1x1+a2,2x2++a2,nxn=b2am,1x1+an,2x2++am,nxn=bm,\begin{cases} a_{1,1}x_1+a_{1,2}x_2+\dots+a_{1,n}x_n=b_1\\ a_{2,1}x_1+a_{2,2}x_2+\dots+a_{2,n}x_n=b_2\\ \vdots\\ a_{m,1}x_1+a_{n,2}x_2+\dots+a_{m,n}x_n=b_m \end{cases},

写成矩阵的形式是

Ax=b,A\vec{x}=\vec{b},

其中

A=[a1,1a1,2a1,na2,1a2,2a2,nam,1am,2am,n]Fm×n, x=[x1x2xn], b=[b1b2bn].A= \begin{bmatrix} a_{1,1} & a_{1,2} & \dots & a_{1,n}\\ a_{2,1} & a_{2,2} & \dots & a_{2,n}\\ \vdots & \vdots & \ddots & \vdots\\ a_{m,1} & a_{m,2} & \dots & a_{m,n} \end{bmatrix} \in\mathbb{F}^{m\times n},\ \vec{x}= \begin{bmatrix} x_1\\ x_2\\ \vdots\\ x_n \end{bmatrix},\ \vec{b}= \begin{bmatrix} b_1\\ b_2\\ \vdots\\ b_n \end{bmatrix}.

消元

先让我们考虑最理想的情况,m=nm=n,即AA是一个方阵,而且我们假设不会发生消不了的情况。

在这种理想情况下,用第11行消掉第2n2\sim nx1x_1的系数,即从第iN[2,n]i\in\mathbb{N}\cap[2,n]行中减去ai,1a1,1\dfrac{a_{i,1}}{a_{1,1}}倍第11行;用第22行消掉第3m3\sim mx2x_2的系数,即从第iN[3,n]i\in\mathbb{N}\cap[3,n]行中减去ai,2a2,2\dfrac{a_{i,2}}{a_{2,2}}倍第22行;……这样直到第nn行,会得到一个上三角形的方程组:

{u1,1x1+u1,2x2++u1,nxn=c1u2,2x2++u2,nxn=c2un,nxn=cn,\left\{ \begin{array}{rrrrr} u_{1,1}x_1 & +u_{1,2}x_2 & +\dots & +u_{1,n}x_n & =c_1\\ & u_{2,2}x_2 & +\dots & +u_{2,n}x_n & =c_2\\ & & \ddots & \vdots\\ & & & u_{n,n}x_n &=c_n \end{array} \right.,

这一步称为消元(elimination)。用来消元的元素称为主元(pivot)。消元后,留在主对角线上的就是主元。

回代

由最后一行可以解出xn=cnun,nx_n=\dfrac{c_n}{u_{n,n}},然后反代回倒数第二行得xn1=cn1un1,nxnun1,n1x_{n-1}=\dfrac{c_{n-1}-u_{n-1,n}x_n}{u_{n-1,n-1}},……直到把所有未知数全部求出。这一步称为回代(back substitution)

以上是最最理想的情况,但现实情况没有这么好。如果在消元到某一行时,我们发现主元等于00了怎么办?例如

{1x+1y+1z=60y+2z=43y2z=5\left\{ \begin{aligned} 1x+1y+1z & =6\\ {\color{red}0y}+2z & =4\\ 3y-2z & =5 \end{aligned} \right.

如果直接按照上面的步骤走,会导致除数为00,然后爆炸。但实际上,这组方程是有解的(x=1,y=3,z=2x=1,y=3,z=2)。显然我们可以在此时将后面的行中这一列系数不为00的行换过来,反正交换两行不会影响方程组的解。进一步地,如果是以浮点数的形式求解,则选择后面的行中该列最大的系数作为主元是使误差最小的。这就是选主元的高斯消元法。

到目前为止,我们的消元法可以解决所有有唯一解nnnn元线性方程了。甚么时候会无解?当方程之间有矛盾时,方程组无解。但“矛盾”是不能直接知道的;如果我们解到最后一个方程发现,左边是0x0x,右边却不为00,方程组就会无解。那甚么时候不止有一组解呢?当我们发现,消元到某一行时,从当前行往后当前列都是00——我们选不出主元,则这个未知数可以任意取值,此时有无穷多组解。

在消元过程中,b\vec{b}需要和AA一起参与初等行变换。为了方便,通常将它们合并到一起,称为增广矩阵

A~=[Ab]=[a1,1a1,2a1,nb1a2,1a2,2a2,nb2am,1am,2am,nbm].\tilde{A}= \begin{bmatrix} A & \vec{b} \end{bmatrix} = \left[ \begin{array}{cccc:c} a_{1,1} & a_{1,2} & \dots & a_{1,n} & b_1\\ a_{2,1} & a_{2,2} & \dots & a_{2,n} & b_2\\ \vdots & \vdots & \ddots & \vdots & \vdots\\ a_{m,1} & a_{m,2} & \dots & a_{m,n} & b_m \end{array} \right].

消元的过程就是分解

A~=L[Uc]\tilde{A}=L \begin{bmatrix} U & \vec{c} \end{bmatrix}

的过程,其中LL是所有初等行变换的结合。由于LL只与AA有关而与b\vec{b}无关,所以我们甚至可以同时求解多个系数矩阵相同的方程组,只需将方程右边统统放进增广矩阵即可。根据我们的消元步骤,LL必定是一个下三角矩阵。因此消元的过程本质上也是将AA分解为一个下三角矩阵LL和一个上三角矩阵UU之积,称为矩阵的LU分解。

那如果未知数的数量和方程的数量不一样呢?这时系数矩阵不是方阵,那我们注定没办法将方程组化为完全的上三角形,我们只能尽可能化简。

满足下面两个条件的矩阵称为*行阶梯形(row echelon form)*矩阵:

各行的首个非零元素都是11而且都是所在列唯一非零元素的行阶梯形矩阵称作*简化行阶梯形(reduced row echelon form)*矩阵。

通过将增广矩阵化为简化行阶梯形,我们可以求出方程组的通解或证明其无解。消元的过程基本相同,唯在找不到主元时将我们考虑的列右移一列;回代时,由于我们的目的不只是求出各个未知数的值,而且要知道未知数之间的关系,所以需要用下面的行消掉主元所在列的其他元素。

模板题:P2455 [SDOI2006] 线性方程组

注意此题要求区分无解和无穷多组解。

inline void solve() {
	constexpr double EPS = 1e-9;
	size_t n;
	std::cin >> n;
	std::vector<std::vector<double>> mat(n, std::vector<double>(n + 1));
	for (auto &row : mat) {
		for (auto &e : row) {
			std::cin >> e;
		}
	}

	// Elimination
	for (size_t i = 0, col = 0; i < n; ++i, ++col) {
		while (col < n) {
			// Pivoting
			size_t pvt_row = i;
			for (size_t j = i + 1; j < n; ++j) {
				if (std::abs(mat[j][col]) > std::abs(mat[pvt_row][col])) {
					pvt_row = j;
				}
			}
			std::swap(mat[i], mat[pvt_row]);
			if (std::abs(mat[i][col]) >= EPS) {
				break;
			}
			++col;
		}

		if (col == n) {
			int ans = 0;
			for (size_t j = i; j < n; ++j) {
				if (std::abs(mat[j][n]) >= EPS) {
					ans = -1; // no solution
					break;
				}
			}
			std::cout << ans << '\n';
			return;
		}

		for (size_t j = i + 1; j < n; ++j) {
			double fac = mat[j][col] / mat[i][col];
			for (size_t k = col; k < n + 1; ++k) {
				mat[j][k] -= fac * mat[i][k];
			}
		}
	}

	// Back substitution
	std::vector<double> res(n);
	for (size_t i = n; i-- > 0;) {
		res[i] = mat[i][n];
		for (size_t j = i + 1; j < n; ++j) {
			res[i] -= mat[i][j] * res[j];
		}
		res[i] /= mat[i][i];
	}

	std::cout << std::setprecision(10) << std::fixed;
	for (size_t i = 0; i < n; ++i) {
		std::cout << 'x' << (i + 1) << '=' << res[i] << '\n';
	}
}

时间复杂度:Θ(n3)\varTheta(n^3)。 空间复杂度:Θ(n)\varTheta(n)额外空间保存解。

对于大型/稀疏矩阵,可以采用迭代算法求近似解。算法竞赛中不会涉及。

模板题:P2447 [SDOI2010] 外星千足虫

这是一道GF(2)域上的消元。

inline void solve() {
	constexpr size_t N = 1000;
	constexpr char EVEN[] = "Earth", ODD[] = "?y7M#", INF_SLNS[] = "Cannot Determine";

	size_t m, n;
	std::cin >> n >> m;
	std::vector<std::bitset<N + 1>> mat(m);
	for (auto &row : mat) {
		char ch;
		for (size_t i = 0; i <= n; ++i) {
			std::cin >> ch;
			row[i] = (ch == '1');
		}
	}

	size_t r = n;
	for (size_t i = 0; i < n; ++i) {
		size_t pvt_row = m;
		for (size_t j = i; j < m; ++j) {
			if (mat[j][i]) {
				pvt_row = j;
				break;
			}
		}
		if (pvt_row == m) {
			std::cout << INF_SLNS << '\n';
			return;
		}
		if (pvt_row >= r) {
			r = pvt_row + 1;
		}
		std::swap(mat[i], mat[pvt_row]);
		for (size_t j = i + 1; j < m; ++j) {
			if (mat[j][i]) {
				mat[j] ^= mat[i];
			}
		}
	}

	std::bitset<N + 1> res{};
	for (size_t i = n; i--;) {
		res[i] = mat[i][n] ^ ((mat[i] & res).count() & 1);
	}

	std::cout << r << '\n';
	for (size_t i = 0; i < n; ++i) {
		std::cout << (res[i] ? ODD : EVEN) << '\n';
	}
}

时间复杂度:Θ(mn2W)\varTheta\left(\dfrac{m\cdot n^2}{W}\right),其中WW是计算机的字长。

逆矩阵

AA为矩阵,若存在矩阵A1A^{-1}满足A1A=IA^{-1}A=I,则称A1A^{-1}AA左逆矩阵;若AA1=IAA^{-1}=I,则称A1A^{-1}AA右逆矩阵

AA的左右逆矩阵都存在,则它们必相等,称为AA逆矩阵,此时称AA可逆的。一个矩阵是可逆的,当且仅当它是满秩的方阵。

容易证明若A,BA,B都可逆,则(AB)1=B1A1(AB)^{-1}=B^{-1}A^{-1}

利用消元法可以求逆矩阵。首先取增广矩阵A~=[AI]\tilde{A}=\begin{bmatrix}A & I\end{bmatrix},利用消元法将A~\tilde{A}化为简化行阶梯形。AA是可逆的当且仅当rrefA~\operatorname{rref}\tilde{A}的左半部分是II,此时右半部分必为A1A^{-1},因为

A1A~=[A1AA1I]=[IA1].A^{-1}\tilde{A}= \begin{bmatrix} A^{-1}A & A^{-1}I \end{bmatrix} = \begin{bmatrix} I & A^{-1} \end{bmatrix}.

rrefA~\operatorname{rref}\tilde{A}的右半部分即可。

模板题:P4783 【模板】矩阵求逆

inline void solve() {
	constexpr uint32_t MOD = 1e9 + 7;

	size_t n;
	std::cin >> n;
	std::vector<std::vector<uint32_t>> mat(n, std::vector<uint32_t>(n << 1));
	for (size_t i = 0; i < n; ++i) {
		mat[i][n + i] = 1;
		for (size_t j = 0; j < n; ++j) {
			std::cin >> mat[i][j];
		}
	}

	for (size_t i = 0; i < n; ++i) {
		size_t pvt_row = -1;
		for (size_t j = i; j < n; ++j) {
			if (mat[j][i]) {
				pvt_row = j;
				break;
			}
		}
		if (pvt_row == size_t(-1)) {
			std::cout << "No Solution\n";
			return;
		}
		std::swap(mat[i], mat[pvt_row]);

		uint32_t inv = modMulInv(mat[i][i], MOD);
		for (size_t j = i; j < (n << 1); ++j) {
			mat[i][j] = SC<uint64_t>(mat[i][j]) * inv % MOD;
		}

		for (size_t j = i + 1; j < n; ++j) {
			uint64_t fac = mat[j][i];
			mat[j][i] = 0;
			for (size_t k = i + 1; k < (n << 1); ++k) {
				mat[j][k] = (MOD - fac * mat[i][k] % MOD + mat[j][k]) % MOD;
			}
		}
	}

	for (size_t i = n; i--;) {
		for (size_t j = i; j--;) {
			uint64_t fac = mat[j][i];
			mat[j][i] = 0;
			for (size_t k = i + 1; k < (n << 1); ++k) {
				mat[j][k] = (MOD - fac * mat[i][k] % MOD + mat[j][k]) % MOD;
			}
		}
	}

	for (size_t i = 0; i < n; ++i) {
		for (size_t j = n; j < (n << 1); ++j) {
			std::cout << mat[i][j] << ' ';
		}
		std::cout << '\n';
	}
}

时间复杂度:Θ(n3)\varTheta(n^3)。 空间复杂度:Θ(n3)\varTheta(n^3)

行列式

行列式的递归定义:

直接按照定义计算行列式,复杂度为Θ(n!)\varTheta(n!)

容易证明上三角矩阵、下三角矩阵的行列式都等于其主对角线上元素之积,而消元法恰好可以帮我们将矩阵化为上三角形。只不过,消元的过程会稍稍改变其行列式,需要我们人为地维护:

没那么模板的模板题:P7112 【模板】行列式求值

Wrong Answer

这道题不但可能以合数为模数,而且可能出现逆元不存在的情况。如果像我一样直接用exGcd求逆元会导致将不是00的误判为00,然后获得WA掉7个点。

inline void solve() {
	size_t n;
	uint32_t mod;
	std::cin >> n >> mod;
	std::vector<std::vector<uint32_t>> mat(n, std::vector<uint32_t>(n));
	for (auto &row : mat) {
		for (auto &e : row) {
			std::cin >> e;
		}
	}

	uint32_t det = 1;
	for (size_t i = 0; i < n; ++i) {
		// Pivoting
		size_t pvt_row = n;
		for (size_t j = i; j < n; ++j) {
			if (mat[j][i]) {
				pvt_row = j;
				break;
			}
		}
		if (pvt_row == n) {
			std::cout << "0\n";
			return;
		}
		if (pvt_row != i) {
			std::swap(mat[i], mat[pvt_row]);
			det = (mod - det) % mod; // det *= -1
		}

		uint32_t inv = modMulInv(mat[i][i], mod);
		for (size_t j = i + 1; j < n; ++j) {
			uint64_t fac = static_cast<uint64_t>(mat[j][i]) * inv % mod;
			for (size_t k = i; k < n; ++k) {
				mat[j][k] = (mod - fac * mat[i][k] % mod + mat[j][k]) % mod;
			}
		}
	}

	for (size_t i = 0; i < n; ++i) {
		det = SC<uint64_t>(det) * mat[i][i] % mod;
	}

	std::cout << det << '\n';
}

正解

正确的做法是采用fraction-free (Euclidean) Gaussian elimination,其优点在于避免除法(只用了整除而且保证安全)。我们来看看它是怎么实现的。

假设目前我们在第ii行(设为rir_i),想要用第ii行消掉第j (j>i)j\ (j>i)行(设为rjr_j)的第ii列。本来应该是

rjrjaj,iai,iri,r_j\gets r_j-\frac{a_{j,i}}{a_{i,i}}r_i,

但这里可能aj,i1a_{j,i}^{-1}不存在。正确的做法是:

  1. 计算qai,iaj,iq\gets\left\lfloor\dfrac{a_{i,i}}{a_{j,i}}\right\rfloor
  2. rjrjqrir_j\gets r_j-q\cdot r_i
  3. 交换rir_irjr_j
  4. 重复以上步骤直到aj,i=0a_{j,i}=0

这个算法的正确性来源于辗转相除法的正确性。在辗转相除法中,出于gcd(a,b)=gcd(b,amodb)\gcd(a,b)=\gcd(b,a\bmod b),我们每次令(a,b)(b,amodb)(a,b)\gets(b,a\bmod b)直至b=0b=0,而amodb=aabba\bmod b=a-\left\lfloor\dfrac{a}{b}\right\rfloor\cdot b

如果ai,i=0a_{i,i}=0而存在aj,i0a_{j,i}\neq0的话,一定会在遇到jj时将两行直接交换,从而将主元换上来。如果遍历完所有的jj仍然ai,i=0a_{i,i}=0,说明从第ii行往下这一列都是00,就可以宣告行列式为00了。

进行“辗转相除”时注意维护行列式的值。

	uint32_t det = 1;
	for (size_t i = 0; i < n; ++i) {
		for (size_t j = i + 1; j < n; ++j) {
			while (mat[j][i]) {
				uint64_t q = mat[i][i] / mat[j][i];
				for (size_t k = i; k < n; ++k) {
					mat[i][k] = (mod - q * mat[j][k] % mod + mat[i][k]) % mod;
				}
				std::swap(mat[i], mat[j]);
				det = (mod - det) % mod; // det = -det
			}
		}
		if (!mat[i][i]) {
			det = 0;
			break;
		}
		det = SC<uint64_t>(det) * mat[i][i] % mod;
	}

	std::cout << det << '\n';

时间复杂度:O(n3+n2log2(mod))O(n^3+n^2\log^2(mod)),因为在关于某一列执行辗转相除消元时复杂度均摊到O(n)O(n)行上,对于固定的iwhile (mat[j][i])循环的总次数上界为O(n+log2(mod))O(n+\log^2(mod))

注意此题的时间限制是5 s5\ \mathrm{s}

线性基

如果给你了一组向量,如何从中提取出它们的基/极大线性无关组

普适方法

给定v1,,vmFn (m,nN)\vec{v}_1,\dots,\vec{v}_m\in\mathbb{F}^n\ (m,n\in\mathbb{N}^*),求span(v1,,vm)\operatorname{span}(\vec{v}_1,\dots,\vec{v}_m)的一个基。

  1. 构造矩阵

    M=[v1vm].M= \begin{bmatrix} \vec{v}_1^\top\\ \vdots\\ \vec{v}_m^\top \end{bmatrix}.
  2. 执行消元法,将MM化为行阶梯形矩阵(不用到简化行阶梯形矩阵)。

  3. 提取基底:矩阵中剩下的非全零行就是原向量组张成空间的基。

这样得到的基可能不全是原本的向量。如果想知道怎么完全从原来的向量中抽出基(而非它们的线性组合),只需在消元时额外维护每一行最初的位置即可。

要检验一个向量能否由一个基线性表示,只需用基中的向量依次对该向量消元即可。

GF(2)域

给定v1,,vmGF(2)n (m,nN)v_1,\dots,v_m\in \mathrm{GF}(2)^n\ (m,n\in\mathbb{N}^*),求其基。

GF(2)域仅有两个元素,这使得对于一组基向量,每个基向量会贡献至少一个二进制位(别的基向量都没拥有这一位)。于是可以开辟一个长为nn的数组bb,用于保存基,第ii项表示贡献了第ii位的基。初始时都设为00,表示没有基贡献这一位。依次遍历各个向量,检查其非00位。如果vi,j=1v_{i,j}=1

  1. bj=0b_j=0:目前的基中还没有能贡献第jj位的,可以将viv_i加入基中,即令bjvib_j\gets v_i。退出循环(即使它有其它贡献的位也不能重复记录)。
  2. bj0b_j\neq0:目前的基中已有向量bjb_j贡献了第jj位,跳过。

给定v1,,vmGF(2)n (m,nN)v_1,\dots,v_m\in \mathrm{GF}(2)^n\ (m,n\in\mathbb{N}^*),在其中选取任意个,求其最大异或和。

在求线性基的基础上,我们需要从高到低遍历非00位,且在发生第2.种情况时令vivibjv_i\gets v_i\oplus b_j。这样得到的bb,满足bib_i的最高位是第ii位。要求所有子集的最大异或和,只需从高位到低位考虑,如果当前位的基向量对答案有贡献就合并到答案中,而低位的部分要么也由该向量贡献,要么将来可以随意线性组合,所以无需担心。

inline void solve() {
	constexpr size_t B = 50;
	std::array<uint64_t, B> bases{};
	size_t n;
	std::cin >> n;
	for (size_t i = 0; i < n; ++i) {
		uint64_t x;
		std::cin >> x;
		for (size_t j = B; j--;) {
			if ((x >> j) & 1) {
				if (!bases[j]) {
					bases[j] = x;
					break;
				}
				x ^= bases[j];
			}
		}
	}

	uint64_t res = 0;
	for (size_t i = B; i--;) {
		res = std::max(res, res ^ bases[i]);
	}
	std::cout << res << '\n';
}

时间复杂度:O(mnmax{nW,1})O\left(mn\max\left\{\dfrac{n}{W},1\right\}\right),其中WW为位长。

广义矩阵乘法

矩乘快速幂加速递推

快速求出Fibonacci数列的nn

将递推式写作矩阵乘法形式:

[fn+1fn+2]=[0111][fnfn+1],\begin{bmatrix} f_{n+1}\\ f_{n+2} \end{bmatrix} = \begin{bmatrix} 0 & 1\\ 1 & 1 \end{bmatrix} \begin{bmatrix} f_n\\ f_{n+1} \end{bmatrix},

于是

[fnfn+1]=[0111]n[f0f1] (nN).\begin{bmatrix} f_n\\ f_{n+1} \end{bmatrix} = \begin{bmatrix} 0 & 1\\ 1 & 1 \end{bmatrix}^n \begin{bmatrix} f_0\\ f_1 \end{bmatrix}\ (n\in\mathbb{N}).

这个[0111]n\begin{bmatrix}0 & 1 \\ 1 & 1\end{bmatrix}^n可以用快速幂求出。

只要递推式可以以线性组合的形式表示(不要求一定是普通加法乘法)且所构造出的广义矩阵乘法满足结合律,就可以利用矩乘快速幂计算某一项。有时为了降低常数,我们会手写矩乘而非用循环实现。

数据结构维护广义矩乘

CACC 2024 Regional C题

给定正整数n,q[1,5×105]n,q\in[1,5\times10^5]。一个长为nn的序列a=(a0,...,an1)\vec{a}=(a_0,...,a_{n-1})初始均为00。一共qq次查询op{0,1}op\in\{0,1\}

  1. op=0op=0:再给定整数b[0,n), e[b,n],odd{0,1},dZb\in[0,n),\ e\in[b,n],odd\in\{0,1\},d\in \mathbb{Z},若odd=0odd=0则将a[b,e)a_{[b,e)}中的所有偶数均加上dd,否则将其中的所有奇数均加上dd
  2. op=1op=1:再给定整数b[0,n), e[b,n]b\in[0,n),\ e\in[b,n],求出i=be1ai\sum\nolimits_{i=b}^{e-1}a_i

对于一个区间,我们如果维护以下三个值,就可以实现题目的修改和查询要求:

用一个向量

v=[c0c1s]\vec{v}= \begin{bmatrix} c_0\\ c_1\\ s \end{bmatrix}

保存这三个值。查询时,只需获取区间的ss即为答案。

下面考虑如何将修改操作以矩阵乘法的形式描述:

这里是定义在普通加法和普通乘法上的矩阵乘法,显然满足结合律,且有单位元II,构成幺半群,考虑用线段树维护。

直接这样实现的话,进行一次矩乘需要33=273^3=27次乘法,总复杂度27nq2.56×10827\cdot n\cdot q\approx2.56\times10^8,卡卡常应该能过(CACC的数据挺水的,而且还是类IOI赛制~~,狠狠混分~~)。

struct Matrix {
	std::array<std::array<int64_t, 3>, 3> data{};

	Matrix() {
		data[0][0] = data[1][1] = data[2][2] = 1;
	}

	friend Matrix operator*(const Matrix &lhs, const Matrix &rhs) {
		Matrix res;
		for (size_t i = 0; i < 3; ++i) {
			for (size_t j = 0; j < 3; ++j) {
				res.data[i][j] = 0;
				for (size_t k = 0; k < 3; ++k) {
					res.data[i][j] += lhs.data[i][k] * rhs.data[k][j];
				}
			}
		}
		return res;
	}

	bool isIdentity() const {
		for (size_t i = 0; i < 3; ++i) {
			for (size_t j = 0; j < 3; ++j) {
				if (data[i][j] != (i == j)) return false;
			}
		}
		return true;
	}
};

struct SegTree {
	static size_t leftChild(size_t rt) { return ((rt << 1) | 1); }
	static size_t rightChild(size_t rt) { return ((rt + 1) << 1); }

	struct Node {
		std::array<int64_t, 3> val{};
		Matrix lzy{};

		void applyLazy(const Matrix &lzy) {
			std::array<int64_t, 3> new_val{};
			for (size_t i = 0; i < 3; ++i) {
				for (size_t j = 0; j < 3; ++j) {
					new_val[i] += lzy.data[i][j] * val[j];
				}
			}
			val = new_val;
			this->lzy = lzy * this->lzy;
		}
	};

	std::vector<Node> nodes;

	SegTree(size_t n) : nodes(n << 2) { build(0, 0, n); }

	void pushUp(size_t rt) {
		size_t lch = leftChild(rt), rch = rightChild(rt);
		for (size_t i = 0; i < 3; ++i) {
			nodes[rt].val[i] = nodes[lch].val[i] + nodes[rch].val[i];
		}
	}

	void build(size_t rt, size_t beg, size_t end) {
		assert(beg < end);
		if (beg + 1 == end) {
			nodes[rt].val[0] = 1;
			return;
		}

		size_t mid = beg + ((end - beg) >> 1);
		build(leftChild(rt), beg, mid);
		build(rightChild(rt), mid, end);
		pushUp(rt);
	}

	void pushDown(size_t rt) {
		assert(rightChild(rt) < nodes.size());
		if (nodes[rt].lzy.isIdentity()) return;

		nodes[leftChild(rt)].applyLazy(nodes[rt].lzy);
		nodes[rightChild(rt)].applyLazy(nodes[rt].lzy);

		nodes[rt].lzy = Matrix();
	}

	void modify(size_t rt, size_t nd_beg, size_t nd_end,
				size_t beg, size_t end, bool is_odd, int64_t diff) {
		assert(nd_beg < nd_end && beg < end);

		if (beg <= nd_beg && nd_end <= end) {
			Matrix tag;
			tag.data[2][is_odd] = diff;
			if (diff & 1) {
				if (!is_odd) {
					tag.data[0][0] = 0;
					tag.data[0][1] = 1;
				} else {
					tag.data[1][1] = 0;
					tag.data[1][0] = 1;
				}
			}
			nodes[rt].applyLazy(tag);
			return;
		}

		pushDown(rt);
		size_t nd_mid = nd_beg + ((nd_end - nd_beg) >> 1);
		if (beg < nd_mid) {
			modify(leftChild(rt), nd_beg, nd_mid, beg, end, is_odd, diff);
		}
		if (nd_mid < end) {
			modify(rightChild(rt), nd_mid, nd_end, beg, end, is_odd, diff);
		}
		pushUp(rt);
	}

	int64_t query(size_t rt, size_t nd_beg, size_t nd_end,
				  size_t beg, size_t end) {
		assert(nd_beg < nd_end && beg < end);
		if (beg <= nd_beg && nd_end <= end) return nodes[rt].val[2];

		pushDown(rt);
		size_t nd_mid = nd_beg + ((nd_end - nd_beg) >> 1);
		int64_t res = 0;
		if (beg < nd_mid) res += query(leftChild(rt), nd_beg, nd_mid, beg, end);
		if (nd_mid < end) res += query(rightChild(rt), nd_mid, nd_end, beg, end);

		return res;
	}
};

参考资料

  1. Gilbert Strang, Introduction to Linear Algebra, 4th Edition.
  2. Sheldon Axler 著, 吴俊达、何阳 译, 线性代数应该这样学, 第四版.
  3. 我的同学兼教练(?)Neutralized的课件.
  4. OI Wiki.