实现求解线性方程(矩阵、高斯消去法)------c++程序设计原理与实践(进阶篇)
步骤:
其中A是一个n*n的系数方阵 向量x和b分别是未知数和常量向量:

这个系统可能有0个、1个或者无穷多个解,这取决于系数矩阵A和向量b。求解线性系统的方法有很多,这里使用一种经典的方法——高斯消去法(https://zh.wikipedia.org/wiki/高斯消去法)。首先,我们对A和b进行交换,使得A变为一个上三角矩阵。上三角矩阵就是对角线之下的所有元素均为0。即如下形式:

实现这个目标是很容易的。为了使a(i,j)变为0,我们先将它乘以一个常量,使它等于第j列上的另一个元素,比如说等于a(k,j)。然后,用第i个方程减去第k个方程,a(i,j)即变为0,矩阵第i行其他元素的值也相应发生改变。
如果这样一个变换最终使得对角线上所有元素都非0,方程组就有唯一解,此解可以通过”回代“求得。如果存在为0的元素,则意味着方程组有0个或者无穷多个解。
我们现在用c++来表示上述计算方法。首先,定义两个要使用的具体Matrix类型,以简化程序:
typedef Numeric_lib::Matrix<double, 2>Matrix2; // Matrix库下载地址 :http://www.stroustrup.com/Programming/Matrix/Matrix.h 整个库定义在名字空间 Numeric_lib 中
typedef Numeric_lib::Matrix<double, 1> Vector;
接下来我们将高斯消去法计算过程描述为程序:
Vector classic_gaussian_elimination(Matrix2 A,Vector b){
classical_elimination(A,b);
return back_substitution(A,b);
}
即,先为两个输入A和b创建拷贝(使用赋值函数),然后调用一个函数求解方程组,最后调用回代函数计算结果并将结果返回。关键之处在于,我们分解问题的方式和符号表示都完全来自于原始的数学描述。下面所要做的就是实现classic_elimination()和back_substitution()了,解决方案同意完全来自于数学教科书:
void classical_elimination(Matrix2&A,Vector& b){
const Index n=A.dim1();
//从第1列一直遍历到倒数第二列
//将对角线之下所以元素都填充0
for(Index j=0;j<n-1;++j){
const double pivot =A(j,j);
if(pivot==0)cerr<<"错误:其中有一对角线位为0"<<'\n';
//将第i行在对角线之下的元素都填充为0
for(Index i=j+1;i<n;++i){
const double mult =A(i,j)/pivot;
A[i].slice(j)=scale_and_add(A[j].slice(j),-mult,A[i].slice(j)); //A[i].slice(j)表示从A[i][j]到这一行的末尾。
b(i)-=mult*b(j); //对b做对应变化
}
}
}
“pivot”表示当前行位于对角线上的元素,它必须是非0。因为需要用它作为除数;如果它为0,我们将放弃计算,抛出一个异常:
Vector back_substitution(const Matrix2&A,const Vector&b){
const Index n=A.dim1();
Vector x(n);
for(Index i=n-1;i>=0;--i){
double s=b(i)-dot_product(A[i].slice(i+1),x.slice(i+1));
if(double m=A(i,i))
x(i)=s/m;
else
throw Back_subst_failure(i);
}
return x;
}
完整示例程序:
#include<iostream>
#include"Matrix.h" //Matrix库下载地址 :http://www.stroustrup.com/Programming/Matrix/Matrix.h
#include"MatrixIO.h" //MatrixIO库下载地址 :http://www.stroustrup.com/Programming/Matrix/MatrixIO.h 仅为一维二维提供非常简单的I/O功能
using namespace Numeric_lib; //整个库定义在名字空间 Numeric_lib 中
using namespace std;
typedef Numeric_lib::Matrix<double, 2>Matrix2;
typedef Numeric_lib::Matrix<double, 1> Vector; void classical_elimination(Matrix2& A, Vector& b) {
const Index n = A.dim1();
//从第1列一直遍历到倒数第二列
//将对角线之下所以元素都填充0
for (Index j = 0; j<n - 1; ++j) {
const double pivot = A(j, j);
if (pivot == 0)cerr<<"错误:其中有一对角线位为0"<<'\n'; //将第i行在对角线之下的元素都填充为0
for (Index i = j + 1; i<n; ++i) {
const double mult = A(i, j) / pivot;
A[i].slice(j) = scale_and_add(A[j].slice(j), -mult, A[i].slice(j));
b(i) -= mult*b(j); //对b做对应变化
}
}
}
Vector back_substitution(const Matrix2&A, const Vector&b) {
const Index n = A.dim1();
Vector x(n); for (Index i = n - 1; i >= 0; --i) {
double s = b(i) - dot_product(A[i].slice(i + 1), x.slice(i + 1)); if (double m = A(i, i))
x(i) = s / m;
else
cerr<<"错误:其中有一对角线位为0"<<'\n';
}
return x;
}
Vector classic_gaussian_elimination(Matrix2 A, Vector b) {
classical_elimination(A, b);
return back_substitution(A, b);
}
int main() {
double val2[3][3] = {2,1,-1,-3,-1,2,-2,1,2 };
double val1[3] = {8,-11,-3 };
Matrix2 A(val2);
Vector b(val1);
cout<<classic_gaussian_elimination(A, b); }
改进:
pivot为0的问题是可以避免的,我们可以对行进行排列,从而将0和较小的值从对角线上移开,这样就得到了一个更鲁棒的方案。“更鲁棒”是指对于舍入误差不敏感。但是,随着我们将0置于对角线之下,元素值也会发生改变。因此,我们必须反复进行重排序,以将较小的值从对角线上移开(即,不能一次重排矩阵后就直接使用经典算法):
void elim_with_partial_pivot(Matrix2& A, Vector& b) {
const Index n = A.dim1();
for (Index j = 0; j < n; ++j) {
Index pivot_row = j;
//查找一个合适的主元:
for (Index k = j + 1; k < n; ++k)
if (abs(A(k, j)) > abs(A(pivot_row, j))) pivot_row = k;
//如果我们找到了一个更好的主元,交换两行:
if (pivot_row != j) {
A.swap_rows(j, pivot_row);
std::swap(b(j), b(pivot_row));
}
//消去:
for (Index i = j + 1; i < n; ++i) {
const double pivot = A(j, j);
if (pivot == 0)error("can't solve:pivot==0");
const double mult = A(i, j) / pivot;
A[i].slice(j) = scale_and_add(A[j].slice(j), -mult, A[i].slice(j));
b(i) -= mult*b(j);
}
}
}
在这里我们使用了swap_rows()和scale_and_multipy(),这样程序更符合习惯,我们也不必显式编写循环代码了。
随机数测试:
void solve_random_system(Index n) {
Matrix2 A = random_matrix(n);
Vector b = random_vector(n);
cout << "A=" << A << '\n';
cout << "b=" << b << '\n';
try {
Vector x = classic_gaussian_elimination(A, b);
cout << "classic elim solution is x =" << x << '\n';
Vector v = A*x;
cout << "A*x=" << v << '\n';
}
catch (const exception& e) {
cerr << e.what() << '\n';
}
}
程序在三种情况下会进入catch语句:
- 代码中有bug。
- 输入内容使classic_elimination出现错误(elim_with_partial_pivot在很多情况下可以做得更好)。
- 舍入误差导致问题。
为了测试我们的程序,我们输出 A*x,其值应该与b相单。但考虑到存在舍入误差,若其值与b足够接近就认为结果正确,这也是为什么测试程序中没有采用下面语句来判断结果是否正确的原因:
if(A*b!=b)error ("substitution failed");
在计算机中,浮点数只是实数的近似,因此我们必须接受近似的计算结果。一般来说,应该避免使用==和!=来判断是否正确。
Matrix库中并没有定义矩阵与向量的乘法运算,因此我们为测试程序定义这个运算:
Vector operator*(const Matrix2&m, const Vector&u) {
const Index n = m.dim1();
Vector v(n);
for (Index i = 0; i < n; ++i) v(i) = dot_product(m[i], u);
return v;
}
random_matrix()和random_vector()是随机数的简单应用。Index是索引类型,它是用typedef定义的。
完整程序:
#include<iostream>
#include<random>
#include <time.h>
#include"Matrix.h" //Matrix库下载地址 :http://www.stroustrup.com/Programming/Matrix/Matrix.h
#include"MatrixIO.h"//MatrixIO库下载地址 :http://www.stroustrup.com/Programming/Matrix/MatrixIO.h
using namespace Numeric_lib;//整个库定义在名字空间 Numeric_lib 中
using namespace std;
typedef Numeric_lib::Matrix<double, 2>Matrix2;
typedef Numeric_lib::Matrix<double, 1> Vector; void classical_elimination(Matrix2& A, Vector& b) {
const Index n = A.dim1();
//从第1列一直遍历到倒数第二列
//将对角线之下所以元素都填充0
for (Index j = 0; j<n - 1; ++j) {
const double pivot = A(j, j);
if (pivot == 0)cerr<<"错误:其中有一对角线位为0"<<'\n'; //将第i行在对角线之下的元素都填充为0
for (Index i = j + 1; i<n; ++i) {
const double mult = A(i, j) / pivot;
A[i].slice(j) = scale_and_add(A[j].slice(j), -mult, A[i].slice(j));
b(i) -= mult*b(j); //对b做对应变化
}
}
}
Vector back_substitution(const Matrix2&A, const Vector&b) {
const Index n = A.dim1();
Vector x(n); for (Index i = n - 1; i >= 0; --i) {
double s = b(i) - dot_product(A[i].slice(i + 1), x.slice(i + 1)); if (double m = A(i, i))
x(i) = s / m;
else
;
}
return x;
} void elim_with_partial_pivot(Matrix2& A, Vector& b) {
const Index n = A.dim1(); for (Index j = 0; j < n; ++j) {
Index pivot_row = j; //查找一个合适的主元:
for (Index k = j + 1; k < n; ++k)
if (abs(A(k, j)) > abs(A(pivot_row, j))) pivot_row = k; //如果我们找到了一个更好的主元,交换两行:
if (pivot_row != j) {
A.swap_rows(j, pivot_row);
std::swap(b(j), b(pivot_row));
} //消去:
for (Index i = j + 1; i < n; ++i) {
const double pivot = A(j, j);
if (pivot == 0)error("can't solve:pivot==0");
const double mult = A(i, j) / pivot;
A[i].slice(j) = scale_and_add(A[j].slice(j), -mult, A[i].slice(j));
b(i) -= mult*b(j); }
}
}
Vector classic_gaussian_elimination(Matrix2 A, Vector b) {
elim_with_partial_pivot(A, b);
//classical_elimination(A, b);
return back_substitution(A, b);
}
Vector operator*(const Matrix2&m, const Vector&u) {
const Index n = m.dim1();
Vector v(n);
for (Index i = 0; i < n; ++i) v(i) = dot_product(m[i], u);
return v;
}
int max0 = 10;
Vector random_vector(Index n) {
Vector v(n);
default_random_engine ran{(unsigned int)(time(0)+2)};
uniform_int_distribution<> ureal{ 0,max0 };
for (Index i = 0; i < n; ++i)
{
v(i) = ureal(ran);
} return v;
}
Matrix2 random_matrix(Index n) {
Matrix2 v(n,n);
default_random_engine ran{ (unsigned int)time(0) };
uniform_int_distribution<> ureal{ 0,max0 };
for (Index i = 0; i < n; ++i) { for (Index j = 0; j < n; ++j) v[i][j] = ureal(ran);
} return v;
} void solve_random_system(Index n) {
Matrix2 A = random_matrix(n);
Vector b = random_vector(n);
cout << "A=" << A << '\n';
cout << "b=" << b << '\n';
try {
Vector x = classic_gaussian_elimination(A, b);
cout << "classic elim solution is x =" << x << '\n';
Vector v = A*x;
cout << "A*x=" << v << '\n';
}
catch (const exception& e) {
cerr << e.what() << '\n';
} }
int main() {
/*double val2[3][3] = {2,1,-1,-3,-1,2,-2,1,2 };
double val1[3] = {8,-11,-3 };
Matrix2 A(val2);
Vector b(val1);
cout<<classic_gaussian_elimination(A, b);
*/
solve_random_system(4);
}
c++程序设计原理与实践(进阶篇)
实现求解线性方程(矩阵、高斯消去法)------c++程序设计原理与实践(进阶篇)的更多相关文章
- (c++11)随机数------c++程序设计原理与实践(进阶篇)
随机数既是一个实用工具,也是一个数学问题,它高度复杂,这与它在现实世界中的重要性是相匹配的.在此我们只讨论随机数哦最基本的内容,这些内容可用于简单的测试和仿真.在<random>中,标准库 ...
- 函数形参为基类数组,实参为继承类数组,下存在的问题------c++程序设计原理与实践(进阶篇)
示例: #include<iostream> using namespace std; class A { public: int a; int b; A(int aa=1, int bb ...
- 函数返回值string与返回值bool区别------c++程序设计原理与实践(进阶篇)
为什么find_from_addr()和find_subject()如此不同?比如,find_from_addr()返回bool值,而find_subject()返回string.原因在于我们想说明: ...
- 数值限制------c++程序设计原理与实践(进阶篇)
每种c++的实现都在<limits>.<climits>.<limits.h>和<float.h>中指明了内置类型的属性,因此程序员可以利用这些属性来检 ...
- 有符号数和无符号数------c++程序设计原理与实践(进阶篇)
有符号数与无符号数的程序设计原则: 当需要表示数值时,使用有符号数(如 int). 当需要表示位集合时,使用无符号数(如unsigned int). 有符号数和无符号数混合运算有可能会带来灾难性的后果 ...
- bitest(位集合)------c++程序设计原理与实践(进阶篇)
标准库模板类bitset是在<bitset>中定义的,它用于描述和处理二进制位集合.每个bitset的大小是固定的,在创建时指定: bitset<4> flags; bitse ...
- 编码原则实例------c++程序设计原理与实践(进阶篇)
编码原则: 一般原则 预处理原则 命名和布局原则 类原则 函数和表达式原则 硬实时原则 关键系统原则 (硬实时原则.关键系统原则仅用于硬实时和关键系统程序设计) (严格原则都用一个大写字母R及其编号标 ...
- gets()scanf()有害------c++程序设计原理与实践(进阶篇)
最简单的读取字符串的方式是使用gets(),例如: char a[12]; gets(a); 但gets()和scanf()是有害的,曾经有大约1/4的成功黑客攻击是由于gets()和它的近亲scan ...
- 宏(使用注意事项、主要用途)------c++程序设计原理与实践(进阶篇)
使用宏的时候一定要小心:在c中没有真正有效的方法来避免使用宏,但宏带有严重的副作用,因为宏不遵守通常的c(或c++)作用域和类型规则——它只是一种文本替换. 宏的使用注意事项: 所以宏名全部大写. ...
随机推荐
- 严谨的程序案例Api
文档 功能 同步推荐关系 接口方法 syncRelation 参数描述 OriginalUsername 查询的用户用户名 RecommandUsername 推荐人用户名 返回值 status 1成 ...
- Oracle 利用执行计划来避免排序操作
在oracle中,利用index来避免排序 SQL) NOT NULL); SQL> CREATE INDEX IND_T_NOSORT_NAME ON T_NOSORT(NAME); SQL& ...
- 20181102_WCF简单双工
使用管理员权限打开VS2017 2. 创建一个空白解决方案: 3. 创建几个基于.net framework的类库项目和一个基于.net Framework 的控制台项目, 然后将类库项目的clas ...
- left join的多重串联与groupby
有三张表或组合查询,f1,f2,f3,其中,f1分别与f2,f3是一对多关系,f1一条记录可能对应f2或f3中0条或多条记录 要创建一个查询,以f1为基准,即f1中有多少条记录,结果也就返回对应数量的 ...
- fiddler代理hosts配置
1 需求背景 fidder开启后,C:\Windows\System32\drivers\etc\hosts配置失效问题:fiddler本身代理hosts配置表,修改后,可以省去在手机等代理使用者的系 ...
- ado connection string
Provider=SQLOLEDB.1;Password=123;Persist Security Info=True;User ID=sa;Initial Catalog=mydb;Data Sou ...
- 【279】◀▶ Python 运算符说明
参考:Python 运算符说明 目录: 一.算术运算符 二.比较(关系)运算符 三.赋值运算符 四.位运算符 五.逻辑运算符 六.成员运算符 七.身份运算符 八.运算符优先级 一.Python 算术运 ...
- 【转】教你如何实现linux和W…
原文地址:[转]教你如何实现linux和Windows之间的文件共享,samba的安装与配置作者:铅笔小蜡 本人在虚拟机下装fedora13,已经实现. 1. 首先检查os是否安装好了samba. [ ...
- SkyBox
[SkyBox] Skyboxes 本质是一个Material,这个Meterial的shader必须设置为ShaderFX/Skybox. SkyBox可以被绑定到摄像机或设置一个全局的SkyBox ...
- JavaWeb之JSP入门
JSP原理及执行过程 流程图分析 用户发起请求,用户通过浏览器访问jsp页面,浏览器将HTTP协议的请求部分发送到服务端. 服务端获取请求部分,分析请求,发现本次的请求的的是jsp页面,jsp引擎按照 ...