[算法] 高斯消元及其应用


主要问题

对于一个线性的方程组求解。

假设这个方程有 \(n\) 个,则时间复杂度为 \(O(n^3)\)

有些题目的 \(dp\) 状态有后效性,但是对于线性的方程,可以用高斯消元进行计算。

解决方法

高斯消元法的思路是:通过消元运算,直到得到一个只关于 \(x_1\) 的式子,只关于 \(x_1,x_2\) 的式子,只关于 \(x_1,x_2,x_3\) 的式子,然后回代计算即可。

如下:最初有一个 \(n\) 元线性方程

\[\begin{cases} a_{1,1}x_1+a_{1,2}x_2+...+a_{1,n}x_n=b_1 \\ a_{2,1}x_1+a_{2,2}x_2+...+a_{2,n}x_n=b_2 \\ \vdots \\ a_{n,1}x_1+a_{n,2}x_2+...+a_{n,n}x_n=b_n \\ \end{cases} \]

将上述方程转换为

\[\begin{cases} a_{1,1}x_1+a_{1,2}x_2+...+a_{1,n}x_n=b_1 \\ a_{1,1}x_1+\frac{a_{2,2}a_{1,1}}{a_{2,1}}x_2+...+\frac{a_{2,n}a_{1,1}}{a_{2,1}}x_n=b_2 \\ \vdots \\ a_{1,1}x_1+\frac{a_{n,2}a_{1,1}}{a_{n,1}}x_2+...+\frac{a_{n,n}a_{1,1}}{a_{n,1}}x_n=b_n \\ \end{cases} \]

最后通过减法消去后 \(n-1\) 个方程的 \(x_1\)

\[\begin{cases} a_{1,1}x_1+a_{1,2}x_2+...+a_{1,n}x_n=b_1 \\ (\frac{a_{2,2}a_{1,1}}{a_{2,1}}-a_{1,1})x_2+...+(\frac{a_{2,n}a_{1,1}}{a_{2,1}}-a_{1,n})x_n=b_2-b_1 \\ \vdots \\ (\frac{a_{n,2}a_{1,1}}{a_{n,1}}-a_{1,1})x_2+...+(\frac{a_{n,n}a_{1,1}}{a_{n,1}}-a_{1,n})x_n=b_n-b_1 \\ \end{cases} \]

如此,对于剩下的 \(n-1\) 个方程又是一个新的线性方程求解问题,继续递归求解,最后回代。

若对于当前的子问题中,有一次计算中的 \(x_n\) 就已经消失了,那么原问题中的这一未知数没有唯一解。

若当前中存在一个方程的各项系数都为 \(0\) ,且该条方程的常数项不为 \(0\) ,则一定无解。

当然,代码具体实现的时候并不需要写递归,直接用循环模拟即可。

实现中还有一个问题,每次求解的 \(a_{1,1}\) 是否会对答案产生影响?

可以发现,若 \(a_{1,1}\) 太小,那么会导致精度问题,所以每次需要选取主元素:最大的 \(a_{i,1}\) 所有方程以它为基础来进行消元。

  • Code:
#include 
#include 
#include 
using namespace std;
#define eps 1e-7
void Read(int &n) {
	n = 0; bool f = 0; char c = getchar();
	while (c < '0' || c > '9') {
		if (c == '-') f = 1;
		c = getchar();
	}
	while (c >= '0' && c <= '9') {
		n = (n << 1) + (n << 3) + (c ^ 48);
		c = getchar();
	}
	if (f) n = -n;
}
const int MAXN = 1e2 + 5;
double a[MAXN][MAXN], ans[MAXN];
int n;
int main() {
	Read(n);
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n + 1; j++) scanf("%lf", &a[i][j]);
	for (int i = 1; i <= n; i++) {
		int r = i;
		for (int j = i + 1; j <= n; j++)
			if (fabs(a[r][i]) < fabs(a[j][i])) r = j;//选取主元素
		if (fabs(a[r][i]) < eps) return printf("No Solution"), 0;//不存在唯一解
		if (i != r) swap(a[i], a[r]);
		double tmp = a[i][i];//带入消元
		for (int j = i; j <= n + 1; j++) a[i][j] /= tmp;
		for (int j = i + 1; j <= n; j++) {
			double tmp = a[j][i];
			for (int k = i; k <= n + 1; k++) a[j][k] -= a[i][k] * tmp;
		}
	}
	ans[n] = a[n][n + 1];//回代求解
	for (int i = n - 1; i >= 1; i--) {
		ans[i] = a[i][n + 1];
		for (int j = i + 1; j <= n; j++) ans[i] -= a[i][j] * ans[j];
	}
	for (int i = 1; i <= n; i++) printf("%.2lf\n", ans[i]);
	return 0;
}

模板题目链接。

[JSOI2008]球形空间产生器

题目大意

\(n\) 维空间中,给出 \(n+1\) 个点,求出圆心的坐标。

规定:

  • 球心:到球面上任意一点距离都相等的点。
  • 距离:设两个 \(n\) 维空间上的点 \(A, B\) 的坐标为 \((a_1, a_2, \cdots , a_n), (b_1, b_2, \cdots , b_n)\),则AB的距离定义为:\(dist = \sqrt{ (a_1-b_1)^2 + (a_2-b_2)^2 + \cdots + (a_n-b_n)^2 }\)

思路

设坐标数组为 \(a\),由题意得:

\(\sum_{j=0}^{n} (a_{i,j}-x_j)^2=C\)

化简得:

\(\sum_{j=1}^{n}(a_{i,j}^2-a_{i+1,j}^2-2x_j(a_{i,j}-a_{i+1,j}))=0\)

将常数项与未知数分离:

\(\sum_{j=1}^{n}2(a_{i,j}-a_{i+1,j})x_j=\sum_{j=1}^{n}(a_{i,j}^2-a_{i+1,j}^2)\)

等价转化为了 \(n\) 个线性方程,求解即可。

Code

#include 
#include 
#include 
using namespace std;
#define eps 1e-7
const int MAXN = 1e2 + 5;
double a[MAXN][MAXN], b[MAXN][MAXN], ans[MAXN];
int n;
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n + 1; i++)
		for (int j = 1; j <= n; j++) scanf("%lf", &b[i][j]);
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			a[i][j] = 2 * (b[i][j] - b[i + 1][j]);
			a[i][n + 1] += b[i][j] * b[i][j] - b[i + 1][j] * b[i + 1][j];
		}
	}
	for (int i = 1; i <= n; i++) {
		int r = i;
		for (int j = i + 1; j <= n; j++)
			if (fabs(a[r][i]) < fabs(a[j][i])) r = j;
		if (i != r) for (int j = 1; j <= n + 1; j++) swap(a[i][j], a[r][j]);
		double tmp = a[i][i];
		for (int j = i; j <= n + 1; j++) a[i][j] /= tmp;
		for (int j = i + 1; j <= n; j++) {
			double tmp = a[j][i];
			for (int k = i; k <= n + 1; k++) a[j][k] -= a[i][k] * tmp;
		}
	}
	ans[n] = a[n][n + 1];
	for (int i = n - 1; i >= 1; i--) {
		ans[i] = a[i][n + 1];
		for (int j = i + 1; j <= n; j++) ans[i] -= a[i][j] * ans[j];
	}
	for (int i = 1; i <= n; i++) printf("%.3lf ", ans[i]);
	return 0;
}