1.行阶梯型矩阵与行化简矩阵

  在第一小节中,我们已经学会了使用行初等变化解存在唯一解的线性方程组,但是解的情况是分为三种的,即: 唯一解,无数解,无解,我们编写的程序也只能够处理唯一解的情况,因此在本节将学习两个新的矩阵并为程序实现判断节情况的功能。

1.1解的存在性判断

  对于线性方程组解的情况,我们提出两个问题:

问题:
线性方程组是否相容,也就是是否存在解?
如果线性方程组存在解,那么解是否唯一?

  这两个问题称为解的存在性问题与解的唯一性问题,在第一节中我们已经见过了只存在唯一解的线性方程组,下面来看看存在无数解的线性方程组与无解的线性方程组是什么样子的:

无数解:
{ x 1 + x 2 + x 3 = 10 2 x 1 + x 3 = 8 \begin{cases} x_1+x_2+x_3 = 10\\ 2x_1 + x_3 = 8 \end{cases} {x1+x2+x3=102x1+x3=8

无解:
{ x 1 + x 2 = 9 3 x 1 + x 2 = 12 4 x 1 + 3 x 2 = 10 \begin{cases} x_1+x_2=9\\ 3x_1 +x_2=12\\ 4x_1+3x_2 = 10 \end{cases} x1+x2=93x1+x2=124x1+3x2=10

  使用第一节学到到行初等变化进行求解,看看这两个线性方程组的解是什么样子的,我们先解存在无数解的线性方程组,使用行初等变化化简后变为:

[ 1 0 1 2 4 0 1 1 2 6 ] \left[ \begin{array}{ccc|c} 1&0&\frac{1}{2}&4\\ 0&1&\frac{1}{2}&6 \end{array} \right] [1001212146]
放回到线性方程组中,得:
{ x 1 + 1 2 x 3 = 4 x 2 + 1 2 x 3 = 6 \begin{cases} x_1+\frac{1}{2}x_3 = 4\\ x_2 + \frac{1}{2}x_3 = 6 \end{cases} {x1+21x3=4x2+21x3=6
x 3 x_3 x3移动到等式左侧,得:
{ x 1 = 4 − 1 2 x 3 x 2 = 6 − 1 2 x 3 \begin{cases} x_1 = 4-\frac{1}{2}x_3\\ x_2=6-\frac{1}{2}x_3 \end{cases} {x1=421x3x2=621x3
  发现了吗, x 1 x_1 x1 x 2 x_2 x2完全是由 x 3 x_3 x3决定的,但是 x 3 x_3 x3本身就是一个未知数,可以是任意值,这意味着此时 x 1 x_1 x1, x 2 x_2 x2 x 3 x_3 x3都不是唯一的,这就是所谓的线性方程组存在无数个解了,我们还可以从几何的视角看待无数解,对于上方的线性方程组,先使用第一个线性方程中的 x 1 x_1 x1表示出 x 3 x_3 x3,得出 x 3 = 8 − 2 x 1 x_3 = 8 - 2x_1 x3=82x1,然后再将第二个线性方程中的 x 3 x_3 x3替换掉,得出 x 2 = x 1 + 2 x_2=x_1+2 x2=x1+2,发现了吗,最终的解实际上是一条直线,从几何的视角看,存在三个未知数的线性方程组表示的就是一个平面,因此上方无穷多个解的例子就是两个平面相交,得出一条直线,直线上的每一个点都同时满足这两个线性方程的约束,这意味着每一个点都是一个解,而一条直线上存在无数个点,因此就存在无数个解了。下面来看看无解的情况,同样的先将线性方程组化为增广矩阵然后进行化简,得出:
[ 1 0 − 17 0 1 26 0 0 37 ] \left[ \begin{array}{cc|c} 1&0&-17\\ 0&1&26\\ 0&0&37 \end{array} \right] 100010172637
回代到线性方程组中,得:
{ x 1 = − 17 x 2 = 26 0 = 37 \begin{cases} x_1=-17\\ x_2=26\\ 0=37 \end{cases} x1=17x2=260=37
  非常明显,0是不可能等于37的,从代数的角度看,该线性方程组明显无解,当然也可以从几何的角度看,让我们把线性方程组中的三个线性方程全部画出来:
在这里插入图片描述
  显然,这三条直线没有相交于一点,对应的线性方程组当然就不存在解了,你可能会问: 不是两两相交了吗?交点有三个,那不应该是存在三个解吗?会产生这个疑问,本质上是因为对于线性方程组的"解"这个概念还不够明确,下面从多个角度分析"解",以此来回答该问题,首先在第一节中我们说线性方程本质上是约束,而线性方程组的解就是满足所有线性方程约束的集合,还可以从"满足"的角度来看,在这个角度,线性方程组的解就是满足所有线性方程等式的值的集合,在几何的角度,线性方程组的解就是一个个点,这些点必须同时位于每一个线性方程构成的几何图形中,在三条线两两相交的情况下,显然每一个交点能且只能位于两个线性方程构成的直线上,不存在一个点同时位于三个线性方程构成直线上,因此就不存在解了。

1.2行阶梯矩阵

  我们可以采用行初等变化,直接解出线性方程组,以此来确定线性方程组解的情况,但是这太复杂了,如果我们的需求只是判断某一个线性方程组解的情况,那么其实是不需要完整的解出线性方程组的,更准确的说,在使用行初等变化的过程中,我们就已经能够通过增广矩阵的形式判断出线性方程组解的情况了,这个在解线性方程组的某一个特殊步骤中产生的矩阵,就是所谓的行阶梯型矩阵,尽管目前还没有介绍任何有关行阶梯型矩阵的知识,但是我们可以得知,行阶梯型矩阵的一个重要作用就是判断线性方程组解的情况。在讲解抽象的概念前,先来看看行阶梯型矩阵是什么样子的:
[ 5 2 0 0 5 0 0 0 9 ] [ 9 8 0 10 0 0 4 2 0 0 0 3 ] [ 5 2 3 0 5 0 0 0 2 0 0 0 ] [ 0 2 3 9 0 0 1 3 0 0 0 0 0 0 0 0 ] \begin{array}{c c c c} \begin{bmatrix} 5 & 2 & 0 \\ 0 & 5 & 0 \\ 0 & 0 & 9\\ \end{bmatrix} & & \begin{bmatrix} 9 & 8 & 0 & 10\\ 0 & 0 & 4 & 2\\ 0 & 0 & 0 & 3 \end{bmatrix} & & \begin{bmatrix} 5 & 2 & 3 \\ 0 & 5 & 0 \\ 0 & 0 & 2\\ 0&0&0 \end{bmatrix} & \begin{bmatrix} 0 & 2 & 3 & 9\\ 0 & 0 & 1 & 3 \\ 0 & 0 & 0 & 0\\ 0 & 0 & 0 & 0 \end{bmatrix} \end{array} 500250009 9008000401023 500025003020 0000200031009300
再来看看非行阶梯型矩阵是什么样子的:
[ 5 2 1 0 5 6 0 1 9 ] [ 9 8 7 10 0 0 4 2 0 2 0 0 ] [ 5 2 3 0 5 6 0 0 0 0 0 1 ] \begin{array}{c c c c} \begin{bmatrix} 5 & 2 & 1 \\ 0 & 5 & 6 \\ 0 & 1 & 9\\ \end{bmatrix} & & \begin{bmatrix} 9 & 8 & 7 & 10\\ 0 & 0 & 4 & 2\\ 0 & 2 & 0 & 0 \end{bmatrix} & & \begin{bmatrix} 5 & 2 & 3 \\ 0 & 5 & 6 \\ 0 & 0 & 0\\ 0& 0 & 1 \end{bmatrix} \end{array} 500251169 9008027401020 500025003601
在说明标准定义前,得补充一些概念: 如果一行全是零元素,那么称该行为零行,对于至少存在一个非零元素的行,称为非零行,非零行中从左往右的第一个非零元素称为该行的先导元素,然后可以得出行阶梯型矩阵的性质:

性质
1.所有非零行都必须在零行的上面
2.先导元素所在列下方的元素必须全部为零
3.每一行的先导元素都必须位于上一行先导元素的右侧

在满足这些性质的基础上,如果还满足了下方的性质,那么就称这种类型的矩阵为行化简矩阵:

性质
4.非零行的先导元素值为1
5.先导元素1是该元素所在列的唯一非零元素

重点:**1.所有元素都为零的矩阵即是行阶梯型矩阵,又是行化简矩阵,因为它满足上方的五条性质;2.行化简矩阵一定是行阶梯型矩阵,但是行阶梯型矩阵不一定是行化简矩阵。**下面来举几个行化简矩阵的例子:
[ 1 0 0 0 1 0 0 0 1 ] [ 1 8 0 10 0 0 1 2 0 0 0 0 ] [ 1 0 0 0 1 0 0 0 1 0 0 0 ] \begin{array}{c c c c} \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1\\ \end{bmatrix} & & \begin{bmatrix} 1 & 8 & 0 & 10\\ 0 & 0 & 1 & 2\\ 0 & 0 & 0 & 0 \end{bmatrix} & & \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1\\ 0& 0 & 0 \end{bmatrix} \end{array} 100010001 1008000101020 100001000010
  发现了吗,我们使用行初等变化解线性方程组,其实就是把增广矩阵化简为行化简矩阵,在第一节中,我们称该方法为消元法,但是实际上更加准确的称呼是: 行化简算法,在设计相应的C++算法时,第一步的对换变化就是对换出了行阶梯型矩阵,之后使用倍乘变化与倍加变化就是把行阶梯型矩阵化简成了行化简矩阵。
  对于一个存在非零元素的普通矩阵,一定能够通过行化简变化出多个不同的行阶梯型矩阵,比如对于矩阵 ( 2 1 0 1 ) \bigl(\begin{smallmatrix} 2 & 1 \\ 0 & 1 \end{smallmatrix}\bigr) (2011),使用倍乘变化让每一行乘以2,就可以得出一个新的行阶梯型矩阵: ( 4 2 0 2 ) \bigl(\begin{smallmatrix} 4 & 2 \\ 0 & 2 \end{smallmatrix}\bigr) (4022),也就是说一个非零的普通矩阵能够通过行初等变化变为无数个行阶梯型矩阵,但是对于任意一个含非零元素的普通矩阵,能且仅能变化出一个行化简矩阵,在上文说解线性方程组的过程就是把增广矩阵化简为行化简矩阵,但是这并不意味着无解或是存在无数解的线性方程组的增广矩阵就不能化简为行化简矩阵了,只要存在非零元素,任何矩阵都可以变为行阶梯型矩阵与行化简矩阵,与线性方程组解的情况无关。
  对于唯一性,以目前的知识储备还无法使用代数给出严谨从证明,但是可以从线性方程组与矩阵的对应关系的角度来大概的证明一下,首先一个线性方程组的解只存在三种情况,对于任意一个线性方程组,都可以使用一个矩阵来进行表示,反过来看,使用线性方程组同样也可以表示出任意一个矩阵,而求解线性方程组就是将线性方程组对应的增广矩阵化简成行化简矩阵,因此可以从解的情况入手,证明行化简矩阵变化的唯一性,首先是线性方程组只存在唯一解的情况,此时行化简矩阵的形式必然就是这样的:
[ 1 0 0 ⋯ 0 b 1 0 1 0 ⋯ 0 b 2 0 0 1 ⋯ 0 b 3 ⋮ ⋮ ⋮ ⋱ ⋮ ⋮ 0 0 0 ⋯ 1 b 4 ] \begin{bmatrix} 1 & 0 & 0 & \cdots & 0 & b_1 \\ 0 & 1 & 0 & \cdots & 0 & b_2 \\ 0 & 0 & 1 & \cdots & 0 & b_3 \\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & 0 & \cdots & 1 & b_4 \end{bmatrix} 1000010000100001b1b2b3b4
由于解是唯一的,因此对应的行化简矩阵必然也是唯一的,然后是存在无数解的情况,此时从几何的角度入手,虽然解是无数的,但是解所在的直线,平面,体是唯一的,不可能存在两个平面相交于多条直线的情况,因此对应的行化简矩阵也是唯一的,最后就是无解的情况,此时在几何上,可以是三条直线两两相交,尽管不存在解,但是交点绝对是固定的,对应到行化简矩阵中就是唯一的,当前的论证仅仅只是依靠直觉的,并不完备,在学习了后面的章节后会给出严谨的论证。

笔者的奇思妙想:
  在奇思妙想中的推导和理论不保证正确性和严谨性,就只是奇思妙想,读者可以直接跳过,不影响后面的内容。写到这里,笔者的脑海中出现几个问题,来看看吧:
  在一个线性方程组中,如果线性方程的未知数的个数是不同的,那么这些线性方程表示的是同一个维度的信息吗?换句话说就是一个线性方程组的整体维度是否是确定的?如果是,那么是由其中的哪一个线性方程决定的?我们从维度的角度出发,首先高维度能够表示低维度的信息,但是低维度绝对无法表示高维度的信息,比如二维中绝对无法表示出高度信息,但是在三维中却可以表示出二维的长和宽,那么一个线性方程组的维度就应该是由其最高维度的线性方程确定的,也就是未知数最多的那个线性方程,只有这样才能够表示出所有其它线性方程代表的信息,那么就又产生了一个问题,想象你的面前存在一个平面和一条直线,然后它们相交了,显然相交的位置是一个点,那么解就是唯一的?好像有点奇怪,来举一个简单的例子看看:
{ x 1 + x 2 + x 3 = 3 x 1 + x 2 = 2 \begin{cases} x_1+x_2+x_3 = 3\\ x_1+x_2 = 2 \end{cases} {x1+x2+x3=3x1+x2=2
解出来的行化简矩阵是这样的:
[ 1 1 0 2 0 0 1 1 ] \left[ \begin{array}{ccc|c} 1&1&0&2\\ 0&0&1&1 \end{array} \right] [10100121]
带入 x 1 , x 2 , x 3 x_1,x_2,x_3 x1x2x3得出线性方程组:
{ x 1 + x 2 = 2 x 3 = 1 \begin{cases} x_1+x_2=2\\ x_3=1 \end{cases} {x1+x2=2x3=1
  …嗯…显然解出来的不是一个点,但是为什么呢?从几何上来看一条线与一个平面就是会交与一点的啊,先来看看解的几何意义是什么,我们假定 x 1 , x 2 , x 3 x_1,x_2,x_3 x1,x2,x3分别表示长,宽,高,那么 x 3 x_3 x3等于1的几何含义就是高被确定了,因此 x 1 , x 2 x_1,x_2 x1,x2表示的就是三维中高为1的平面上的一条线,…嗯…解的逻辑是自洽的,换个角度来看,在三维中的两个未知数表示出来的真的是一条线吗?在二维面中,由于没有高信息,因此两个未知数表示的就是一条线,那么在二维中的一个未知数表示的就是一个点吗?举个例子x=3,好的,发现问题出在哪里了,x=3在二维中表示的显然是一条线,此时y没有任何约束,可以为任意值,那么对应到三维中二个未知数线性方程的情况,在线性方程 x 1 + x 2 = 2 x_1+x_2=2 x1+x2=2中,不存在 x 3 x_3 x3不代表着 x 3 x_3 x3默认就是0了,而是 x 3 x_3 x3可以为任意值,不受任何约束,此时由于 x 3 x_3 x3代表的是高,因此 x 1 + x 2 = 2 x_1+x_2=2 x1+x2=2在三维中实际上的几何含义就是由直线 x 1 + x 2 = 2 x_1+x_2=2 x1+x2=2和直线 x 3 x_3 x3轴共同组成的平面,因此上方的线性方程组本质上还是两个平面相交,得出的解自然就是一条线了。那么再想想看,在三维中只存在一个未知数的线性方程的几何含义是什么?比如 x 2 = 5 x_2=5 x2=5,此时 x 1 , x 3 x_1,x_3 x1,x3可以是任意值,在三维中,两个任意值表示出来的显然是一个平面,也就是说三维中的一个线性方程表示的似乎必然是一个平面,也就是说在线性方程组中,整体的维度由线性方程组中未知数最多的那个线性方程决定,比如上方的那个线性方程组,未知数最多的线性方程中有三个未知数,那么线性方程组整体就是位于三维的,线性方程组中的每一个线性方程表示的都是一个三维中的平面,与未知数的个数无关,扩充一下就是在一个线性方程组中,如果未知数最多的那个线性方程拥有n个未知数,那么线性方程组整体就位于n维,线性方程组中的每一个线性方程表示的都是一个n维中n-1的几何体,与未知数的个数无关。
  有点神奇,在线性代数的角度,线性方程组似乎永远无法表示出本维度的所有信息,最多只能表示低一维的信息,并且还可以发现,随着维度的上升,信息量似乎是成指数级增长的,比如用d表示一个点,用n表示无穷,那么零维的信息就是d,只有一个点,一维中有n个点,那么信息量就是 n ∗ d n*d nd,在二维中,一个面是由无穷条线组成的,那么信息量就是 n ∗ n ∗ d n*n*d nnd,同理,三维体又是由无数个面组成的,那么信息量就是 n ∗ n ∗ n ∗ d n*n*n*d nnnd,而在生活中,身为三维生物的我们获取信息的主要渠道就是从二维获取,比如你刷手机,打游戏,本质上获取的都是手机屏幕上的二维信息,那么提出一个猜想:在任意维度中,获取信息主要都是从n-1维获取的,并且又由无数点构成线,无数线构成面,无数面构成体可以推测出,无数个体的信息必然能够组成四维,那么设想一下,如果我们的文明强大到能够获取三维世界(整个宇宙)中的所有信息,并且还可以随着时间的推进不断的把信息存储下来,那么不就是近似的实现了无数个三维体信息构成四维吗?此时我们就实现了升维,目前看来,计算机是最有潜力实现的,当算力强大到能够计算这个宇宙,存储高到能够存储宇宙中的所有信息时,那么或许就可以让我们一窥四维空间的样貌了。
  一下子就有趣起来了,下面来思考另一个问题:如果在一个二维平面上,有三条直线交与同一个点,那么它们构成的线性方程组是存在唯一解的吗?让我们来看一个简单的示例:
{ x 1 + 2 x 2 = 6 x 1 − x 2 = 0 2 x 1 + x 2 = 6 \begin{cases} x_1+2x_2=6\\ x_1-x_2=0\\ 2x_1+x_2=6 \end{cases} x1+2x2=6x1x2=02x1+x2=6
上方的线性方程组中的每一个线性方程都经过点 ( 2 , 2 ) (2,2) (2,2),其增广矩阵是:
[ 1 2 6 1 − 1 0 2 1 6 ] \left[ \begin{array}{cc|c} 1&2&6\\ 1&-1&0\\ 2&1&6 \end{array} \right] 112211606
化为行化简矩阵:
[ 1 0 2 0 1 2 0 0 0 ] \left[ \begin{array}{cc|c} 1&0&2\\ 0&1&2\\ 0&0&0 \end{array} \right] 100010220
回代到线性方程组中:
{ x 1 = 2 x 2 = 2 0 = 0 \begin{cases} x_1=2\\ x_2=2\\ 0=0 \end{cases} x1=2x2=20=0
  嗯,在意料之中,线性方程组和几何确实是一一对应的,在几何中交于一点,那么线性方程组似乎就是存在唯一解的,在化简的过程中,有一个线性方程组被消去了,因此并不是说线性方程组中线性方程的个数大于线性方程组的维度,线性方程组就一定无解了,按照同样的思路,我们还可以想象在二维平面中重合的三条直线,那么这三条直线所构成的线性方程组应该就是存在无数解的,从几何直觉上,我们可以得出这些结论,但是还无法使用代数证明,好了,奇思妙想结束,回到严谨的正文吧。

  现在我们已经明白了什么是行阶梯型矩阵与行化简矩阵,但是目前还并没有学会要如何使用行阶梯型矩阵判断线性方程组解的情况,不过先不着急,让我们从数学家变成程序员,将上方的数学定理使用程序语言实现吧。

1.3使用C++判断行阶梯矩阵与行化简矩阵

  在上解中,我们成功的使用了C++表示出线性方程组并根据初等行变化设计出了相应的解线性方程组的算法,也就是下面这两个函数:

//将矩阵变化为行阶梯型矩阵
void PivotDiagonal(matrix& m)
{
    int row_size = m.size();
    int column_size = m[0].size();
    
    for(int i = 0; i < row_size; i++)
    {
        //1.找到对角线元素为0的行
        if(m[i][i] == 0)
        {
            for(int j = 0; j < column_size; j++)
            {
                //2.找到该行的其它非零元素所在的列数,这些数字就是可以与该行进行对换的行号
                if(m[i][j])
                {
                    //3.在可对换行中找到能够让对角线元素变为非0的行,最后进行对换
                    if(m[j][i])
                    {
                        RowSwapping(m, i, j);
                        break;
                    }
                }
            }
        }
    }
}

//将矩阵化简成行化简矩阵
void ClearCol(matrix& m)
{
    int row_size = m.size();
    int column_size = m[0].size();

    for(int i = 0; i < row_size; i++)
    {
        // 1.从第一列开始,确定对角线元素是否为1,如果不是,那么就使用倍乘变化将其变为1
        if(m[i][i] != 1)
        {
            RowScaling(m, i, 1.0/m[i][i]);
        }
        // 2.将该列其它行的元素变为0
        for(int j = 0; j < row_size; j++)
        {
            if(m[j][i] && j != i)
            {
                RowAddition(m, i, -m[j][i], j);
            }
        }
    }
}

  现在我们得知,这两个函数其实就是把一个矩阵先变化为行阶梯型矩阵,然后再变化为行化简矩阵,以此求出线性方程组的解。在本小节中,我们希望根据学到的线性代数知识实现四个新的接口,分别是:

接口
1.判断矩阵中的一行是否是非零行
2.返回非零行的先导元素
3.判断一个矩阵是否是行阶梯型矩阵
4.判断一个矩阵是否是行化简矩阵

先来实现第一个,这个非常简单,接口设计如下:

bool IsZeroRow(matrix& m, size_t row)

该函数接收一个矩阵,判断指定行是否是非零行,如果是,那么返回真,否则返回假,函数实现如下:

bool IsZeroRow(matrix& m, size_t row)
{
    if(row > m.size())
    {
        exit(1);
    }

    //计算列数
    int column_size = m[0].size();
    
    //遍历指定行中的所有元素
    for(int col = 0; col < column_size; col++)
    {
        if(m[row][col])
        {
            return false;
        }
    }
    return true;
}

同样先来测试一下:

void TestIsZeroRow()
{
    matrix m = {{0, 0, 0, 0, 0}, {0, 0, 0, 0, 1}};
    if(IsZeroRow(m, 0))
    {
        std::cout << "第0行的元素全为0" << std::endl;
    }
    else
    {
        std::cout << "第0行是非零行" << std::endl;
    }

    if(IsZeroRow(m, 1))
    {
        std::cout << "第1行的元素全为0" << std::endl;
    }
    else
    {
        std::cout << "第1行是非零行" << std::endl;
    }
}

运行结果
没有问题,下面来实现第二个接口,也就是返回非零行的先导元素,这个也非常简单,接口设计如下:

double RetLeadingElement(matrix& m, size_t row)

该函数返回指定行的先导元素,函数实现如下:

double RetLeadingElement(matrix& m, size_t row)
{
    //判断第row是否是非0行,如果不是,那么程序退出
    if(row > m.size() || IsZeroRow(m, row))
    {
        exit(1);
    }

    int column_size = m[0].size();

    //遍历指定行中的每一个元素,返回第一个非0元素
    for(int col = 0; col < column_size; col++)
    {
        if(m[row][col])
        {
            return m[row][col];
        }
    }
}

测试用例如下:

void TestRetLeadingElement1()
{
    matrix m = {{0, 0, 0, 0, 0}, {0, 0, 0, 0, 1},{0, 3.9, 0, 1, 0}};
    RetLeadingElement(m, 0);
}

void TestRetLeadingElement2()
{
    matrix m = {{0, 0, 0, 0, 0}, {0, 0, 0, 0, 1},{0, 3.9, 0, 1, 0}};

    double a = RetLeadingElement(m, 1);
    double b = RetLeadingElement(m, 2);
    std::cout << "a: " << a << " b: " << b << std::endl;
}

测试1
测试2
好了,下面来实现有点难度的第三个接口,也就是判断一个矩阵是否是行阶梯型矩阵,我们得严格的把判断行阶梯矩阵的三条性质使用程序语言表示出来,也就是这三条:

行阶梯矩阵性质
1.所有非零行都在零行的上方
2.先导元素所在列下方的元素必须全部为零
3.每一行的先导元素都必须位于上一行先导元素的后面

这就是我们的算法基础了,我们要做的就是把上方的数学性质翻译成程序语言并实现,下面正式开始实现函数,函数的接口设计如下:

bool IsREF(matrix& m)

  该函数接收一个矩阵,判断矩阵是否是行阶梯型矩阵,如果是的话就返回真,否则返回假。至于函数具体的实现,我们得思考一下了,从一个具体的实例入手:
[ 0 1 2 8 6 0 0 8 1 5 0 0 0 0 0 0 0 0 7 8 ] \left[ \begin{array}{ccccc} 0&1&2&8&6\\ 0&0&8&1&5\\ 0&0&0&0&0\\ 0&0&0&7&8 \end{array} \right] 00001000280081076508
  显然上方的矩阵不是行阶梯型矩阵,但是我们得让计算机知道才行,那么肯定就是要遍历矩阵的。首先遍历第一行,找到先导元素所在的列,然后遍历该列前方的所有列和该列先导元素下方的所有元素,如果存在非零元素,那么说明不是行阶梯型矩阵,返回假,如果不存在非零元素,那么继续遍历第二行,找到第二行的先导元素所在列,然后遍历该列前方所有列和先导元素所在列下方的元素,不过遍历列时要从第二行开始,对于后方的所有行,采用相同的遍历策略,但是在遍历途中,如果存在某一行的元素全部为零,那么在该行下方所有行的遍历方式就要改为直接遍历,如果存在非零元素就返回假,如果整个流程下来都没有返回假,那么说明矩阵是行阶梯型矩阵,因此返回真,总结出具体的执行流程就是:

Version1流程
1.遍历每一行并找到先导元素所在列
2.遍历先导元素所在列前方的所有列和先导元素所在列下方的元素,在遍历前方列时得从先导元素所在行开始,如果遍历到了非零元素,那么返回假
3.如果遍历到了全零元素行,那么记录行号并退出遍历,采用新的遍历方式遍历后面的行

  下面来考虑一个优化,首先如果遍历到了后面的行,那么说明前面的行必然符合规则,因此从前方行的先导元素所在列开始往前的列(包括先导元素本身所在列)对于后面的行来说都是不需要遍历的,因为遍历了也绝对是符合要求的全零,考虑到了这一点,重新调整我们的算法,具体流程如下:

Version2流程
1.从第一行开始遍历,但是只有第一行从第一个元素开始遍历,后面的所有行都从前一行先导元素后方的列开始遍历
2.遍历先导元素所在列下方的元素和与上一行先导元素之间列的元素,遍历时从先导元素所在行开始,如果遍历到了非零元素,那么返回假
3.如果遍历到了全零元素行,那么记录行号并退出遍历,在新的遍历中,从上一行先导元素所在列往后遍历所有下方行的元素,如果存在非零元素,那么返回假

  复杂了不少,但是时间复杂度同样也降低了不少,因为在Version2版本的算法中,我们把重复性的工作都给去除了,函数的实现如下:

//从start开始,查找矩阵中第row行的先导元素所在的列数并返回,如果没有找到先导元素,那么就返回列数
ssize_t FindFirstElemCol(matrix& m, size_t row, size_t start)
{
    if(row > m.size())
    {
        return -1;
    }
    int col_size = m[0].size();

    for(int col = start; col < col_size; col++)
    {
        if(m[row][col])
        {
            return col;
        }
    }
    return -1;
}

bool IsREF(matrix& m)
{
    int row_size = m.size();
    int col_size = m[0].size();
    int start = 0;

    //从第一行开始遍历,并且第一行的先导元素从第零列开始查找
    for(int row = 0; row < row_size; row++)
    {
        //查找先导元素所在列
        ssize_t first_col = FindFirstElemCol(m, row, start);

        //修改start,下一次从第first_col+1列开始查找,并且限定本次遍历的左列和右列[left_col, right_col)
        int left_col = start;
        int right_col = first_col + 1;
        start = first_col+1;

        //如果查找到了先导元素,那么遍历从先导元素所在行往下的[left_col, right_col)列,如果遍历到了非零元素,那么返回假
        if(first_col != -1)
        {
            for(int i = row+1; i < row_size; i++)
            {
                for(int j = left_col; j < right_col; j++)
                {
                    if(m[i][j])
                    {
                        return false;
                    }
                }
            }
        }
        //没有查找到非零元素,说明出现零行,使用新的方式继续遍历
        else
        {
            //遍历行从row+1开始,列从left_col开始到到col_size结束的所有元素
            for(int i = row+1; i < row_size; i++)
            {
                for(int j = left_col; j < col_size; j++)
                {
                    if(m[i][j])
                    {
                        return false;
                    }
                }
            }
        }
    }
    return true;
}

便与理解的带注释完整版函数如上,但是显然if和else是可以压缩掉的,简化版的函数如下:

ssize_t FindFirstElemCol(matrix &m, size_t row, size_t start)
{
    if (row > m.size())
    {
        return -1;
    }
    int col_size = m[0].size();

    for (int col = start; col < col_size; col++)
    {
        if (m[row][col])
        {
            return col;
        }
    }
    return -1;
}

bool IsREF(matrix &m)
{
    int row_size = m.size();
    int col_size = m[0].size();
    int start = 0;

    for (int row = 0; row < row_size; row++)
    {
        ssize_t first_col = FindFirstElemCol(m, row, start);
        
        int left_col = start;
        int right_col = first_col + 1;
        start = first_col + 1;

        if(first_col == -1) right_col = col_size;
        for (int i = row + 1; i < row_size; i++)
        {
            for (int j = left_col; j < right_col; j++)
            {
                if (m[i][j])
                {
                    return false;
                }
            }
        }
        if(first_col == -1) break;
    }
    return true;
}

算法比较复杂,得多做几组测试才行,最好包含所有情况,测试用例如下,使用简化版本进行测试:
( 1 ) [ 1 9 3 0 1 0 0 0 1 ] ( 2 ) [ 1 8 0 10 0 3 1 2 0 3 0 5 ] ( 3 ) [ 1 0 4 0 1 2 0 0 0 0 0 1 ] ( 4 ) [ 0 0 4 0 0 0 0 0 0 ] ( 5 ) [ 0 0 0 0 ] \begin{array}{c c c c} (1) \begin{bmatrix} 1 & 9 & 3 \\ 0 & 1 & 0 \\ 0 & 0 & 1\\ \end{bmatrix} & & (2) \begin{bmatrix} 1 & 8 & 0 & 10\\ 0 & 3 & 1 & 2\\ 0 & 3 & 0 & 5 \end{bmatrix} & & (3) \begin{bmatrix} 1 & 0 & 4 \\ 0 & 1 & 2 \\ 0 & 0 & 0\\ 0& 0 & 1 \end{bmatrix} & (4) \begin{bmatrix} 0 & 0 & 4 \\ 0 & 0 & 0 \\ 0 & 0 & 0\\ \end{bmatrix} & (5) \begin{bmatrix} 0 & 0 \\ 0 & 0 \\ \end{bmatrix} \end{array} (1) 100910301 (2) 1008330101025 (3) 100001004201 (4) 000000400 (5)[0000]
先用人眼观察到 ( 1 ) , ( 4 ) , ( 5 ) (1),(4),(5) (1)(4)(5)是行阶梯型矩阵, ( 2 ) , ( 3 ) (2),(3) (2)(3)不是行阶梯型矩阵,然后使用代码测试,测试代码如下:

void TestIsREF()
{
    matrix m1 = {{1, 9, 3}, {0, 1, 0}, {0, 0, 1}};
    if (IsREF(m1)) std::cout << "m1是行阶梯型矩阵" << std::endl;
    else  std::cout << "m1不是行阶梯型矩阵" << std::endl;

    matrix m2 = {{1, 8, 0, 10}, {0, 3, 1, 2}, {0, 3, 0, 5}};
    if (IsREF(m2)) std::cout << "m2是行阶梯型矩阵" << std::endl;
    else  std::cout << "m2不是行阶梯型矩阵" << std::endl;

    matrix m3 = {{1, 0, 4}, {0, 1, 2}, {0, 0, 0}, {0, 0, 1}};
    if (IsREF(m3)) std::cout << "m3是行阶梯型矩阵" << std::endl;
    else  std::cout << "m3不是行阶梯型矩阵" << std::endl;

    matrix m4 = {{0, 0, 4}, {0, 0, 0}, {0, 0, 0}};
    if (IsREF(m4)) std::cout << "m4是行阶梯型矩阵" << std::endl;
    else  std::cout << "m4不是行阶梯型矩阵" << std::endl;

    matrix m5 = {{0, 0}, {0, 0}};
    if (IsREF(m5)) std::cout << "m5是行阶梯型矩阵" << std::endl;
    else  std::cout << "m5不是行阶梯型矩阵" << std::endl;
}

见证奇迹吧:
在这里插入图片描述
  判断正确,当然没有经过专门设计的大型测试用例的测试,笔者也无法保证会不会出bug,但是上方的用例仅用于学习是完全足够的,下面开始设计最后一个函数,也就是判断一个矩阵是否是行化简矩阵,首先行化简矩阵是具有行阶梯型矩阵的性质的,因此我们应该得在判断行阶梯型矩阵函数的基础上进行修改,添加判断行化简矩阵的性质,也就是这两条性质:

性质
1.先导元素的值必须为1
2.在先导元素所在列中,除先导元素外的所有元素都必须为0

添加第一条性质非常简单,只需要一个逻辑判断就可以了,添加第二条性质就得动动脑子了,显然得对先导元素所在的列进行特殊处理,简单来说就是每一个先导元素所在的列都得从第一行开始往下遍历,如果遍历到除先导元素之外的非零元素,那么就返回假,代码实现如下:

bool IsRREF(matrix& m)
{
    int row_size = m.size();
    int col_size = m[0].size();
    int start = 0;

    //从第一行开始遍历,并且第一行的先导元素从第零列开始查找
    for(int row = 0; row < row_size; row++)
    {
        //查找先导元素所在列
        ssize_t first_col = FindFirstElemCol(m, row, start);

        //修改start,下一次从第first_col+1列开始查找,并且限定本次遍历的左列和右列[left_col, right_col)
        int left_col = start;
        int right_col = first_col + 1;
        start = first_col+1;

        //如果查找到了先导元素,那么遍历从先导元素所在行往下的[left_col, right_col)列,如果遍历到了非零元素,那么返回假
        if(first_col != -1)
        {
            //判断先导元素是否为1
            if(m[row][first_col] != 1) return false;

            for(int i = row+1; i < row_size; i++)
            {
                for(int j = left_col; j < right_col; j++)
                {
                    if(m[i][j])
                    {
                        return false;
                    }

                    //对先导元素所在列进行特殊处理
                    if(j == first_col)
                    {
                        for(int r = 0; r < row_size; r++)
                        {
                            //如果在该列中不是先导元素并且还不为0,那么就返回假
                            if(r != row && m[r][first_col] != 0)
                            {
                                return false;
                            }
                        }
                    }
                }
            }
        }
        //没有查找到非零元素,说明出现零行,使用新的方式继续遍历
        else
        {
            //遍历行从row+1开始,列从left_col开始到到col_size结束的所有元素
            for(int i = row+1; i < row_size; i++)
            {
                for(int j = left_col; j < col_size; j++)
                {
                    if(m[i][j])
                    {
                        return false;
                    }
                }
            }
        }
    }
    return true;
}

上方是带注释的完整版代码,下面是简化版的代码:

bool IsRREF(matrix& m)
{
    int row_size = m.size();
    int col_size = m[0].size();
    int start = 0;
    for (int row = 0; row < row_size; row++)
    {
        ssize_t first_col = FindFirstElemCol(m, row, start);

        int left_col = start;
        int right_col = first_col + 1;
        start = first_col + 1;

        if(first_col == -1) right_col = col_size;
        else
        {
            if(m[row][first_col] != 1) return false;
        }

        for (int i = row + 1; i < row_size; i++)
        {
            for (int j = left_col; j < right_col; j++)
            {
                if (m[i][j])
                {
                    return false;
                }

                if(first_col != -1 && j == first_col)
                {
                    for(int r = 0; r < row_size; r++)
                    {
                        if(r != row && m[r][first_col] != 0)
                        {
                            return false;
                        }
                    }
                }
            }
        }
        if(first_col == -1) break;
    }

    return true;
}
//注:在编程和线性代数中,我们常使用REF代指行阶梯型矩阵,用RREF代指行化简矩阵

同样的设计几组测试用例,使用简化版的函数进行测试,测试用例如下:
( 1 ) [ 1 9 3 0 1 0 0 0 1 ] ( 2 ) [ 1 0 0 10 0 1 1 2 0 0 0 0 ] ( 3 ) [ 1 0 4 0 1 2 0 0 0 0 0 1 ] ( 4 ) [ 0 0 1 0 0 0 0 0 0 ] ( 5 ) [ 0 0 0 1 ] ( 6 ) [ 0 ] ( 7 ) [ 1 ] \begin{array}{c c c c} (1) \begin{bmatrix} 1 & 9 & 3 \\ 0 & 1 & 0 \\ 0 & 0 & 1\\ \end{bmatrix} & & (2) \begin{bmatrix} 1 & 0 & 0 & 10\\ 0 & 1 & 1 & 2\\ 0 & 0 & 0 & 0 \end{bmatrix} & & (3) \begin{bmatrix} 1 & 0 & 4 \\ 0 & 1 & 2 \\ 0 & 0 & 0\\ 0& 0 & 1 \end{bmatrix} & (4) \begin{bmatrix} 0 & 0 & 1 \\ 0 & 0 & 0 \\ 0 & 0 & 0\\ \end{bmatrix} & (5) \begin{bmatrix} 0 & 0 \\ 0 & 1 \\ \end{bmatrix} & (6) \begin{bmatrix} 0\\ \end{bmatrix} & (7) \begin{bmatrix} 1\\ \end{bmatrix} \end{array} (1) 100910301 (2) 1000100101020 (3) 100001004201 (4) 000000100 (5)[0001](6)[0](7)[1]
同样的先自己判断一下,在上方示例中 ( 1 ) , ( 3 ) , ( 5 ) (1),(3),(5) (1)(3)(5)都不是行化简矩阵, ( 2 ) , ( 4 ) , ( 6 ) , ( 7 ) (2),(4),(6),(7) (2)(4)(6)(7)都是行化简矩阵,然后使用代码进行函数测试,测试代码如下:

void TestIsRREF()
{
    matrix m1 = {{1, 9, 3}, {0, 1, 0}, {0, 0, 1}};
    if (IsRREF(m1)) std::cout << "m1是行化简矩阵" << std::endl;
    else  std::cout << "m1不是行化简矩阵" << std::endl;

    matrix m2 = {{1, 0, 0, 10}, {0, 1, 1, 2}, {0, 0, 0, 0}};
    if (IsRREF(m2)) std::cout << "m2是行化简矩阵" << std::endl;
    else  std::cout << "m2不是行化简矩阵" << std::endl;

    matrix m3 = {{1, 0, 4}, {0, 1, 2}, {0, 0, 0}, {0, 0, 1}};
    if (IsRREF(m3)) std::cout << "m3是行化简矩阵" << std::endl;
    else  std::cout << "m3不是行化简矩阵" << std::endl;

    matrix m4 = {{0, 0, 1}, {0, 0, 0}, {0, 0, 0}};
    if (IsRREF(m4)) std::cout << "m4是行化简矩阵" << std::endl;
    else  std::cout << "m4不是行化简矩阵" << std::endl;

    matrix m5 = {{0, 0}, {0, 1}};
    if (IsRREF(m5)) std::cout << "m5是行化简矩阵" << std::endl;
    else  std::cout << "m5不是行化简矩阵" << std::endl;

    matrix m6 = {{0}};
    if (IsRREF(m6)) std::cout << "m6是行化简矩阵" << std::endl;
    else  std::cout << "m6不是行化简矩阵" << std::endl;

    matrix m7 = {{1}};
    if (IsRREF(m7)) std::cout << "m7是行化简矩阵" << std::endl;
    else  std::cout << "m7不是行化简矩阵" << std::endl;
}

结果如下:
在这里插入图片描述
  结果正确,这可太酷了,这才是真正的跨学科,如果没有线性代数的指导,那么就算把脑子想爆炸也绝对想不出上方的算法,如果没有对于C++语法的掌握和算法基础,那么同样也绝对无法将数学性质翻译成代码,跨学科不是用来应付考试,应付教育局的,而是实现你想实现的一切的基础,最后笔者将第一小节的代码与上方的代码封装成了一个类,如果想要进行测试的读者请自取(没有提供测试用例):
位于rowchange.hpp文件中:https://gitee.com/tan-chuwei/matrix
  文章的最后,让来判断一下我们实现的判断行阶梯矩阵函数与判断行化简矩阵函数的时空复杂度,首先是判断行阶梯矩阵函数,空间复杂度非常明显是 O ( 1 ) O(1) O(1)的,然后是时间复杂度,尽管嵌套了三层循环但是我们把所有重复性的遍历都去除了,因此最坏情况下也只会遍历整个矩阵中的所有元素一遍,因此时间复杂度是 O ( n 2 ) O(n^2) O(n2)的,对于判断行化简矩阵的函数,空间复杂度显然同样是O(1)的,由于我们对先导元素所在的列进行了特殊判断,在内部多嵌套了一层完整列的遍历,因此时间复杂度就是 O ( n 3 ) O(n^3) O(n3)的。
  在本小节中,我们看到了解的三种情况具体是什么样子的,学习了行阶梯型矩阵与行化简矩阵并实现了相应的算法使用C++进行判断,笔者同样感觉自己收获巨大,也希望这篇文章能够给读者一些收获。

更多推荐