宁可少些,但要好些,二分之一个证明等于 0
——德国数学家、物理学家 高斯
板块:线性代数-高斯消元法
难度:较难
前置知识点:矩阵的基本概念和三角阵、矩阵的初等变换
前置知识一览:
高斯消元法是线性代数中的重点知识,也是信息学奥赛中的考纲知识点,一般用于求解形如下所示的多元线性方程组。
{
a
11
x
1
+
a
12
x
2
+
⋯
+
a
1
n
x
n
=
b
1
a
21
x
1
+
a
22
x
2
+
⋯
+
a
3
n
x
n
=
b
2
⋮
a
n
1
x
1
+
a
n
2
x
2
+
⋯
+
a
n
n
x
n
=
b
n
;
{a11x1+a12x2+⋯+a1nxn=b1a21x1+a22x2+⋯+a3nxn=b2⋮an1x1+an2x2+⋯+annxn=bn;
⎩
⎨
⎧a11x1+a12x2+⋯+a1nxn=b1a21x1+a22x2+⋯+a3nxn=b2⋮an1x1+an2x2+⋯+annxn=bn;
概述: 高斯消元法的基本原理是把多元线性方程组的系数和等式右边记录为一个矩阵,并通过矩阵的变换使其成为上三角阵。最后再判定根据行数判断其有无解的情况,如果有解,还需要倒序回溯求解。
完美阶梯形:在执行完变换上三角阵的操作后,恰好为 n n n 行且 1 ∼ n 1\sim n 1∼n 的各方程未知数个数为 n , n − 1 , ⋯ , 1 n,n-1,\cdots,1 n,n−1,⋯,1
比如对于上文给出的样例,就是有唯一解的情况。
对于无解或有无穷组的情况,我们根据题目要求输出即可,但对于有唯一解的情况,我们必然是要求出唯一解的。
求解的过程就是顺次计算 x n , x n − 1 , x n − 2 , ⋯ , x 1 x_n,x_{n-1},x_{n-2},\cdots,x_1 xn,xn−1,xn−2,⋯,x1。因为我们已经将矩阵化为了上三角阵,所以显然,求解的过程是先计算 x n x_n xn 所在的一元一次方程,然后将 x n x_n xn 代入到上一行,就可以只计算 x n − 1 x_{n-1} xn−1 了,再将 x n , x n − 1 x_n,x_{n-1} xn,xn−1 代入 上一行,计算 x n − 2 ⋯ x_{n-2}\cdots xn−2⋯
最后,我们会将矩阵化为如下的形式,也就是除各行需要求解的未知数的系数外的其他系数均化为了 0:
(
1
0
0
1
0
1
0
−
2
0
0
1
3
)
(1001010−20013)
⎝
⎛1000100011−23⎠
⎞
以上是高斯消元法的简单说明,接下来将结合算法步骤详细讲述全过程
。
高斯消元法的算法步骤如下,在步骤 1 中,经过一轮操作后,会固定一个方程,最后变换成的上三角阵就是经过 n n n 轮变换后的矩阵:
依然以该矩阵为例:
(
1
2
−
1
−
6
2
1
−
3
−
9
−
1
−
1
2
7
)
(12−1−621−3−9−1−127)
⎝
⎛12−121−1−1−32−6−97⎠
⎞
从第 0 列开始枚举,定义
r
r
r 表示上一次固定的行编号加 1,初始
r
=
0
r=0
r=0。然后开始执行上述算法步骤。
步骤 1 的(1)找到绝对值最大的一行,也就是第二行,将它换至第 0 行,也就是:
(
2
1
−
3
−
9
1
2
−
1
−
6
−
1
−
1
2
7
)
(21−3−912−1−6−1−127)
⎝
⎛21−112−1−3−12−9−67⎠
⎞
然后,将该行的第 1 个数变成 1,也就是让每一个数都除以 2,变成:
(
1
0.5
−
1.5
−
4.5
1
2
−
1
−
6
−
1
−
1
2
7
)
(10.5−1.5−4.512−1−6−1−127)
⎝
⎛11−10.52−1−1.5−12−4.5−67⎠
⎞
最后,将下面每一行的第
c
c
c 列化为 0,那么就可以运用操作③,将第二行减去第一行,变成:
(
1
0.5
−
1.5
−
4.5
0
1.5
0.5
−
1.5
0
−
0.5
0.5
2.5
)
(10.5−1.5−4.501.50.5−1.50−0.50.52.5)
⎝
⎛1000.51.5−0.5−1.50.50.5−4.5−1.52.5⎠
⎞
然后再枚举第
2
∼
n
−
1
2~\sim n-1
2 ∼n−1 列,反复迭代,即可得到上文中给出的最终上三角阵:
(
1
0.5
−
1.5
−
4.5
0
1
1
3
−
1
0
0
2
3
3
)
(10.5−1.5−4.50113−100233)
⎝
⎛1000.510−1.53132−4.5−13⎠
⎞
这一上三角阵恰好有
n
n
n 行且是一个完美阶梯形,因此可以判断是具有唯一解的,按照讲解原理时的步骤计算,先将
x
n
x_n
xn 计算出来,将其系数化为 1,得:
(
1
0.5
−
1.5
−
4.5
0
1
1
3
−
1
0
0
1
3
)
(10.5−1.5−4.50113−10013)
⎝
⎛1000.510−1.5311−4.5−13⎠
⎞
得到
x
3
=
3
x_3=3
x3=3,然后再向上回溯,第 2 行减去第 3 行乘
1
3
\frac{1}{3}
31,将第 2 行的
x
3
x_3
x3 的系数化为 0(消元),得到:
(
1
0.5
−
1.5
−
4.5
0
1
0
−
2
0
0
1
3
)
(10.5−1.5−4.5010−20013)
⎝
⎛1000.510−1.501−4.5−23⎠
⎞
不难发现,第 2 个方程也解出来了,得
x
2
=
−
2
x_2=-2
x2=−2。再考虑第一行,先消掉 0.5,将第一行加上第二行乘0.5;再消掉 -1.5,将第一行加上第三行乘 -1.5(因为
a
23
a_{23}
a23 为0,无法消元),得:
(
1
0
0
1
0
1
0
−
2
0
0
1
3
)
(1001010−20013)
⎝
⎛1000100011−23⎠
⎞
于是,我们得到了该方程组的唯一解为
{
x
1
=
1
x
2
=
−
2
x
3
=
3
{x1=1x2=−2x3=3
⎩
⎨
⎧x1=1x2=−2x3=3.
特别注意,在实际算法中,由于可能出现因为 C++ 的精度问题导致的变量存储不准确,比如 0 存为0.00000…01 这种情况,因此我们判定为 0 的情况,就是判定某个数是否小于一个很小的数,比如 e p s = 1 e − 8 eps=1e-8 eps=1e−8。
有注释版:
#include
#include
#include
using namespace std;
const int N = 110;
const double eps = 1e-9;
int n;
double a[N][N];
int gauss()
{
int c, r;
for (c = 0, r = 0; c < n; c++)
{
int t = r; //上一次固定的行数+1
for (int i = r; i < n; i++)
{
//找到第c行绝对值最大的一列
if (fabs(a[i][c]) > fabs(a[t][c]))
t = i;
}
//如果该行的第c列为0就无意义了,后面会出现除以0的情况
if (fabs(a[t][c]) < eps) continue;
for (int i = c; i <= n; i++) swap(a[t][i], a[r][i]); //换行
for (int i = n; i >= c; i--) a[r][i] /= a[r][c]; //将第一个数变成1
//将下面的每一行的第c列都变成0
for (int i = r + 1; i < n; i++)
{
if (fabs(a[i][c]) > eps)
{
for (int j = n; j >= c; j--)
{
a[i][j] -= a[r][j] * a[i][c];
}
}
}
r++; //加1,下一轮操作时就可以直接用
}
//非完美矩形
if (r < n)
{
for (int i = r; i < n; i++)
{
//右边出现了非0的情况
if (fabs(a[i][n]) > eps)
return 2; //无解
}
return 1; //无穷组解
}
//有解就要求解,将除等式右边外的矩形转化为一个对角阵
for (int i = n; i >= 0; i--)
{
for (int j = i + 1; j <= n; j++)
{
a[i][n] -= a[i][j] * a[j][n];
}
}
return 0;
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n + 1; j++)
cin >> a[i][j];
int ans = gauss();
if (ans == 0)
{
for (int i = 0; i < n; i++)
{
//可能出现-0.0的情况
if (fabs(a[i][n]) < eps) a[i][n] = 0;
printf("%.2lf\n", a[i][n]);
}
}
else if (ans == 1) printf("Infinite group solutions"); //有无穷组解
else printf("No solution"); //无解
return 0;
}
无注释版:
#include
#include
#include
using namespace std;
const int N = 110;
const double eps = 1e-9;
int n;
double a[N][N];
int gauss()
{
int c, r;
for (c = 0, r = 0; c < n; c++)
{
int t = r;
for (int i = r; i < n; i++)
{
if (fabs(a[i][c]) > fabs(a[t][c]))
t = i;
}
if (fabs(a[t][c]) < eps) continue;
for (int i = c; i <= n; i++) swap(a[t][i], a[r][i]);
for (int i = n; i >= c; i--) a[r][i] /= a[r][c];
for (int i = r + 1; i < n; i++)
{
if (fabs(a[i][c]) > eps)
{
for (int j = n; j >= c; j--)
{
a[i][j] -= a[r][j] * a[i][c];
}
}
}
r++;
}
if (r < n)
{
for (int i = r; i < n; i++)
{
if (fabs(a[i][n]) > eps)
return 2;
}
return 1;
}
for (int i = n; i >= 0; i--)
{
for (int j = i + 1; j <= n; j++)
{
a[i][n] -= a[i][j] * a[j][n];
}
}
return 0;
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n + 1; j++)
cin >> a[i][j];
int ans = gauss();
if (ans == 0)
{
for (int i = 0; i < n; i++)
{
if (fabs(a[i][n]) < eps) a[i][n] = 0;
printf("%.2lf\n", a[i][n]);
}
}
else if (ans == 1) printf("Infinite group solutions");
else printf("No solution");
return 0;
}
异或线性方程组中,未知数的系数均为 0 或 1,做的运算不是四则运算而是异或运算,因此,整体框架不变,只是有三处变动:
#include
#include
using namespace std;
const int N = 110;
int n;
int a[N][N];
int gauss()
{
int c, r;
for (c = 0, r = 0; c < n; c++)
{
int t = r;
for (int i = r; i < n; i++)
{
if (a[i][c])
{
t = i;
break;
}
}
if (!a[t][c]) continue;
for (int i = c; i <= n; i++) swap(a[t][i], a[r][i]);
for (int i = r + 1; i < n; i++)
{
if (a[i][c])
{
for (int j = n; j >= c; j--)
a[i][j] ^= a[r][j];
}
}
r++;
}
if (r < n)
{
for (int i = r; i < n; i++)
{
if (a[i][n])
return 2;
}
return 1;
}
for (int i = n; i >= 0; i--)
{
for (int j = i + 1; j <= n; j++)
{
a[i][n] ^= a[i][j] & a[j][n];
}
}
return 0;
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n + 1; j++)
cin >> a[i][j];
int ans = gauss();
if (ans == 0)
{
for (int i = 0; i < n; i++) printf("%d\n", a[i][n]);
}
else if (ans == 1) printf("Multiple sets of solutions");
else printf("No solution");
return 0;
}