数值算法
约 948 个字 124 行代码 预计阅读时间 5 分钟
一元非线性方程的求解
二分法
缺陷: 1. 收敛较慢 2. 好的中间近似解可能被丢弃
不动点迭代
从几何上理解就是蛛网图,收敛条件是: 1. \(g\in [a,b],g(x)\in [a,b]\)对所有\(x\in [a,b]\)成立,那么g在\([a,b]\)有一个不动点 2. \(g'\)存在,且存在\(0<k<1\)使得\(|g'(x)|\leq k\)对于\(x\in [a,b]\)成立
Newton 迭代法
原理
初始时我们从给定的f(x)和一个近似解\(x_0\)开始。
假设我们目前的近似解是\(x_i\),我们画出f(x)在\(x_i\)处的切线l,将l与x轴的交点记为\(x_{i+1}\),那么这就是一个更优近似解。根据导数的几何意义,有如下关系:\(f'(x_i)=\frac{f(x_i)}{x_i-x_{i+1}}\),整理后得到如下递推关系\(x_{i+1}=x_i-\frac{f(x_i)}{f'(x)}\)。重复这个迭代过程,由于Newton迭代法的收敛是平方级别的,这意味着每次迭代后近似解精确数位会翻倍。
代码示例
#include <iostream>
#include <cmath>
#include <functional>
using namespace std;
// 函数:newt
// 参数:
// x:初始根的猜测值,返回时存储最终的根
// eps:精度要求
// f:待求解方程的原函数
// df:原函数的导函数
// 返回值:
// 返回迭代次数,如果失败则返回-1
// 使用C++的std::function代替函数指针
int newt(double* x, double eps, function<double(double)> f, function<double(double)> df)
{
int k, inter;
double y, dy, d, p, x0, x1;
inter = 500; // 最大迭代次数为500
k = 0;
x0 = *x;
y = f(x0);
dy = df(x0);
d = eps + 1.0;
while (d >= eps && k != inter)
{
if (fabs(dy) < 1e-15) // 小于这个临界值就可以认为是等于0了
{
cout << "dy == 0 !" << endl;
return -1;
}
x1 = x0 - y / dy;
y = f(x1);
dy = df(x1);
d = fabs(x1 - x0); // 新的近似根相比于旧的的变化幅度,是度量精度的主要依据
p = fabs(y); // 新的根对应的函数值到零点的距离,也就是残差,是度量精度的次要依据
if (p > d) // 避免在根变化很小但函数值还未足够接近零的情况下过早停止迭代
d = p;
x0 = x1;
k = k + 1;
}
*x = x0;
return k;
}
插值
概念
插值是一种通过已知的、离散的数据点推算一定范围内的新数据点的方法。插值法常用于函数拟合中。
Lagrange 插值
原理
由于要构造一个函数f(x)过所有已知点。首先设第\(i\)个点在\(x\)轴上的投影为\(P'_i(x_i,0)\)。考虑构造\(n\)个函数\(f_1(x),f_2(x),...f_n(x)\),使得对于第\(i\)个函数\(f_i(x)\),其图像过\(\begin{cases}P'_j(x_j,0),(j\neq i)\\P_i(x_i,y_i)\end{cases}\),则\(f(x)=\Sigma_{i=1}^nf_i(x)\)。对于每一个\(f_i(x)\),显然已经知道它\(n-1\)个零点的值,根据因式分解的原理,函数可以用一个乘积表示,设\(f_i(x)=a\cdot \Pi_{j\neq i}(x-x_j)\),将剩下那一个点代入可得\(a=\frac{y_i}{\Pi_{j\neq i}(x_i-x_j)}\),所以\(f_i(x)=y_i\cdot \Pi_{j\neq i}\frac{x-x_j}{x_i-x_j}\),求和得到Lagrange插值公式\(f(x)=\Sigma_{i=1}^ny_i\cdot \Pi_{j\neq i}\frac{x-x_j}{x_i-x_j}\)
代码实现
朴素的实现代码时间复杂度是\(O(N^2)\),但如果横坐标是连续整数,可以靠一定技巧做到\(O(N)\)时间复杂度
Newton 插值法
原理
Newton插值法是基于高阶差分来插值的方法,优点是支持\(O(N)\)插入新数据。令\(f(x)=\Sigma_{j=0}^na_jn_j(x)\),其中\(n_j(x)=\Pi_{i=0}^{j-1}(x-x_i)\)称为Newton基
递归定义k阶向前差商(forward divided differences):\([x_i,x_{i+1}\dots,x_{i+k}=\frac{[x_{i+1},x_{i+2},\dots,x_{i+k}]-[x_i,x_{i+1}\dots,x_{i+k-1}]}{x_{i+k}-x_i}]\),向前差商的记号也可以带上一个f
其实所谓的Newton插值法就是用差商去重写Lagrange插值公式,写成\(f(x)=\Sigma_{j=0}^n[y_0,\dots y_j]n_j(x)\),若样本是等距的(即\(x_i=x_0+ih,i=1,\dots n\)),上式可以简化为\(f(x)=\Sigma_{j=0}^n\binom{s}{j}j!h^j[y_0,\dots y_j]\)
代码实现
朴素的实现代码时间复杂度还是\(O(N^2)\)
Hermite 插值
三次样条插值
数值积分
概念
不借助牛顿莱布尼茨定理,高效准确地求出一个定积分的近似值
高斯消元
原理
高斯消元法是求解线性方程组的经典算法,还可以用于行列式的计算,矩阵求逆等。高斯消元法的原理很简单,线性代数一入门就学了,老老实实计算就行了
代码实现
下面这段代码其实是优化过的,相比朴素的实现增加了一个选择最大主元的过程以减小误差
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
// 使用浮点数类型
typedef double dtype;
void gaussianElimination(vector<vector<dtype>>& A, vector<dtype>& b) {
int n = A.size();
// 消元过程
for (int i = 0; i < n; i++) {
// 寻找第 i 列的主元
int maxRow = i;
for (int k = i + 1; k < n; k++) {
if (fabs(A[k][i]) > fabs(A[maxRow][i])) {
maxRow = k;
}
}
// 交换当前行和主元行
swap(A[i], A[maxRow]);
swap(b[i], b[maxRow]);
// 消去当前列以下的元素
for (int k = i + 1; k < n; k++) {
dtype factor = A[k][i] / A[i][i];
for (int j = i; j < n; j++) {
A[k][j] -= factor * A[i][j];
}
b[k] -= factor * b[i];
}
}
// 回代过程
vector<dtype> x(n);
for (int i = n - 1; i >= 0; i--) {
x[i] = b[i] / A[i][i];
for (int j = i - 1; j >= 0; j--) {
b[j] -= A[j][i] * x[i];
}
}
// 输出解向量
cout << "Solution: ";
for (int i = 0; i < n; i++) {
cout << x[i] << " ";
}
cout << endl;
}
int main() {
int n;
cout << "Enter the number of variables: ";
cin >> n;
vector<vector<dtype>> A(n, vector<dtype>(n));
vector<dtype> b(n);
cout << "Enter the coefficient matrix A:" << endl;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cin >> A[i][j];
}
}
cout << "Enter the constant vector b:" << endl;
for (int i = 0; i < n; i++) {
cin >> b[i];
}
gaussianElimination(A, b);
return 0;
}