AI底层系列:用C++实现线性代数的公式推导与算法设计-6.线性方程组的解集

  对于线性方程组的解集我们已经并不陌生了,无非就是唯一解,无数解和无解三种情况,在本节将会从另一个角度来看待线性方程组的解集。

线性代数部分

1.齐次线性方程组与非齐次线性方程组

  考虑下方两个线性方程组:

{x1+x2+x3=103x1+2x2+x3=9x1+x2=7 \begin{cases} x_1 + x_2 + x_3 = 10\\ 3x_1 + 2x_2 +x_3 = 9\\ x_1 + x_2 = 7 \end{cases} ⎩ ⎨ ⎧x1+x2+x3=103x1+2x2+x3=9x1+x2=7

{x1+x2+x3=03x1+2x2+x3=0x1+x2=0 \begin{cases} x_1 + x_2 + x_3 = 0\\ 3x_1 + 2x_2 +x_3 =0 \\ x_1 + x_2 = 0 \end{cases} ⎩ ⎨ ⎧x1+x2+x3=03x1+2x2+x3=0x1+x2=0

  容易发现上方两个线性方程组唯一的区别就在于线性方程中等号右侧的数字是否为零,由初等行变化的性质易得:如果矩阵中某一列存在非零元素,那么我们总能够将该列变化为只存在一个非零元素的形式,但是我们绝对无非将非零列通过初等行变化变为全零列,反过来看,如果矩阵中的某一列是全零列,那么无论怎么使用初等行变化都无非将该列变为非零列,也就是说以我们现有的知识来看,在矩阵中全零列和非零列是绝对无非互相转换的,利用该性质,我们可以将线性方程组分为两类,一类是增广矩阵最右侧列为全零列,比如下方的那个线性方程组,我们称这种形式的线性方程组为齐次线性方程组 ,另一类就是增广矩阵最右侧列为非零列,称为非齐次线性方程组 ,从直观上记忆,可以认为全零列中的元素全部为零,就是"对齐"的,那么对应的线性方程组就是齐次的,反之就是非齐次的,齐次线性方程组和非齐次线性方程组的标准定义是:如果线性方程组对应的矩阵方程是Ax=0\boldsymbol{A}\boldsymbol{x}=\boldsymbol{0}Ax=0,那么就称该线性方程组是齐次的,否则称为非齐次的。

2.齐次线性方程组的解

  对于齐次线性方程组Ax=0\boldsymbol{A}\boldsymbol{x}=\boldsymbol{0}Ax=0,我们容易发现其必定有解,因为增广矩阵最右侧一列绝对不是主元列,而且显然当x=0\boldsymbol{x}=\boldsymbol{0}x=0时方程组有解,由于x=0\boldsymbol{x}=\boldsymbol{0}x=0这个解对于齐次线性方程组来说绝对存在,不会有特例,因此我们称其为齐次线性方程组的平凡解 ,对于一个固定存在的东西,我们显然是不需要太过于关心的,因此对于齐次线性方程组,我们关心的是其是否存在非平凡解 ,也就是当x!=0\boldsymbol{x}!=\boldsymbol{0}x!=0时的解,由于线性方程组在有解的情况下,要么是唯一解,要么是无数解,而判断唯一解与无数解的依据就解集中是否存在自由变量,因此可以推知:如果齐次线性方程组的解集中存在自由变量,那么该齐次线性方程组存在无数个非平凡解,如果齐次线性方程组的解集中不存在自由变量,那么该齐次线性方程组仅存在唯一解,且该解必定为平方解 ,又由于是否存在自由变量能够通过线性方程组系数矩阵的行阶梯形式看出来,简单来说从列来看就是如果系数矩阵的每一列都是主元列,那么就不存在自由变量,从行来看就是行阶梯矩阵中每一行的第一个非零元素都是连续间隔一个单位的,因此我们在判断齐次线性方程组是否存在非平凡解时,只需要将线性方程组对应的增广矩阵化为行阶梯型矩阵就可以了。考虑下方的齐次线性方程组:

{x1+9x2+2x3=02x1+27x2+4x3=0x1+18x2+2x3=0 \begin{cases} x_1 + 9x_2 + 2x_3 = 0\\ 2x_1 + 27x_2 +4x_3 =0 \\ x_1 + 18x_2 + 2x_3 = 0 \end{cases} ⎩ ⎨ ⎧x1+9x2+2x3=02x1+27x2+4x3=0x1+18x2+2x3=0

其增广矩阵对应的行化简形式为:

102001000000 \left \\begin{array}{ccc\|c} 1\&0\&2\&0\\\\ 0\&1\&0\&0\\\\ 0\&0\&0\&0 \\end{array} \\right 100010200000

回代到齐次线性方程组中并使用自由变量表示基变量得出:

{x1=−2x3x2=00=0 \begin{cases} x_1 = -2x_3\\ x_2 = 0 \\ 0 = 0 \end{cases} ⎩ ⎨ ⎧x1=−2x3x2=00=0

x3x_3x3不受任何约束,是自由变量,将上方的解集变为向量形式,得出:

x=x1x2x3=−2x30x3=x3−201 \boldsymbol{x} = \left \\begin{array}{c} x_1\\\\ x_2\\\\ x_3 \\end{array} \\right = \left \\begin{array}{c} -2x_3\\\\ 0\\\\ x_3 \\end{array} \\right =x_3 \left \\begin{array}{c} -2\\\\ 0\\\\ 1 \\end{array} \\right x= x1x2x3 = −2x30x3 =x3 −201

  显然易见的,这是一条过原点的直线,容易得知,由于齐次线性方程组必然存在平凡解,因此其解集必然过原点,当不存在非平凡解时,解集对应的就是原点,当存在非平凡解时,解集对应的就是过原点的一个子空间。要注意的是,如果齐次线性方程组对应的系数矩阵是全零矩阵,那么解集对应的就是线性方程组所处的空间,下面来看两个比较特殊的齐次线性方程组:

{x1+x2+x3=0 \begin{cases} x_1 + x_2 + x_3 = 0 \end{cases} {x1+x2+x3=0

{4x1+2x2+x3=03x1+1x2+5x3=03x1+2x2+2x3=0x2+x3=04x1+2x2+5x3=09x1+4x2=0x1+x2+x3=0 \begin{cases} 4x_1 + 2x_2 + x_3 = 0\\ 3x_1 + 1x_2 + 5x_3 = 0\\ 3x_1 + 2x_2 + 2x_3 = 0\\ x_2 + x_3 = 0\\ 4x_1 + 2x_2 + 5x_3 = 0\\ 9x_1 + 4x_2= 0\\ x_1 + x_2 + x_3 = 0\\ \end{cases} ⎩ ⎨ ⎧4x1+2x2+x3=03x1+1x2+5x3=03x1+2x2+2x3=0x2+x3=04x1+2x2+5x3=09x1+4x2=0x1+x2+x3=0

第一个线性方程组显然有解,并且解是:

{x1=−x2−x30=00=0 \begin{cases} x_1=-x_2 - x_3\\ 0 = 0\\ 0 = 0 \end{cases} ⎩ ⎨ ⎧x1=−x2−x30=00=0

对应的向量形式为:

x=x1x2x3=−x2−x3x2x3=x2−110+x3−101 \boldsymbol{x} = \left \\begin{array}{c} x_1\\\\ x_2\\\\ x_3 \\end{array} \\right = \left \\begin{array}{c} -x_2-x_3\\\\ x_2\\\\ x_3 \\end{array} \\right =x_2 \left \\begin{array}{c} -1\\\\ 1\\\\ 0 \\end{array} \\right +x_3 \left \\begin{array}{c} -1\\\\ 0\\\\ 1 \\end{array} \\right x= x1x2x3 = −x2−x3x2x3 =x2 −110 +x3 −101

显然解集是三维中的一个过原点的平面,然后是第二个线性方程组,看起来有点恐怖,但是我们可以判断其一定有解,因为增广矩阵最右侧列绝对不是主元列,并且由初等行变化的性质可知,该线性方程组增广矩阵的行最简形式必然是:

1000010000100000000000000000 \left \\begin{array}{ccc\|c} 1\&0\&0\&0\\\\ 0\&1\&0\&0\\\\ 0\&0\&1\&0\\\\ 0\&0\&0\&0\\\\ 0\&0\&0\&0\\\\ 0\&0\&0\&0\\\\ 0\&0\&0\&0\\\\ \\end{array} \\right 1000000010000000100000000000

那么最终该齐次线性方程组只存在平凡解,也就是说解集是三维中的原点。

3.非齐次线性方程组的解

  非齐次线性方程组与齐次线性方程组唯一的区别就在于其增广矩阵最右侧一列是非零列,这意味着非齐次线性方程组可能存在无解的情况(因为增广矩阵最右侧一列可能成为主元列),并且还容易推知,非齐次线性方程组一定不存在平凡解,也就是Ax=b\boldsymbol{A}\boldsymbol{x}=\boldsymbol{b}Ax=b中的x\boldsymbol{x}x不可能为零向量,因此非齐次线性方程组只存在非平凡解,我们在此处重点要讨论的是齐次线性方程组与非齐次线性方程组之间的联系,考虑下方的线性方程组:

{x1+9x2+2x3=02x1+27x2+4x3=0x1+18x2+2x3=0 \begin{cases} x_1 + 9x_2 + 2x_3 = 0\\ 2x_1 + 27x_2 +4x_3 =0 \\ x_1 + 18x_2 + 2x_3 = 0 \end{cases} ⎩ ⎨ ⎧x1+9x2+2x3=02x1+27x2+4x3=0x1+18x2+2x3=0

{x1+9x2+2x3=192x1+27x2+4x3=56x1+18x2+2x3=37 \begin{cases} x_1 + 9x_2 + 2x_3 = 19\\ 2x_1 + 27x_2 +4x_3 =56 \\ x_1 + 18x_2 + 2x_3 = 37 \end{cases} ⎩ ⎨ ⎧x1+9x2+2x3=192x1+27x2+4x3=56x1+18x2+2x3=37

显然上方的线性方程组是齐次的,下方的是非齐次的,将它们对应的增广矩阵化为行化简形式后得出:

102001000000 \left \\begin{array}{ccc\|c} 1\&0\&2\&0\\\\ 0\&1\&0\&0\\\\ 0\&0\&0\&0 \\end{array} \\right 100010200000

102101020000 \left \\begin{array}{ccc\|c} 1\&0\&2\&1\\\\ 0\&1\&0\&2\\\\ 0\&0\&0\&0 \\end{array} \\right 100010200120

  上方解集的几何含义是一条过原点的直线,将下方的结果回代到线性方程组中,得:

{x1=1−2x3x2=20=0 \begin{cases} x_1 = 1- 2x_3 \\ x_2 =2 \\ 0 = 0 \end{cases} ⎩ ⎨ ⎧x1=1−2x3x2=20=0

化为向量形式,得:

x=x1x2x3=1−2x32x3=x3−201+120 \boldsymbol{x} = \left \\begin{array}{c} x_1\\\\ x_2\\\\ x_3 \\end{array} \\right = \left \\begin{array}{c} 1-2x_3\\\\ 2\\\\ x_3 \\end{array} \\right =x_3 \left \\begin{array}{c} -2\\\\ 0\\\\ 1 \\end{array} \\right + \left \\begin{array}{c} 1\\\\ 2\\\\ 0 \\end{array} \\right x= x1x2x3 = 1−2x32x3 =x3 −201 + 120

  容易发现,如果齐次线性方程组与非齐次线性方程组的系数矩阵有相同的行化简形式,那么在非齐次线性方程组相容时,非齐次线性方程组的解集是齐次线性方程组的解集加上一个向量,并且该向量等于非齐次线性方程组对应的增广矩阵在化为行化简形式的最右侧的列向量。还可以从几何的角度进行理解,这里我们可以使用伪向量的概念,将最终的解集左侧视为一条直线的伪向量表示,右侧视为向量,那么几何含义就是将过原点的直线进行了平移,也就是这样的:

笔者的奇思妙想

1.从伪向量的视角看待齐次线性方程组和非齐次线性方程组

  在上一节中,笔者提出了伪向量的概念,其定义就是几何元素参数方程的向量表示,明显可以感觉到伪向量这个模型非常利于我们理解几何元素与向量,矩阵之间的关系,我们同样可以使用伪向量的视角来理解上方提到的线性代数中的所有概念,首先是齐次线性方程组:Ax=0\boldsymbol{A}\boldsymbol{x}=\boldsymbol{0}Ax=0,我们将x\boldsymbol{x}x视为伪向量,也就是一个几何元素参数方程的向量表示,那么Ax=0\boldsymbol{A}\boldsymbol{x}=\boldsymbol{0}Ax=0的含义就是将一个几何元素投入矩阵A\boldsymbol{A}A进行处理,将几何元素变化为原点,平凡解的含义就是几何元素x\boldsymbol{x}x本身就是原点的参数方程,即当变化目标是原点时,若几何元素本身就是原点,那么变化的结果就一定是原点,与矩阵无关,而非平凡解的含义就有说法了,考虑下方的齐次线性方程组:

{x1+3x2=03x1+9x2=0 \begin{cases} x_1+3x_2 = 0\\ 3x_1+9x_2 = 0 \end{cases} {x1+3x2=03x1+9x2=0

显然最终解的向量表示为:x2−31x_2\bigl\\begin{smallmatrix} -3 \\\\ 1 \\end{smallmatrix}\\bigrx2−31,表示一条直线,这意味着将该直线上的任意一点投入矩阵:

1339\] \\left\[ \\begin{array}{cc} 1\&3\\\\ 3\&9 \\end{array} \\right\] \[1339

处理后都可以得出原点,那么我们就可以将上方的线性方程组变为矩阵方程的形式:

1339\]\[x1x2\]=\[00\] \\left\[ \\begin{array}{cc} 1\&3\\\\ 3\&9 \\end{array} \\right\] \\left\[ \\begin{array}{cc} x_1\\\\ x_2 \\end{array} \\right\] = \\left\[ \\begin{array}{cc} 0\\\\ 0 \\end{array} \\right\] \[1339\]\[x1x2\]=\[00

  我们可以将x1x2\bigl\\begin{smallmatrix} x_1 \\\\ x_2 \\end{smallmatrix}\\bigrx1x2视为伪向量,该伪向量代表的几何元素就是解空间直线上的任意几何元素,即:

我们取该直线上的一段线段,端点为(3,−1),(6,−2)(3, -1),(6, -2)(3,−1),(6,−2),其参数方程为:

{x=3+3ty=−1−t0<=t<=1 \begin{cases} x = 3+3t\\ y = -1-t\\ 0<=t<=1 \end{cases} ⎩ ⎨ ⎧x=3+3ty=−1−t0<=t<=1

  可以让该参数方程成为伪向量x1x2\bigl\\begin{smallmatrix} x_1 \\\\ x_2 \\end{smallmatrix}\\bigrx1x2代表的几何元素,即伪向量 = 3+3t−1−t\bigl\\begin{smallmatrix} 3+3t \\\\ -1-t \\end{smallmatrix}\\bigr3+3t−1−t,将该伪向量投入上方的矩阵进行处理,得出:(3+3t)13+(−1−t)39=00(3+3t)\bigl\\begin{smallmatrix} 1 \\\\ 3 \\end{smallmatrix}\\bigr + (-1-t)\bigl\\begin{smallmatrix} 3 \\\\ 9 \\end{smallmatrix}\\bigr = \bigl\\begin{smallmatrix} 0 \\\\ 0 \\end{smallmatrix}\\bigr(3+3t)13+(−1−t)39=00,从伪向量的角度看,齐次线性方程组对应的系数矩阵能够将对应解空间中的任意几何元素映射到原点上。那么对于非齐次线性方程组来说,伪向量视角下的含义也很明确了,就是将解空间中的任意几何元素映射到指定点,从高维中定点压缩数据在计算机图形学和AI中都是绝对的核心,比如在把一个三维体建模压缩到二维时,可以将三维体切分成很多个小正方体,然后按照一定的规则映射到平面的点上,甚至还可以利于该原理实现模型以点为单位的批量精确移动。

2.几何映射变化

  基于上方的探讨,自然的会提出疑问:能否将体几何元素映射成指定面?能否将面几何元素映射成线?能否反向的将线映射成面...,在此处就来深入的讨论一下,我们先来看如何将线段映射成另一条线段,比如:

  我们希望将任意一条线段a映射变化成另一条线段b,首先线段a的参数方程是:

{xa=x1+ta(x2−x1)ya=y1+ta(y2−y1)0<=ta<=1 \begin{cases} x_a = x_1+t_a(x_2-x_1)\\ y_a = y_1+t_a(y_2-y_1)\\ 0<=t_a<=1 \end{cases} ⎩ ⎨ ⎧xa=x1+ta(x2−x1)ya=y1+ta(y2−y1)0<=ta<=1

伪向量表示为:x1+t(x2−x1)y1+t(y2−y1)\bigl\\begin{smallmatrix} x_1+t(x_2-x_1) \\\\ y_1+t(y_2-y_1)\\end{smallmatrix}\\bigrx1+t(x2−x1)y1+t(y2−y1),线段b的参数方程为:

{xb=x3+tb(x4−x3)yb=y3+tb(y4−y3)0<=tb<=1 \begin{cases} x_b = x_3+t_b(x_4-x_3)\\ y_b = y_3+t_b(y_4-y_3)\\ 0<=t_b<=1 \end{cases} ⎩ ⎨ ⎧xb=x3+tb(x4−x3)yb=y3+tb(y4−y3)0<=tb<=1

伪向量表示为:x3+t(x4−x3)y3+t(y4−y3)\bigl\\begin{smallmatrix} x_3+t(x_4-x_3) \\\\ y_3+t(y_4-y_3)\\end{smallmatrix}\\bigrx3+t(x4−x3)y3+t(y4−y3),我们要求矩阵A\boldsymbol{A}A使得:ALa=Lb\boldsymbol{A}\boldsymbol{L_a}=\boldsymbol{L_b}ALa=Lb,如果成功使用端点坐标表示出了矩阵A\boldsymbol{A}A,那么就意味着只要知道了原线段与目标线段端点,就可以使用该矩阵在计算机中让原线段变化成目标线段,下面来尝试求解,首先表示出矩阵方程:

a11a12a21a22\]\[x1+ta(x2−x1)y1+ta(y2−y1)\]=\[x3+tb(x4−x3)y3+tb(y4−y3)\] \\left\[ \\begin{array}{cc} a_{11}\&a_{12}\\\\ a_{21}\&a_{22} \\end{array} \\right\] \\left\[ \\begin{array}{cc} x_1+t_a(x_2-x_1)\\\\ y_1+t_a(y_2-y_1) \\end{array} \\right\] = \\left\[ \\begin{array}{cc} x_3+t_b(x_4-x_3)\\\\ y_3+t_b(y_4-y_3) \\end{array} \\right\] \[a11a21a12a22\]\[x1+ta(x2−x1)y1+ta(y2−y1)\]=\[x3+tb(x4−x3)y3+tb(y4−y3)

非常复杂,最终的目标是实用已知量表示出未知量:a11...a22a_{11}...a_{22}a11...a22,将其化为线性方程组的形式:

{a11x1+a11ta(x2−x1)+a12y1+a12ta(y2−y1)=x3+tb(x4−x3)a21x1+a21ta(x2−x1)+a22y1+a22ta(y2−y1)=x3+tb(y4−y3)0<=tb<=1 \begin{cases} a_{11}x_1+a_{11}t_a(x_2-x_1)+a_{12}y_1+a_{12}t_a(y_2-y_1)=x_3+t_b(x_4-x_3)\\ a_{21}x_1+a_{21}t_a(x_2-x_1)+a_{22}y_1+a_{22}t_a(y_2-y_1)=x_3+t_b(y_4-y_3)\\ 0<=t_b<=1 \end{cases} ⎩ ⎨ ⎧a11x1+a11ta(x2−x1)+a12y1+a12ta(y2−y1)=x3+tb(x4−x3)a21x1+a21ta(x2−x1)+a22y1+a22ta(y2−y1)=x3+tb(y4−y3)0<=tb<=1

有点恐怖,试着来解一下吧,先切换视角再次化为矩阵方程:

x1+ta(x2−x1)y1+ta(y2−y1)0000x1+ta(x2−x1)y1+ta(y2−y1)\]\[a11a12a21a22\]=\[x3+tb(x4−x3)y3+tb(y4−y3)\] \\left\[ \\begin{array}{cc} x_1+t_a(x_2-x_1)\&y_1+t_a(y_2-y_1)\&0\&0\\\\ 0\&0\&x_1+t_a(x_2-x_1)\&y_1+t_a(y_2-y_1) \\end{array} \\right\] \\left\[ \\begin{array}{cc} a_{11}\\\\ a_{12}\\\\ a_{21}\\\\ a_{22} \\end{array} \\right\] = \\left\[ \\begin{array}{cc} x_3+t_b(x_4-x_3)\\\\ y_3+t_b(y_4-y_3) \\end{array} \\right\] \[x1+ta(x2−x1)0y1+ta(y2−y1)00x1+ta(x2−x1)0y1+ta(y2−y1)\] a11a12a21a22 =\[x3+tb(x4−x3)y3+tb(y4−y3)

  显然线性方程组相容,但是存在自由变量,也就是说最终得出的矩阵不只一个,其实还挺好解的,因为可以轻松的将左侧的矩阵化为行最简形式,即:

1\[y1+ta(y2−y1)\]/\[x1+ta(x2−x1)\]00001\[y1+ta(y2−y1)\]/\[x1+ta(x2−x1)\]\]\[a11a12a21a22\]=\[\[x3+tb(x4−x3)\]/\[x1+ta(x2−x1)\]\[y3+tb(y4−y3)\]/\[x1+ta(x2−x1)\]\] \\left\[ \\begin{array}{cc} 1\&\[y_1+t_a(y_2-y_1)\]/\[x_1+t_a(x_2-x_1)\]\&0\&0\\\\ 0\&0\&1\&\[y_1+t_a(y_2-y_1)\]/\[x_1+t_a(x_2-x_1)\] \\end{array} \\right\] \\left\[ \\begin{array}{cc} a_{11}\\\\ a_{12}\\\\ a_{21}\\\\ a_{22} \\end{array} \\right\] = \\left\[ \\begin{array}{cc} \[x_3+t_b(x_4-x_3)\]/\[x_1+t_a(x_2-x_1)\]\\\\ \[y_3+t_b(y_4-y_3)\]/\[x_1+t_a(x_2-x_1)\] \\end{array} \\right\] \[10\[y1+ta(y2−y1)\]/\[x1+ta(x2−x1)\]0010\[y1+ta(y2−y1)\]/\[x1+ta(x2−x1)\]\] a11a12a21a22 =\[\[x3+tb(x4−x3)\]/\[x1+ta(x2−x1)\]\[y3+tb(y4−y3)\]/\[x1+ta(x2−x1)\]

最后再回代到线性方程组中并实用自由变量表示出基变量,我们令y1+ta(y2−y1)/x1+ta(x2−x1)y_1+t_a(y_2-y_1)/x_1+t_a(x_2-x_1)y1+ta(y2−y1)/x1+ta(x2−x1)为uuu,x3+tb(x4−x3)/x1+ta(x2−x1)x_3+t_b(x_4-x_3)/x_1+t_a(x_2-x_1)x3+tb(x4−x3)/x1+ta(x2−x1)为vvv,y3+tb(y4−y3)/x1+ta(x2−x1)y_3+t_b(y_4-y_3)/x_1+t_a(x_2-x_1)y3+tb(y4−y3)/x1+ta(x2−x1)为www(就是typedef重命名),得出最终的结果为:

{a11=v−ua12a21=w−ua22a12=a12a22=a22 \begin{cases} a_{11} = v-ua_{12}\\ a_{21} = w-ua_{22}\\ a_{12} = a_{12}\\ a_{22} = a_{22} \end{cases} ⎩ ⎨ ⎧a11=v−ua12a21=w−ua22a12=a12a22=a22

  不知道你有没有感受到线性代数的强大,反正笔者是感受到了,没得出解的时候就可以判断出解的情况,而且操作起来非常方便,既有明确的几何意义,又有便利的代数运算方法,不愧是人类现有最强大的数学工具之一。下面来详细的分析一下解的几何含义,我们发现解存在两个自由变量,这意味着解空间是一个平面,先尝试伪造出两条线段,看看能不能通过上方求出来的矩阵映射过去:

{xa=1+ta3ya=2+ta0<=ta<=1 \begin{cases} x_a =1+t_a3\\ y_a = 2+t_a\\ 0<=t_a<=1 \end{cases} ⎩ ⎨ ⎧xa=1+ta3ya=2+ta0<=ta<=1

{xb=5+tb6yb=1+tb90<=tb<=1 \begin{cases} x_b = 5+t_b6\\ y_b = 1+t_b9\\ 0<=t_b<=1 \end{cases} ⎩ ⎨ ⎧xb=5+tb6yb=1+tb90<=tb<=1

带入数据,得出u=(2+ta)/(1+3ta),v=(5+6tb)/(1+3ta),w=(1+9tb)/(1+3ta)u = (2+t_a)/(1+3t_a),v=(5+6t_b)/(1+3t_a),w=(1+9t_b)/(1+3t_a)u=(2+ta)/(1+3ta),v=(5+6tb)/(1+3ta),w=(1+9tb)/(1+3ta),带入解中,得:

{a11=(5+6tb)/(1+3ta)−(2+ta)/(1+3ta)a12a21=(1+9tb)/(1+3ta)−(2+ta)/(1+3ta)a22a12=a12a22=a22 \begin{cases} a_{11} = (5+6t_b)/(1+3t_a)-(2+t_a)/(1+3t_a)a_{12}\\ a_{21} = (1+9t_b)/(1+3t_a)-(2+t_a)/(1+3t_a)a_{22}\\ a_{12} = a_{12}\\ a_{22} = a_{22} \end{cases} ⎩ ⎨ ⎧a11=(5+6tb)/(1+3ta)−(2+ta)/(1+3ta)a12a21=(1+9tb)/(1+3ta)−(2+ta)/(1+3ta)a22a12=a12a22=a22

我们取最简单的,直接让a12,a22a_{12},a_{22}a12,a22为0,得出变化矩阵:

(5+6tb)/(1+3ta)0(1+9tb)/(1+3ta)0\] \\left\[ \\begin{array}{cc} (5+6t_b)/(1+3t_a)\&0\\\\ (1+9t_b)/(1+3t_a)\&0 \\end{array} \\right\] \[(5+6tb)/(1+3ta)(1+9tb)/(1+3ta)00

带入伪向量进行运算:

(5+6tb)/(1+3ta)0(1+9tb)/(1+3ta)0\]\[1+3ta2+ta\]=\[5+6tb1+9tb\] \\left\[ \\begin{array}{cc} (5+6t_b)/(1+3t_a)\&0\\\\ (1+9t_b)/(1+3t_a)\&0 \\end{array} \\right\] \\left\[ \\begin{array}{c} 1+3t_a\\\\ 2+t_a\& \\end{array} \\right\] = \\left\[ \\begin{array}{cc} 5+6t_b\\\\ 1+9t_b \\end{array} \\right\] \[(5+6tb)/(1+3ta)(1+9tb)/(1+3ta)00\]\[1+3ta2+ta\]=\[5+6tb1+9tb

  结果正确,现在来思考一下为什么会存在无数解,首先从几何的角度看,存在无数解就意味着有无数种几何变化流程可以将一个线段变化成另一个线段,这在几何上是非常合理的,就像是从点AAA移动到点BBB有无数种移动方法一样,从代数上来看存在自由变量自然就意味着无数解了。几何映射变化多种多样,笔者自然不能在一节中就手写出所有的可能性,在后续章节的奇思妙想中,笔者会不断的穿插几何映射变化,下面让我们从数学家变为程序员吧。

编程部分

  我们新学到了大量的知识,但是仅仅停留在纸上是不够的,还得写出可以运行的代码才行,先考虑一下需要实现哪些功能,首先理解线性代数的视角多种多样,比如我们之前已经实现的线性方程组,又或者是向量方程与矩阵方程等,笔者在此处选择:以伪向量为数据基础,向量和矩阵为操作基础的视角进行编程设计,因为伪向量这个概念本就是笔者结合编程创造出来的,具备极强的几何图形意义和数据意义,在本节,我们考虑将伪向量的大框架使用编程表示出来,首先我们不能再按照一个个算法的设计来实现编程了,要把知识点全部组织起来,也就是说得把《用C++实现线性代数》当成项目来写了,那么就得考虑项目的可扩展性(要把我们学到的所有线性代数知识都在该项目中实现),注意到伪向量和向量拥有相同的形式,因此可以在顶层实现一个抽象基类,其次注意到不同维度的伪向量之间存在明显的差别,因此第二次可以按照维度进行划分再次实现一批抽象基类,最终才是实现表示几何图形的具体伪向量的类,类图草稿如下:

  下面正式开始设计,我们采用基于目标导向的设计思路,也就是说先思考最终要达成的效果是什么,初步来看,最终要实现表示出0,1,2,3维任意几何元素的效果,在线性代数中我们可以使用参数方程来表示无数的概念,但是在计算机中不行,总不能随便存储一个几何元素都需要无数个点吧,内存消耗太大了,因此我们选择使用字符串表示出给人看的参数方程,在具体需要使用几何元素时就读取参数方程字符串并转换成数值,而且只存储能够唯一确定几何元素的值,比如对于平面三角形,就只存储三个顶点坐标,对于圆,就只存储圆心坐标和半径信息...,最终我们可以考虑实现一个统一的接口,用户指定维度信息,图形信息,返回一个具体的对象给用户,就类似于这样:

cpp 复制代码
T fun(二维,三角形,[(1, 2), (3, 3), (4, 5)])

  然后就返回给用户一个三角形对象,用户可以通过该对象读取参数方程,获取顶点信息,在这里我们还得考虑一点,那就是是否要在伪向量对象中提供几何变化的接口,在笔者的设计中,伪向量就是纯粹的数据,本身不应该具备变化的能力,就比如一个int a = 1;如果要让a进行变化的话,那么就必须使用运算操作符,按照这个思路,不应该在返回给用户的对象中提供变化接口,只提供读取接口,用户如果想要实现几何变化的话就必须使用向量或矩阵来进行操作,也就是说伪向量就是纯粹的数据,而向量和矩阵就是操作符,只有通过操作符才能够操作数据,而且还得要注意,两个伪向量之间进行操作是没有意义的,因此我们规定伪向量不能操作伪向量,也就是说尽管向量和伪向量拥有相同的形式,但是我们必须能够分辨出具体的类型,初步来看具体实现会涉及到模板,类型萃取,几何有效性判断等困难,一点点来实现吧,先来把大框架给搭建出来,首先的顶层的抽象基类,笔者将其命名为形式向量类:

cpp 复制代码
class FormalVector
{
};

然后是伪向量抽象基类:

cpp 复制代码
class FalseVector : public FormalVector
{
};

显然伪向量具备向量形式,和形式向量满足is_a关系,因此采用public继承,下面是抽象维度基类:

cpp 复制代码
class ZeroDimension : public FalseVector
{
};

class OneDimension : public FalseVector
{
};

class TwoDimension : public FalseVector
{
};

class ThreeDimension : public FalseVector
{
};

按照维度划分在目前看来能够起到不错的解耦效果,如果出问题的话那么就后期再调整吧,然后是具体的几何图形伪向量类,首先是零维:

cpp 复制代码
class ZeroPoint : public ZeroDimension
{
};

在线性代数中,零维就代表一个点,但是零维本身不存在信息,为了与一维的点区分开来,我们可以认为在零维中存在的几何元素是nullptr,表示一个什么都没有的点,下面来看看一维:

cpp 复制代码
class OnePoint : public OneDimension
{
};

class OneLine : public OneDimension   
{
};

class OneLineSegment : public OneDimension
{
};

在一维中存在的几何元素是点,线段与直线(一维本身就是一条直线),然后是二维,在实现前我们得先考虑一下二维中存在什么几何元素,首先从维度的角度看,二维本身就表示一个面,并且在二维中存在无数的直线与点,因此点,线,面这三个类是要实现的,然后就是从几何图形的角度看,首先不规则图形是可以由规则图形组成的,因此我们只考虑实现规则图形,对于规则图形,可以分为直边形和曲边形,显然存在无数种可能,我们先实现常见的,即:

cpp 复制代码
class TwoPoint : public TwoDimension // 点
{
};

class TwoLine : public TwoDimension // 线
{
};

class TwoFace : public TwoDimension // 面
{
};

class TwoRectilinearFigure : public TwoDimension // 直边形
{
};

class TwoLineSegment : public TwoRectilinearFigure // 线段
{
};

class TwoTrigon : public TwoRectilinearFigure // 三边形
{
};

class TwoQuadrilateral : public TwoRectilinearFigure // 四边形
{
};

class TwoPentagon : public TwoRectilinearFigure // 五边形
{
};

class TwoHexagon : public TwoRectilinearFigure // 六边形
{
};

class TwoCurvilinearFigure : public TwoDimension // 曲边形
{
};

class TwoCircle : public TwoCurvilinearFigure // 圆
{
};

class TwoEllipse : public TwoCurvilinearFigure // 椭圆
{
};

  最后是三维中的几何元素,容易发现在三维中也可以出现平面图形,那么三维的平面图形就按照二维的规则进行划分,但是我们似乎不能复用二维图形的代码,因为三维中几何图形的参数方程和二维中几何图形的参数方程是完全不同的,然后是几何体的实现,几何体的分类就更多了,得先分成大类,首先肯定可以分为规则几何体与不规则几何体,与二维类似,我们在此处就只考虑规则的,在规则几何体中,可以分为曲面几何体与非曲面几何体,在曲面几何体中我们先实现球和圆柱,在非曲面几何体中我们先只实现最简单的正方体:

cpp 复制代码
class ThreePoint : public ThreeDimension // 点
{
};

class ThreeLine : public ThreeDimension // 线
{
};

class ThreeFace : public ThreeDimension // 面
{
};

class ThreeBody : public ThreeDimension // 体
{
};

// 平面图形
class ThreeRectilinearFigure : public ThreeDimension // 直边形
{
};

class ThreeLineSegment : public ThreeRectilinearFigure // 线段
{
};

class ThreeTrigon : public ThreeRectilinearFigure // 三边形
{
};

class ThreeQuadrilateral : public ThreeRectilinearFigure // 四边形
{
};

class ThreePentagon : public ThreeRectilinearFigure // 五边形
{
};

class ThreeHexagon : public ThreeRectilinearFigure // 六边形
{
};

class ThreeCurvilinearFigure : public ThreeDimension // 曲边形
{
};

class TwoCircle : public ThreeCurvilinearFigure // 圆
{
};

class TwoEllipse : public ThreeCurvilinearFigure // 椭圆
{
};

// 体
class ThreeRectilinearBody : public ThreeDimension // 非曲面体
{
};

class ThreeCurvilinearBody : public ThreeDimension // 曲面体
{
};

class ThreeSphere : public ThreeCurvilinearBody // 球
{
};

class ThreeCylinder : public ThreeCurvilinearBody // 圆柱
{
};

class ThreeCube : public ThreeRectilinearBody // 正方体
{
};

  框架搭建完毕,我们先来考虑最核心的接口要如何与框架进行配合,简单来说最终只提供一个统一的几何类给用户进行使用,用户提供指定几何元素就可以让该几何类返回对应的几何元素,遵循以目标为导向的设计思路,我们先实现这个暴露给用户的类,首先用户可以这么进行使用:

cpp 复制代码
int main()
{
    Geometry gme(2, {{1, 2}, {5, 6}});  //指定让Geometry变为二维中的一条线段
    //或是Geometry gme(2, 3, {{1, 1}});       //指定让Geometry变为二维中的一个圆
    gme.GetDimensionMsg();              //获取维度信息
    gme.GetFigureMsg();                 //获取图形信息,返回字符串,比如四边形,五边形...
    gme.IsRectilinear();                //判断图形是否是曲边图形 
    gme.GetEndPoints();                  //对于非曲图形,获取端点信息
    gme.GetRadius();                    //对于曲图形,获取半径信息
    gme.MidPoint();                     //对于曲图形,获取中点信息
    gme.GetParametricEquation();        //获取集合元素的参数方程
    //.... 
}

  在后期实现了向量和矩阵后,还可以利用运算符重载来实现几何变化,将复杂的底层封装,提供统一便利的接口是一项基本的设计原则,而以目标为导向的设计可以瞬间让设计变得清晰并且可以保证最终的接口不会出现太大的偏差,那么下面我们就先来设计Geometry类的具体细节,首先Geometry本质上是对上方伪向量类的封装,因此其中必须要有成员变量和上方伪向量类关联,我们考虑让抽象基类FalseVector的引用成为其成员变量,利用多态行为模拟出不同的几何元素:

cpp 复制代码
class Geometry
{
public:
    typedef std::vector<std::vector<double>> EndPoints;
public:
    Geometry() = default;

    Geometry(unsigned short dmsg, EndPoints&& endpoints)
    :_false_vector()
    {}

    Geometry(unsigned short dmsg, int radius, EndPoints&& endpoints)
    :_false_vector()
    {}

private:
    std::unique_ptr<FalseVector> _false_vector;   //自动释放内存,并且每一个几何体对象都应该是唯一的,因此使用unique_ptr
};

  显然第一关就是构造函数,我们必须通过用户传递的几何信息来推断出具体的几何类型,动态开辟内存来创建出对象并返回,显然返回几何对象的函数的返回值不能采用固定类型的返回值,我们可以利用C++14中的特性,让auto成为返回值(笔者还是第一次知道有这个特性),此时编译器就会自动进行类型推导,比如下方代码:

cpp 复制代码
struct A
{
    int a = 10;
    int b = 20;
};

auto fun()
{
    A* a = new A();
    return a;
}

int main()
{
    A* pa = fun();
    std::cout << pa->a << " " << pa->b << std::endl;
    return 0;
}

  那么返回值就搞定了,现在主要的难题是设计几何匹配算法,首先可以使用维度信息进行第一次匹配,然后进行参数合理性检测,如果参数逻辑错误,那么直接抛出异常,初次匹配设计如下:

cpp 复制代码
auto Init(unsigned short dmsg, EndPoints &&endpoints)
{
    switch (dmsg)
    {
    case 0:
    {
        // endpoints = {}
        if (endpoints.size() != 0)
            throw std::invalid_argument("Array must be empty when zero-dimensional is specified.");
        FalseVector *zp = new ZeroPoint();
        return zp;
        break;
    }
    case 1:
    {
        // endpoints = {{x}}一个点  或   {{x}, {y}}一条线段   如果传入的点超过了两个,那么只取前两个进行判断
        if (endpoints.size() == 1)
        {
            if (endpoints[0].size() != 1)
                throw std::invalid_argument("1D requires 1 or 2 points (sub-array size = 1).");
            FalseVector *op = new OnePoint(std::move(endpoints));
            return op;
        }
        else if (endpoints.size() >= 2)
        {
            if (endpoints[0].size() != 1 || endpoints[1].size() != 1)
                throw std::invalid_argument("1D requires 1 or 2 points (sub-array size = 1).");
            if (endpoints[0][0] == endpoints[1][0]) // 指定的两个点相同,因此返回一个一维点
            {
                FalseVector *op = new OnePoint(std::move(endpoints));
                return op;
            }
            else
            {
                FalseVector *ol = new OneLineSegment(std::move(endpoints));
                return ol;
            }
        }
        else
            throw std::invalid_argument("1D requires 1 or 2 points (sub-array size = 1).");
        break;
    }
    case 2:
    {
        FalseVector *two_all = CheckTwoGeo(std::move(endpoints));
        return two_all;
        break;
    }
    case 3:
    {
        FalseVector *three_all = CheckThreeGeo(std::move(endpoints));
        return three_all;
        break;
    }
    default:
        throw std::invalid_argument("Unsupported dimension. Only 0-3D is allowed.");
    }
    throw std::invalid_argument("Unsupported dimension. Only 0-3D is allowed.");
}

  在此处要注意的是当使用了auto推导返回类型时,每一条分支的返回类型都必须是一样的,否则就会编译报错(因为编译器无法判断出那条分支会返回,具体的逻辑是在运行时才可以确定的),对于零维和一维来说,匹配后就可以直接返回对象了,但是对于二维和三维要设计复杂的匹配策略,也就是实现CheckTwoGeo和CheckThreeGeo函数,我们先考虑二维的,首先在该Init函数中我们判断的都是只存在端点的非曲几何元素,因此在endpoints中的一定都是端点,首先必须满足每个端点中都有两个元素,其次可以通过端点的数量进行匹配,由于我们设计的二维几何图形类最多只能够表示出六边形,因此端点数量最多为六个,如果用户传入的端点数量超过了六,那么我们就只取前六个端点进行判断,关键的难题就是判断端点定下来的线段是否是重合的,是否能够构成有效的几何图形,比如传入三个端点构成的不一定就是三角形了,还要考虑端点重合的情况,并且在端点数量大于四时还得考虑线段重合的情况,先把函数的架子搭出来:

cpp 复制代码
FalseVector *CheckTwoGeo(EndPoints &&endpoints)
{
    // 匹配端点数量
    int point_nums = endpoints.size();
    FalseVector *ret = nullptr;
    switch (point_nums)
    {
    case 1:
    {
        if (endpoints[0].size() != 2)
            throw std::invalid_argument("The endpoint must be a 2-tuple.");
        ret = new TwoPoint(std::move(endpoints));
        return ret;
        break;
    }
    case 2:
    {
        if (endpoints[0].size() != 2 || endpoints[1].size() != 2)
            throw std::invalid_argument("The endpoint must be a 2-tuple.");
        if (endpoints[0][0] == endpoints[0][1] && endpoints[1][0] == endpoints[1][1]) // 端点重合,返回二维点
        {
            ret = new TwoPoint(std::move(endpoints));
            return ret;
        }
        else
        {
            ret = new TwoLineSegment(std::move(endpoints));
            return ret;
        }
        break;
    }
    case 3:
    {
    }
    case 4:
    {
    }
    case 5:
    {
    }
    case 6:
    {
    }
    default:
        break;
    }
}

  当端点数量大于2时就要进行复杂的判断流程了,首先得进行合法性判断,我们可以封装出一个函数:

cpp 复制代码
bool IsLegalEndPoint(unsigned short dmsg, EndPoints &&EndPoints)
{
    for (int i = 0; i < EndPoints.size(); i++)
    {
        if (EndPoints[i].size() != dmsg)
            return false;
    }
    return true;
}

  该函数是通用的,因此笔者将上方所有合法性判断都替换为该函数了,然后是判断端点是否重合,同样的我们设计出一个通用的函数,该函数将重合的端点去除,返回新的有效端点集,显然我们的代码需要重构一次了,完全可以在调用Init前就把用户传进来的端点统一清洗一次,后面就不用那些麻烦的判断了,并且这个清洗模块还可以更强大一些,比如直接就把那些位于两点线段之间的端点给洗掉,也就是这种端点:

现在来考虑具体的实现,首先清洗分为三步:1.合法性判断,2.洗掉重复端点,3.洗掉线段之间的端点,第一步非常简单和第二步都非常简单,我们先把这两个简单的步骤给实现了:

cpp 复制代码
namespace UTIL_MOD
{
    struct Util
    {
        typedef std::vector<std::vector<double>> EndPoints;
        //判断指定端点是否合法
        static bool IsLegalEndPoint(unsigned short dmsg, int index, EndPoints&& endpoints)
        {
            if(endpoints[index].size() != dmsg) 
                return false;
            return true;
        }

        //判断端点合法性并将无效的端点全部清洗掉
        static EndPoints CleanEndpoints(unsigned short dmsg, EndPoints&& endpoints)
        {
            EndPoints ret;
            for(int i = 0; i < endpoints.size(); i++)
            {
                //1.合法性判断
                if(!IsLegalEndPoint(dmsg, i, std::move(endpoints)))
                    throw std::invalid_argument("Dimension-endpoint mismatch");

                //2.洗掉重复端点
                int flag = 1;
                for(int j = i + 1; j < endpoints.size(); j++)
                {
                    //为了防止迭代器失效,不能之间删除endpoints中的端点
                    if(IsSamePoint(dmsg, std::move(endpoints), i, j))
                        flag = 0;
                }

                //flag为1说明下标i处的端点和后面所有的端点都不重复,那么就插入到ret中
                if(flag)
                {
                    ret.push_back(endpoints[i]);
                }
            }

            //ret中存储都是合法且不重复的端点,下面要洗掉线段上的端点
            
            return ret;
        }

        //判断两个端点是否相同
        static bool IsSamePoint(unsigned short dmsg, EndPoints&& endpoint, int i, int j)
        {
            for(int k = 0; k < dmsg; k++)
            {
                if(endpoint[i][k] == endpoint[j][k])
                    return true;
            }
            return false;
        }
    };
}

最后的第三步需要好好设计一下了,情况比较复杂,比如如果用户传入的端点是这样的:

  这显然是合法的端点,并且也没有重复的,连位于两点线段上的端点都没有,但是中间那个红色的端点显然是有问题的,我们要做的就是把这种有问题的点给洗掉,指望用户的几何水平显然是不可行的,而且在三维中会更加复杂,先将假设出一个尽可能包含更多情况的实例:

  此时就算是使用肉眼分辨都有些困难了,更别说没有眼睛的计算机了,我们可以利用有些边一定位于最外层的性质,使用圆捕法(笔者自创的名称,有没有类似的方法笔者也不知道),具体操作就是这样的:

首先构造出一个尽可能小的包含所有端点的圆,然后逐步让圆缩小,在缩小的过程中将暴露出去的端点全部相连,直到连接成一个闭几何图形,得出的端点就是有效端点了,圆内部剩余的端点全部清洗掉,在三维显然可以使用类似的操作的球捕法,我们先实现二维的圆捕法,要解决的主要问题有:初始圆的构造,半径缩小,碰撞检测和端点连接,先来解决初始圆的构造,该圆内部必须包含所有端点,并且半径要尽可能的小,先分析最简单的情况,即只存在两点,此时显然可以使用这两点作为圆直径,正好就能够包含:

然后是三点的情况,此时分为三点共线和三点不共线两种情况:

此时我们得先以一点为起点,连接剩下的两个点,判断出是否共线,如果共线,那么得比较两条线段的长度,以较长长度的线段最为圆的直径,如果三点不共线,那么可以利用求三角形外切圆的方法得出中点坐标,进而求出圆半径,此时就涉及到了线段长度计算,线段中点计算和线段交点判断,然后是四点的情况,能够直接想到的就是下方的四种情况:

显然四点的情况就已经非常复杂了,而且还是二维的,三维中的四点会涉及到更多的情况,或许我们应该从几何直观中总结通用规律了,可以采用一个"笨方法",我们任意选择一个点最为圆形,让半径从零开始往外扩充,并且维护一个从一开始计数的计数器,在扩充途中每碰撞到一个点都让计数器加一,知道计数器的值和端点数相等,那么该圆就一定包含了所有端点,如下图:

该策略只能够保证初始圆包含所有端点,但是无法保证初始圆最小,后期再考虑优化吧,那么第一步构造初始圆就完成了,然后是半径缩小,容易发现算法出现问题了,如果以圆心为基点并按照半径进行缩小的话就会出现这种情况:

显然此时图形闭环了,但是没有得出正确的边,其实在第三个点出圆时图形就已经闭环了,因此我们不能采用以圆心为基点并按照半径进行缩小的策略,我们应该让圆以第一个碰撞到的端点为缩小基点,然后直到碰撞到第一个端点,得出的第一条线段一定是图形的有效边:

此时圆就变为了包含所有端点的最小圆,有两个端点位于圆上,但是此时对于圆碰撞到的第二个端点,我们必须确定要使用哪一个端点与其相连,最终的目标是得出面积最大的闭环线段图形,可以考虑做出以存在线段的中线,将所有点划分为两部分:

显然所有点被一分为二了,并且我们发现只有同边的点进行相连才可以得出面积最大的闭环图形,并且此时我们还发现在圆底部出现了同时与两个点碰撞的情况,那么首先这两个点一定能够构成一条有效边长,其次这两个点都位于下半部分,因此我们让原先的点同时连接这两个点,保留短的作为有效边长,得出:

显然易见的一点是在闭环图形中,一个端点能且只能衍生出两条线段,因此在连接过程中,我们应该得统计每个点已衍生线段的个数,如果已经为二了,那么该点就不能再连接新的端点了,还容易发现在连接途中,每次都有且只有两个端点拥有连接新端点的能力,我们应该得每次都求出拥有连接端点的线段的中线,以此确定下次碰撞时要让哪个端点继续连接,比如在上图中碰撞到了圆心点,...嗯...笔者发现中线划分策略是有问题的,比如这种情况:

显然此时就无法连接了,这个点是凹进去的,我们要考虑用户是不是真的需要一个凹几何元素,毕竟这也是非常常见的,圆捕法能够得出的是指定端点中面积最大的几何元素,不过用户可能需要的是这种几何元素:

作为开发人员,我们得提供给用户选择的权利,而不是自顾自的自作多情,因此我们实际上应该将圆捕法最为一个内置的工具实现,当用户希望求出端点构成的最大面积的几何图形时,再使用圆捕法进行计算,因此最终的结论就是用户传进来的端点,只有是合法的,不重复的,不位于同一线段上的,都是有效端点,我们在连接图形时,只需要去除掉无效点,然后按照用户指定传入端点的顺序进行连接就可以了,笔者在问了AI后,发现圆捕法实际上是一个求凸包的算法,我们相当于自己设计出了一个强大的几何算法,当然目前还存在不少问题,但是框架和思路是可行的,这么有意思的东西自然是得手撕出来的,不过具体细节我们后面再谈,本节到此处就结束了,我们搭建出了项目的框架(还存在不少问题),设计出了总体思路,并且学习了齐次与非齐次线性方程组,希望本节能够让读者有所收获。

相关推荐
古城小栈1 小时前
Python 的主流Ai框架为什么优先适配 Linux 系统?
linux·人工智能·python
luoyayun3611 小时前
从零实现 EBU R128 LUFS 响度分析:K-weighting 滤波、双门限算法
算法·lufs响度分析
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年6月15日
大数据·人工智能·python·ai·信息可视化·自然语言处理·灵砚智能
暮云星影1 小时前
瑞芯微rk3588利用Rockchip NPU运行大语言模型(LLM)
arm开发·人工智能·语言模型·自然语言处理
ujainu小1 小时前
CANN ops-transformer:编译和运行 FlashAttention 示例
人工智能·深度学习·transformer
小糯米6011 小时前
JS 数组
数据结构·算法·排序算法
Xiaofeng36931 小时前
硬核编码与推理对决:Gemini 3.5 Flash vs GPT-5.5 真实能力横向测评
人工智能·gpt
邵宇然1 小时前
编译优化技术全解:从 LLVM Pass 到链接时优化的性能提升路径
人工智能
宝贝儿好1 小时前
【LLM】第一章:知识体系框架概览
人工智能·深度学习·机器学习·自然语言处理