《数值分析方法》想来是不少工科研究生的必修课程,最近发现不少工程问题的初始推导都来自数值分析的一些方法,全面梳理其中的算法和结论推导是想做很久的事情之一,虽然很耗时间,但是并不是什么很困难的事情(对比此前各种的Labs),毕竟教会一个engineer如何做researching,比教会一个reseacher如何做好engineering要来得简单(然而很多reseacher乐意去做engineering,但engineer有机会且沉下心做reseach的少之又少)。这本书更是有很多适合工程化的算法,因此本文会基于原教材的编排和定理结论,尤其注重结论的证明相关算法demo的C++实现,提供:

  • 数值分析的基本思想方法手段

  • 遵循数理的结论推导证明

  • 开箱即用的各种C++算法。

  • 适量的数值优化/最优化理论拓展;

第一章:数值分析与科学计算引论

误差定性与分析

误差分类

  • 模型误差:建立的数学模型实际问题之间出现的误差,称为模型误差;只有简化和抽象问题时合理才能得到较好的结果,这种误差不在数值分析的讨论中,同时要衡量这种误差有时候需要根据观测物理量,如温度、电压等,也会引入观测误差

  • 截断误差/方法误差:在数学模型不能求解精确解时,需要使用数值方法求解它的近似解,其和精确解之间的误差称为截断误差或者方法误差,例如可微的f(x)与泰勒多项式误差表示为:

  • 舍入误差:计算机字长不能完美表示浮点数,因此在数值计算时会产生新的误差,这就是舍入误差,例如带入Π的计算误差:

数值分析主要衡量截断误差舍入误差

舍入误差定性

目前对舍入误差的定量估计还没有有效的分析方法,为了确保数值计算正确性通常只进行定性分析;

算法的数值稳定性

计算 并且估计误差。

解:根据分部积分法: 移项得: 故有: 假设使用该递推公式,并且使用: 推到第八项,会发现,而: 可见递推公式是正确的,但是却得到错误的估计结果,这就是初始误差每步累计的误差造成的错误,其关系是: 故: 由递推可知: 由于阶乘的存在,随着n增大,误差会逐渐放大,因此这种数值计算方法是不稳定的。

我们把计算过程舍入误差不增长的算法称为数值稳定算法,否则为不稳定算法;对于上例,更好的估计方法是使用的均值估计,然后往前递推,这样误差反而越来越小(往后递推有n的倍增,往前递推则有n分1的递减)。

数值计算中的算法设计艺术

最快速的多项式计算算法——秦九韶算法(Hernor算法)

多项式计算中,计算这样形式的式子:

反复地计算,需要次乘法计算和n次的加法计算,总复杂度为

容易观察到,低次项的是能够被高次项复用的,这最早于十三世纪被南宋数学家秦九韶提出,在1819年西方提出了相同的Hernor算法(霍纳算法),表示为:

从该式子,我们能算出每次迭代的系数表示,为:

C++实现

从实现来看,实际上就是复用每次的计算结果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <vector>
#include <cmath>

using namespace std;

int Hernor(std::vector<int>& coeff, int value){ //计算coeff[0]x^{n-1} + coeff[1]x^{n-1} + ... + coeff[n-1]
int n = coeff.size() - 1; //最高阶数
int res = coeff[0];
for(int i = 1; i < coeff.size(); i++){
res = res*value + coeff[i];
}
cout << "res = " << res << endl;
return res;
}

int main(){

std::vector<int> vp{1,2,3,4,5,6};
int value = 5;

//普通算法
int normal = 0;
for(int i=0; i<vp.size(); i++){
normal += vp.at(i)*pow(value, vp.size()-i-1);
}
cout << " normal = " << normal << endl;

//秦九韶算法
Hernor(vp, value);

cout << "done" << endl;
return 0;
}

拓展

由于秦九韶算法使用了递推的形式,其与乘数求导的链式法则不谋而合,该算法还能进一步推导用于求解多项式在某个点上的一阶导数,表示为: 该式子的推导也比较巧妙,我们知道,任意多项式P(X)总能表示为Q(X)代表长除法R代表长除法产生的余数。所以使用长除以,商的系数恰好为,余数即为

对上式子两边同时求导得: 即: 这种形式还是符合多项式,一样可以展开为:

值得注意的是,比起函数值系数,这里的末项是而不是,因此c也只递推到,即: 可见我们求出函数值b,通过相同的秦九韶递推,能够得到某一点的一阶导数值,该结论能继续递推,继续求解某点的二阶、任意高阶等导数值。

看理论是很抽象的,以下是应用秦九韶进行函数值、导数值计算的例子:

计算在x = 2函数值和导数值:

解:由递推可知:

由:

迭代法与开方求值

计算机能够加减乘除四则运算的前提是其能进行二进制的加减和移位,但是如果计算一个数的开方,没有相关的二进制计算方式,因此使用的是迭代法,并且将开方转化成四则运算;

假定a>0, 求解等价于求解方程: 方程求根的相关迭代会在第七章具体介绍,此处构造简单迭代法:令,即:

此处忽略了,这是一个小量的高阶项,则: 故有: 此处,是一个基于初始值的校正量,不断重复这个过程: 且能够证明: 而且收敛速度很快,该方法迭代4-5次就能满足精度

注:证明过程

  1. 证明若, 单调且有界,即收敛:

    基本不等式得: 有下界,即, 单调递减,因此收敛;

  2. 收敛至:

    两侧取极限: 得:

  3. 的收敛速度: 故:

因此这是一种误差平方迭代,收敛速度很快。

数值松弛法

数值松弛法能够通过更少的迭代次数,逼近真值,其公式就是一个加权平均: 其中细网格迭代次数更高、更接近真值的估计值,粗网格则是迭代次数较少的粗值,被称为松弛因子是一个比细网格更逼近真值的值。

魏晋时期的刘徽在估计圆周率时就使用了类似的思想,估计出,等效于使用半径为10的割圆法到3072边形,而使用松弛法,只需要使用96和192边形,松弛系数为:

大大降低了计算量

关于松弛因子的确定,可以使用泰勒公式估计,首先给出一个半径为r,内接n边形的面积为:

根据泰勒展开:

使用右式表示,令,有:

故仅考虑二阶因子情况下,能够计算96边形和192边形的松弛因子:

估计: 得: 这个值是来自二阶的理论计算值,与刘徽根据递增关系得到的比较接近,都能加快割圆法的收敛速度。一个思想类似,但现代的理论是理查森外推法(Richardson Extrapolation),将在后续的章节介绍。

第二章:插值法

引言

很多实际问题使用y=f(x)表示某种内在规律的数量关系,其中相当一部分是通过实验或者观测得到的,虽然f(x)区间[a, b]存在,但可能不连续,或者只给出了函数表,或者是连续,但解析式计算复杂,因此希望使用一个简单函数P(x),使得成立,这个P(x)就是f(x)对应的插值函数,求解这个函数的过程就是插值法,对应的就是插值节点。

本章仅讨论多项式插值分段插值,即P(x)多项式或者分段多项式

多项式插值

根据原函数的n+1个点,能够建立一个P(x)多项式方程,求解这个多项式方程系数的过程就是多项式插值,y = f(x)n+1个点:

求解n+1个系数的矩阵表示为:

这样的矩阵被称为范德蒙德(Vandermonde)矩阵,其行列式计算为: 例如矩阵:

范德蒙德行列式计算证明

范德蒙德行列式计算证明也比较简单,转置矩阵的行列式等于原矩阵:

从倒数第二行开始,乘()加到下一行得到:

按第一个数余子式展开得:

继续以此类推,从倒数第二行开始,乘()加到下一行,有:

得证;

定理:满足范德蒙德系数矩阵的插值多项式P(x)是存在且唯一的。

由上述分析可知,选取n+1个点进行多项式插值,系数矩阵是一个范德蒙德矩阵,其行列式的值非零:

即系数矩阵必然满秩,求解、... 、均存在且唯一,故多项式存在且唯一

但直接求解n+1个点组成的多项式解是求解插值多项式最繁杂的方式,一般不会使用,后续两节将提出构造多项式更简单的方法;

拉格朗日插值

线性插值

给定区间和端点值,通过线性拟合函数易得:

是通过两个端点的拟合的直线,从两点式可看出,该拟合直线是两个线性函数的加权和,权值系数是两个端点值,两个线性函数分别为: 故: 称为线性插值基函数,基函数满足:

抛物线插值

对抛物线插值,给定端点,满足:

求解抛物线方程宜从基函数角度入手,其三个基函数均需要满足在一个端点值为1,在另外两个端点值为0,故每个基函数都有两个已知零点,故: 有:

故第一个基函数为:

同理可知第二、三个基函数, 为:

故已知三个端点值,其抛物线插值的方程表示为:

拉格朗日插值多项式

拉格朗日插值线性插值、抛物线插值推广到一般情形,给定n+1个端点(),可以求出其n次的插值多项式插值多项式满足:

其方法还是n次的插值基函数,对于n次项,这样的基函数有n+1个,均可以表示为:

即对于分子, 用减去不含的所有项,分母是减去不含的所有项。

故:

这就是拉格朗日插值多项式,值得注意的是,通常是n次的,但是也可能小于n次,例如在抛物线插值中,如果三点共线,那么实际上是一条直线,即一次多项式;

插值余项和误差估计

在区间[a,b]使用近似f(x),则其截断误差为,截断误差也称为插值多项式的余项,有定理

  • 在[a,b]上连续在(a,b)存在,对任何,插值余项满足:

    其中:

插值余项证明

余项证明如下:令K(x)是关于x的待定函数,有:

假设在[a,b]存在固定点x,使得:

注意,上式xK(x)看成常数。根据假设,在[a,b]n阶连续,n+1阶存在,后两项均为多项式,而且是n次多项式,其n+1阶导数为0;是n+1次多项式,其n+1阶导数就是最高阶数求导数,为(n+1)!;

因此有:

因为可微,存在n+2个零点,由罗尔定理可知,存在n+1个零点,一次类推,存在至少一个零点,有:

故:

得证。

注意,当且仅当f(x)的高阶导数存在时,该插值余项才能被使用,假设找出:

则得到截断误差限为:

拉格朗日插值C++实现

根据拉格朗日多项式:

实现方法比较简单,拟合一个值都需要遍历所有的参考点,其复杂度是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <opencv2/core/matx.hpp>
#include <vector>
#include <opencv2/opencv.hpp>

using namespace std;

//输入已知点(2个以上),需要插值的x坐标,输出插值x坐标对应的逼近值
float lagrange_interpolation(const std::vector<cv::Point2f>& input_points, float target_x){
float res = 0.0f;
for(int i=0; i<input_points.size(); i++){
float li = 1.0f;
for(int j=0; j<input_points.size(); j++){
if(i != j){
li *= (target_x - input_points[j].x )/ (input_points[i].x - input_points[j].x);
}
}
res += input_points[i].y*li;
}
return res;
}

int main(){

//模拟数据
std::vector<cv::Point2f> calibPoint{
{100,100.5},
{200,298.8},
{300,399.6},
{400,402.1},
{500,596.6},
{600,698.2},
{700,799.5},
{800,900.2},
};

//8个点插值至1024个点
std::vector<cv::Point2f> res_points;

for(int x=0; x < 1024; x++){
res_points.push_back({(float)x,lagrange_interpolation(calibPoint, x)});
}

//画图示意
cv::Mat drawPic(1024,1024,CV_8UC3); //底图
drawPic.setTo(255);

//结果点
for(const auto&pos : res_points){
circle(drawPic, cv::Point(pos.x, pos.y), 2, cv::Scalar(0,0,255), -1);
}

//参考点
for(const auto& pos : calibPoint){
circle(drawPic, cv::Point(pos.x, pos.y), 2, cv::Scalar(255,0,0), -1);
}

cv::imshow("drawPic",drawPic);

cout <<"done" << endl;
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}

效果: 拉格朗日插值

多点低阶插值余项为0

从上述证明可知:

时,也为0;

这意味着:如果要拟合的原本就是一个多项式,而且给定的点数多于该多项式的阶数,其n+1阶导数为0,余项也为0,这也再次印证了拉格朗日插值多项式的唯一性

勒贝格常数(Lebesgue常数)

勒贝格常数常被用于分析插值算法的数值稳定性,定义为:

对拉格朗日插值,由前述所知有:

勒贝格常数有这样的不等式 这说明插值函数的误差不仅取决于自身的可逼近性,而且微小的扰动可能被勒贝格常数放大,造成龙格(Runge)现象,对于等距节点,可以计算其勒贝格常数为:

可见是一种指数增长,其基函数在端点附近可能产生剧烈的震荡。

因为插值算法得到的多项式是唯一的,不同插值算法只是基函数不同、计算复杂度不同,使用拉格朗日来定义勒贝格常数只是因为其基函数相对简单,要优化勒贝格常数,需要从插值节点分布的方向进行优化。

切比雪夫节点(Chebyshev Points)

区间上有一条光滑的曲线,要逼近这条线,很直觉的方法是通过在曲线上均匀地取点,然后构成一个多项式来逼近它,但从勒贝格常数来看,等距取点往往导致区间的两端疯狂摆动,远远偏离原始曲线的龙格现象,甚至随着均匀取点数目的增加,龙格现象会更加剧烈,可见均匀取点是一种错误的直觉;为了驯服这头在区间边缘摆动的野兽,我们只得在区间两端取更多的插值点将其钉住,实现两侧密集、中段稀疏的点分布,这是通往正确的第一缕曙光。

几何运动学中为这种点集的产生提供了思路。在的区间上,一个点绕上半圆周以恒定角速度进行运动,其投影几产生的点集,恰好对应了这种两侧密集、中段稀疏的分布特性,其投影为,这些投影点集,称为切比雪夫(Chebyshev)节点

第一类切比雪夫多项式

第一类第二类切比雪夫多项式是切比雪夫多项式族最常用的两种,它们在逼近理论、数值分析和谱方法中起核心作用;

第一类切比雪夫多项式通过三角恒等式定义:

等价的,令,即:

由和差化积公式可知: 即:

而且可知初始条件:

第一类切比雪夫多项式其有相关数学性质

  • 有界性

  • 奇偶性:n为奇数时为奇函数,为偶数时为偶函数,即

  • 首项系数

    首项系数证明

    , 有:

    故第一类切比雪夫多项式的最高阶系数

  • 零点:为切比雪夫-高斯(CG)点,即:

  • 极值点:为切比雪夫-高斯-洛巴托点(CGL),即:

  • 极小极大性质:在所有的首一多项式在[-1,1]上的最大模最小

    注:这个性质很有趣,如果在所有多项式中,要找到最高阶系数是1的多项式(首一多项式),使得其在[-1,1]区间上波动最小,那么最优的选择就是第一类切比雪夫多项式,因为其在[-1,1]区间等幅震荡,幅度就是,其他任意首一多项式震荡幅度不可能小于该多项式,这也说明了切比雪夫插值点插值最小化误差的本质

第一类切比雪夫多项式还有一些复杂性质和边值问题、微分方程等研究相关,不在本文的讨论范围内:

  • 正交性:关于权函数正交:

  • 微分方程(施图姆-刘维尔问题)的解:

第二类切比雪夫多项式

第二类切比雪夫多项式的三角恒等式定义为: 等价的:

其递推公式为: 可见第二类切比雪夫多项式和第一类具有相同的递推公式,但初始条件不同:

第二类切比雪夫多项式有以下性质:

  • 首项系数:为,证明略;

  • 零点, k = 1, 2, ... , n

  • 正交性:关于权函数正交

  • 微分方程:

与第一类切比雪夫多项式存在关系:

第一个差分关系由三角关系容易证明,此处略;

第二项求导证明如下:令: 即: 证毕。

切比雪夫-高斯点(Chebyshev-Gauss Points, CG)和切比雪夫-高斯-洛巴托点(Chebyshev-Gauss-Lobatto Poitns, CGL)

如前文所述,两类切比雪夫点都来自三角函数,其中段稀疏、端点密集的插值点特性使得勒贝格常数更优,对CG点: 的基数倍,所以其不含-11两个端点,取的值都是内部点,广用于插值和积分等不重视边值条件的情况;

而CGL点: 其同时包含了-11两个端点,比较适用于谱方法、边值问题;

C++实现:等距与切比雪夫插值的拉格朗日插值对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <opencv2/core/cvdef.h>
#include <opencv2/core/matx.hpp>
#include <opencv2/core/types.hpp>
#include <opencv2/highgui.hpp>
#include <vector>
#include <opencv2/opencv.hpp>

using namespace std;

double test_func(double x){
return 1.0 / (1 + 25*x*x);
}

double lagrange_interpolation(const std::vector<cv::Point2d>& input_points, double target_x){
double res = 0.0;
for(int i=0; i<input_points.size(); i++){
double li = 1.0;
for(int j=0; j<input_points.size(); j++){
if(i != j){
li *= (target_x - input_points[j].x )/ (input_points[i].x - input_points[j].x);
}
}
res += input_points[i].y*li;
}
return res;
}

std::vector<double> getChebyshevsPoint(int num, double low, double high){
std::vector<double> res;
for(int i=0; i<num; i++){
double theta = (2*i+1)*CV_PI/(2*num + 2);
double xi = std::cos(theta);
double mapped_x = 0.5*((high-low)*xi + (low + high));
res.push_back(mapped_x);
}
return res;
}


int main(){
//200个参考点,表征原函数趋势
std::vector<cv::Point2d> vp_orig;
for(int i=0; i<=200; i++){ //[-1, 1], 200个等距取点
double xi = -1 + i/100.0;
vp_orig.push_back({xi, test_func(xi)});
}

//20个等距节点,作为初始插值参考
std::vector<cv::Point2d> vec_fixed_point;
for(int i=0; i<=20; i++){ //[-1, 1], 20个等距取点
double xi = -1 + i/10.0;
vec_fixed_point.push_back({xi, test_func(xi)});
}

//拉格朗日插值结果
std::vector<cv::Point2d> vp_fixed;
for(int i=0; i<=200; i++){
double xi = -1 + i/100.0;
vp_fixed.push_back({xi, lagrange_interpolation(vec_fixed_point, xi)});
}

//获取切比雪夫插值点
std::vector<double> vec_chebyshev_x = getChebyshevsPoint(20, -1, 1);
std::vector<cv::Point2d> vec_chebyshev_point;
for(const auto& pos : vec_chebyshev_x){
vec_chebyshev_point.push_back({pos, test_func(pos)});
}

//切比雪夫点插值效果
std::vector<cv::Point2d> vp_chebyshev;
for(int i=0; i<=200; i++){
double xi = -1 + i/100.0;
vp_chebyshev.push_back({xi, lagrange_interpolation(vec_chebyshev_point, xi)});
}

//画图
cv::Mat drawPic(1024,1024,CV_8UC3); //底图
drawPic.setTo(255);

const int ratio = 1000; //画图点,等辐放大

//绿色线--表征原函数趋势
for(const auto&pos : vp_orig){
circle(drawPic, cv::Point(ratio*pos.x, ratio*pos.y), 2, cv::Scalar(0,255,0), -1);
}

//红色线--等距的拉格朗日插值
for(const auto&pos : vp_fixed){
circle(drawPic, cv::Point(ratio*pos.x, ratio*pos.y), 2, cv::Scalar(0,0,255), -1);
}

//蓝色线--切比雪夫点的拉格朗日插值
for(const auto&pos : vp_chebyshev){
circle(drawPic, cv::Point(ratio*pos.x, ratio*pos.y), 2, cv::Scalar(255,0,0), -1);
}

// 添加标签
cv::putText(drawPic, "Green: Original Function", cv::Point(20, 30),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0,255,0), 2);
cv::putText(drawPic, "Red: Equidistant Lagrange", cv::Point(20, 60),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0,0,255), 2);
cv::putText(drawPic, "Blue: Chebyshev Lagrange", cv::Point(20, 90),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255,0,0), 2);

cv::imshow("drawPic", drawPic);


/**********************************************勒贝格常数估算************************************/

//计算基函数lk在target_x处的值, k = 0,1...,n
auto calculate_lk = [](const std::vector<cv::Point2d>& input_points, int k, double target_x){
double lk = 1.0;
int n = input_points.size()-1; //插值的阶数,三点二阶、四点三阶......
for(int j=0; j<=n; j++){
if(j != k){
lk *= (target_x - input_points[j].x) / (input_points[k].x - input_points[j].x);
}
}
return lk;
};

//估计勒贝格常数,按0.01的精度取最大值作勒贝格常数值
auto getLebesgue = [calculate_lk](const std::vector<cv::Point2d>& input_points, double low, double high){
double sum, maxsum;
maxsum = sum = 0.0;
int n = input_points.size()-1; //插值的阶数,三点二阶、四点三阶......
for(double x=low; x<=high; x += 0.01){
sum = 0.0;
for(int k=0; k<=n; k++){
sum += std::abs(calculate_lk(input_points, k, x));
}
maxsum = sum > maxsum ? sum : maxsum;
}
return maxsum;
};

//等距节点勒贝格常数:
auto fixed_leb = getLebesgue(vec_fixed_point, -1, 1);
cout << "fixed lebesgue = " << fixed_leb << endl;

//切比雪夫点勒贝格常数:
auto chebyshev_leb = getLebesgue(vec_chebyshev_point, -1, 1);
cout << "chebyshev lebesgue = " << chebyshev_leb << endl;

cout <<"done" << endl;
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}

输出:

1
2
fixed lebesgue = 10772.4
chebyshev lebesgue = 2.90082
等距取点的勒贝格常数指数增长,存在不稳定的摆动现象,切比雪夫点的勒贝格常数是对数增长,不稳定现象不明显,图的对比结果如下: 等距节点、切比雪夫节点插值对比

上述代码没有虽然使用标准的CG点的标准形式,实际上是等效的,也可以进一步对比CG、CGL点的插值效果,但对于该函数插值而言,效果是相近的,可经过改代码进行对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <opencv2/core/cvdef.h>
#include <opencv2/core/matx.hpp>
#include <opencv2/core/types.hpp>
#include <opencv2/highgui.hpp>
#include <vector>
#include <opencv2/opencv.hpp>

using namespace std;

double test_func(double x){
return 1.0 / (1 + 25*x*x);
}

double lagrange_interpolation(const std::vector<cv::Point2d>& input_points, double target_x){
double res = 0.0;
for(int i=0; i<input_points.size(); i++){
double li = 1.0;
for(int j=0; j<input_points.size(); j++){
if(i != j){
li *= (target_x - input_points[j].x )/ (input_points[i].x - input_points[j].x);
}
}
res += input_points[i].y*li;
}
return res;
}

std::vector<double> getChebyshevsPoint(int num, double low, double high, int mode){
std::vector<double> res;
double theta = 0.0;

switch (mode) {
case 0: { // 一般切比雪夫点(Chebyshev points of the first kind)
for(int i = 0; i < num; i++){
theta = (2*i+1)*CV_PI/(2*num);
double xi = std::cos(theta);
double mapped_x = 0.5*((high-low)*xi + (low + high));
res.push_back(mapped_x);
}
break;
}
case 1: { // Chebyshev-Gauss points (CG)
for(int i = 0; i < num; i++){
theta = (2*i+1)*CV_PI/(2*num);
double xi = std::cos(theta);
double mapped_x = 0.5*((high-low)*xi + (low + high));
res.push_back(mapped_x);
}
break;
}
case 2: { // Chebyshev-Gauss-Lobatto points (CGL)
for(int i = 0; i <= num; i++){
theta = i*CV_PI/num;
double xi = std::cos(theta);
double mapped_x = 0.5*((high-low)*xi + (low + high));
res.push_back(mapped_x);
}
break;
}
}
return res;
}
int main(){
const int ratio = 600;

//200个参考点,表征原函数趋势
std::vector<cv::Point2d> vp_orig;
for(int i=0; i<=200; i++){
double xi = -1 + i/100.0;
vp_orig.push_back({xi, test_func(xi)});
}

//获取切比雪夫插值点
std::vector<double> vec_chebyshev_x = getChebyshevsPoint(20, -1, 1,0);
std::vector<cv::Point2d> vec_chebyshev_point;
for(const auto& pos : vec_chebyshev_x){
vec_chebyshev_point.push_back({pos, test_func(pos)});
}

//获取CG插值点
std::vector<double> vec_chebyshev_cg = getChebyshevsPoint(20, -1, 1,1);
std::vector<cv::Point2d> vec_chebyshev_point_cg;
for(const auto& pos : vec_chebyshev_cg){
vec_chebyshev_point_cg.push_back({pos, test_func(pos)});
}

//获取CGL插值点
std::vector<double> vec_chebyshev_cgl = getChebyshevsPoint(20, -1, 1,2);
std::vector<cv::Point2d> vec_chebyshev_point_cgl;
for(const auto& pos : vec_chebyshev_cgl){
vec_chebyshev_point_cgl.push_back({pos, test_func(pos)});
}

//切比雪夫点插值效果
std::vector<cv::Point2d> vp_chebyshev;
for(int i=0; i<=200; i++){
double xi = -1 + i/100.0;
vp_chebyshev.push_back({xi, lagrange_interpolation(vec_chebyshev_point, xi)});
}

//CG插值效果
std::vector<cv::Point2d> vp_chebyshev_cg;
for(int i=0; i<=200; i++){
double xi = -1 + i/100.0;
vp_chebyshev_cg.push_back({xi, lagrange_interpolation(vec_chebyshev_point_cg, xi)});
}


//CGL点插值效果
std::vector<cv::Point2d> vp_chebyshev_cgl;
for(int i=0; i<=200; i++){
double xi = -1 + i/100.0;
vp_chebyshev_cgl.push_back({xi, lagrange_interpolation(vec_chebyshev_point_cgl, xi)});
}


//画图
cv::Mat drawPic(512,1024,CV_8UC3); //底图
drawPic.setTo(255);

//绿色线--表征原函数趋势
for(const auto&pos : vp_orig){
circle(drawPic, cv::Point(ratio*pos.x, ratio*pos.y), 2, cv::Scalar(0,255,0), -1);
}

//红色线--切比雪夫点的拉格朗日插值
for(const auto&pos : vp_chebyshev){
circle(drawPic, cv::Point(ratio*pos.x, ratio*pos.y), 2, cv::Scalar(0,0,255), -1);
}

//蓝色线--CG点的拉格朗日插值
for(const auto&pos : vp_chebyshev_cg){
circle(drawPic, cv::Point(ratio*pos.x, ratio*pos.y), 2, cv::Scalar(255,0,0), -1);
}

//黑色线--CGL点的拉格朗日插值
for(const auto&pos : vp_chebyshev_cgl){
circle(drawPic, cv::Point(ratio*pos.x, ratio*pos.y), 2, cv::Scalar(0,0,0), -1);
}


// 添加标签
cv::putText(drawPic, "Green: Original Function", cv::Point(20, 30),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0,255,0), 2);
cv::putText(drawPic, "Red: Normal Chebyshev Lagrange", cv::Point(20, 60),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0,0,255), 2);
cv::putText(drawPic, "Blue: CG Chebyshev Lagrange", cv::Point(20, 90),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255,0,0), 2);
cv::putText(drawPic, "Black: CGL Chebyshev Lagrange", cv::Point(20, 120),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0,0,0), 2);

cv::imshow("drawPic", drawPic);


/**************************************************************勒贝格常数估算**************************************************************/

//计算基函数lk在target_x处的值, k = 0,1...,n
auto calculate_lk = [](const std::vector<cv::Point2d>& input_points, int k, double target_x){
double lk = 1.0;
int n = input_points.size()-1; //插值的阶数,三点二阶、四点三阶......
for(int j=0; j<=n; j++){
if(j != k){
lk *= (target_x - input_points[j].x) / (input_points[k].x - input_points[j].x);
}
}
return lk;
};

//估计勒贝格常数,按0.01的精度取最大值作勒贝格常数值
auto getLebesgue = [calculate_lk](const std::vector<cv::Point2d>& input_points, double low, double high){
double sum, maxsum;
maxsum = sum = 0.0;
int n = input_points.size()-1; //插值的阶数,三点二阶、四点三阶......
for(double x=low; x<=high; x += 0.01){
sum = 0.0;
for(int k=0; k<=n; k++){
sum += std::abs(calculate_lk(input_points, k, x));
}
maxsum = sum > maxsum ? sum : maxsum;
}
return maxsum;
};

//切比雪夫点勒贝格常数:
auto chebyshev_leb = getLebesgue(vec_chebyshev_point, -1, 1);
cout << "normal chebyshev lebesgue = " << chebyshev_leb << endl;

//cg点勒贝格常数:
auto chebyshev_leb_cg = getLebesgue(vec_chebyshev_point_cg, -1, 1);
cout << "cg chebyshev lebesgue = " << chebyshev_leb_cg << endl;

//cgl点勒贝格常数:
auto chebyshev_leb_cgl = getLebesgue(vec_chebyshev_point_cgl, -1, 1);
cout << "cgl chebyshev lebesgue = " << chebyshev_leb_cgl << endl;

cout <<"done" << endl;
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
可见CG点红线和蓝线是完全重合的,CG点和CGL点的插值效果也是相近: 切比雪夫节点插值对比

均差与牛顿插值多项式

从上节可知,拉格朗日插值是根据基函数推导的,推导方式简单,所以也用其基函数定义了勒贝格常数。勒贝格常数与插值算法实现无关,可以根据点分布进行优化,切比雪夫分布点是优化插值龙格现象的有效方法。但从其实现方法也可知,拉格朗日的计算复杂度比较高计算一个插值点的插值函数值就需要遍历所有的点,牛顿多项式旨在设计一种可逐次生成插值多项式的方法,以下。

线性插值点斜式可以表示为:

其可以看成的修正,即:

进一步推广到抛物线插值,有: 因为插值算法是一定经过插值点的,所以:

故:

均差(差商)定义

我们称是函数f(x)关于点和点一阶均差为f(x)的二阶均差推广到一般的: 称为f(x)的k阶均差,也称k阶差商

如上式,令三阶差商就能够表示为:

结合k阶差商,插值多项式就能够表示为: 这就是牛顿插值多项式

均差的相关重要性质如下:

    1. k阶均差可以表示为函数值,,...,线性组合,即:

    关于线性组合这个关系可由数学归纳法证明,此处我们反向结合拉格朗日表达式可以粗略说明这个等式的推导(注意,非证明,因为我们不经证明承认了牛顿插值多项式),根据拉格朗日插值多项式表示: 由于我们在上一节已经证明,不同方法得到的插值多项式是唯一的,因此,因此其系数也是对应的,故对系数是相同的,有: 表述为:k阶均差(差商)等于通过k+1个节点的插值多项式的最高次项系数

    1. 由性质1可知均差的计算具有对称性k阶差商的表示可以推广为: 即分母减去什么,只要和分子的均差项对应,结果是相等的。
    1. f(x)在[a,b]上存在n阶导数,且节点,则n阶均差导数满足关系: 此结论可有罗尔定理证明;更简单的记忆是对应到泰勒公式(实际上泰勒公式是基于牛顿多项式推导,但我们更熟悉泰勒公式),泰勒公式展开的系数是使用导数关系描述的,而牛顿多项式的系数是使用均差关系描述的,根据插值多项式的唯一性,二者自然有上述关系。

综上所述,可以通过函数值的线性组合低阶递推导数三种方法计算均差;

牛顿插值余项

拉格朗日插值余项中,我们的表示是:

其中

根据上述差商导数关系,自然得到牛顿插值余项可以表示为:

牛顿插值余项证明

除了对应到拉格朗日,我们还可以正向证明一下,使用到性质1和性质3的定义式:

根据线性组合定义式子,我们可以得到关于f(x)的均差表示式:

故:

为了得到的递推关系,我们把x看成区间[a,b]一点,构造有:

故有:

得证,以此类推,我们会有: 将后一项不断代入前一项,会得到:

故余项:

得证,该余项等价于拉格朗日定义的余项,而且更具有一般性,当f(x)离散给出或者导数不存在时,拉格朗日余项失效,但该牛顿插值余项仍然适用

C++实现:牛顿插值

牛顿插值通过递推得到均差表,插值算法只使用均差表每列的首行,理论上存储优化可到, 计算过程最大空间不超过; 差商表确定,通过的复杂度即可递推出所有插值点,对每个插值点,高次项可复用低次项的连乘结果,算上计算均差的复杂度,总体算法复杂度,其中n参考插值点m需要插值的点数,对应的拉格朗日插值的复杂度,可见若需要插值的数据很多,牛顿法提高的性能也非常可观,实现见下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <iostream>
#include <opencv2/core/types.hpp>
#include <vector>
#include <opencv2/opencv.hpp>

using namespace std;

//输入:插值点
//输出:均差表
std::vector<std::vector<double>> caldivided_difference(const std::vector<cv::Point2d>& points)
{
int n = points.size();
std::vector<std::vector<double>> res(n, std::vector<double>(n, 0.0));

for(int j=0; j<n; j++){
res[j][0] = points.at(j).y;
}

for(int i=1; i<n; i++){
for(int j=i; j<n; j++){
res[j][i] = (res[j][i-1] - res[j-1][i-1])/(points.at(j).x - points.at(j-i).x);
}
}
return res;
}

//输入:插值点、均差表和目标插值点,输出插值函数值
double newton_interpolation(const std::vector<cv::Point2d>& points, const std::vector<std::vector<double>> &divided_diff, double target_x){
double res = divided_diff[0][0];
double term = 1.0;
for(int i=1; i<divided_diff.size(); i++) //牛顿插值只用到均差表每列的首行
{
term *= (target_x - points.at(i-1).x);
res += divided_diff[i][i]*term;
}
return res;
}


int main(){

//模拟数据
std::vector<cv::Point2d> calibPoint{
{100,100.5},
{200,298.8},
{300,399.6},
{400,402.1},
{500,596.6},
{600,698.2},
{700,799.5},
{800,900.2},
};

//8个点插值至1024个点
std::vector<cv::Point2d> res_points;

for(int x=0; x < 1024; x++){
res_points.push_back({(double)x,newton_interpolation(calibPoint,caldivided_difference(calibPoint), x)});
}

//画图示意
cv::Mat drawPic(1024,1024,CV_8UC3); //底图
drawPic.setTo(255);

//结果点
for(const auto&pos : res_points){
circle(drawPic, cv::Point(pos.x, pos.y), 2, cv::Scalar(0,0,255), -1);
}

//参考点
for(const auto& pos : calibPoint){
circle(drawPic, cv::Point(pos.x, pos.y), 2, cv::Scalar(255,0,0), -1);
}

cv::imshow("newton_interpolation",drawPic);

cout <<"done" << endl;
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}

差分形式牛顿插值

前述牛顿插值多项式适用于任意节点分布,但通常为了方便,实际应用经常使用等距节点(虽然可能导致数值不稳定),其牛顿公式还能进一步优化

先引入一些概念:

等距节点, , 且称h步长,因此有: 称为一阶(前向)差分

对应的二阶差分n阶差分

函数值与差分

为了描述方便,引入两个算子,有: I称为不变算子E称为位移算子,由此有: 其中又记二项式展开系数

反过来,f(x)的函数值可以使用各阶差分表示:

算子关系: 有:

导数和前向差分转换

由导数定义就可知:

均差和前向差分转换

等距节点步长h,有:

故:

牛顿前插公式

从牛顿多项式看,为:

设取了n个等距节点,步长为h,自变量步数为t,令 根据前向差分和均差关系式,替换即可得到: 牛顿前插公式

对应的其余项形式变为:

埃尔米特(Hermite)插值

插值算法均要求插值函数插值点的值等于原函数值,有的实际问题不仅要求函数值相等,还要求其导数值高阶导数值相等,满足这种要求的多项式就称为埃尔米特插值多项式

重节点均差与泰勒插值

给出一个关于均差的结论(原书也没有证明):

  • 为[a,b]上相异节点,则均差是其变量的连续函数,即:

由此结论,定义重节点均差,即重复节点的均差为:

故:

故得到泰勒插值多项式 余项为:

泰勒多项式的导数满足: 泰勒多项式是一种埃尔米特插值多项式,事实上:给定包含函数值、导数值、高阶导数值等共m+1插值条件,可造出次数不超过m埃尔米特插值多项式。

两个典型埃尔米特插值函数

三次埃尔米特插值

三次埃尔米特插值给定条件为:

给定条件数是,因此确定的多项式最高是3次,因此待定形式有:

可计算得待定系数为:

此即为三次埃尔米特插值多项式。

余项为:

该结论能从罗尔定理开始证明,但是我们在证明拉格朗日和均差时都用过了,直接推导也可以,如果是个条件,四个不同节点,由牛顿插值余项可知余项为:

此处因为一阶导数条件施加在,因此双重根差商也变成重节点均差,由上述结论可知,有:

两点三次埃尔米特插值

两点三次埃尔米特插值是很常用的埃尔米特插值公式,其约束条件只有两个点,常用于某段区间插值,满足:

采用拉格朗日相同的基函数推导,两点三次埃尔米特插值待定函数为:

基函数分别满足:

观察到,可以推到必然存在重根项, 而最高次项都可能是三次,因此设待定式子为: 此处除以一个是为了计算方便进行的归一化。

因为满足,故:

故:

故有:

同理可计算其它三个基函数待定系数,此处略,两点三次埃尔米特插值最后解析式记为:

其余项形式和上述三次埃尔米特插值还是一样的,事实上n代表给出条件数,重根项取决于条件数中函数值、导数、高阶导数施加的条件多少,例如如果某个点函数值、导数、二阶导数都是0,那么这个项就是一个三阶项。

两点三次埃米尔特插值的余项表示为:

三次样条插值

分段高次插值会造成严重的龙格现象,低次插值的光滑程度不满足应用关系,三次的埃尔米特插值虽然比低次插值光滑程度有所改善,但需要提高导数信息,结果也仅仅是一阶连续的光滑程度,因此应用场景局限,此几节不再讨论。

基于这些缺点,三次样条插值被提出,由于该方法太著名,在很早的插值算法文章就提过并且给出相关实现,见数值分析:三次样条插值算法(Cubic Spline Interpolation),本节略。

第三章:函数逼近与快速傅里叶变换

第二章插值法是函数逼近的一种,在本章,我们给出的函数逼近定义是:对函数类A中给定的函数,要求在另一类简单的且便于计算的函数类中求得函数, 使得某种度量意义下误差最小.通常是一种连续函数空间,记或者,即具有p阶连续导数的函数空间。B类多项式乘法和加法也构成数域中一个线性空间,记为,称为多项式空间

魏尔斯特拉斯(Weierstrass)定理

  • , 对任意, 总是存在一个代数多项式p(x),使得: 在[a,b]区间上一致成立。

魏尔斯特拉斯定理主要告诉我们,任何连续函数总能找到一个代数多项式逼近,且在某个闭区间误差小于任意给定的正数。

很多定理反过来证明了这点,如泰勒展开的余项总是一个与高阶导数相关的无限小,这里要介绍的是最经典的证明方法————伯恩斯坦多项式证明,它遵循我们很熟悉的分布,即二项分布

伯恩斯坦多项式证明

伯恩斯坦多项式证明用到二次项分布的三个引理:

    对引理1,是典型二项分布关系式,其概率和就是1。

    1. 由引理1可以证明引理2: 实际上引理2和二项分布的期望有关,即:
    1. 同理,使用引理1和2硬算来证明,有: 引理三显然和二项分布的方差计算相关,利用引理三计算二项分布的方差

扯远了,总而言之,我们引入并且证明了上述三个引理,构造伯恩斯坦多项式为:

因为f(x)是连续的,严谨来说,任意连续区间映射到[0,1]区间,f(x)一致连续,即对使得:

估计误差: 将误差分为两部分,其中一部分k取值落在一致连续的误差,另一部分在

利用引理3对第二项误差进行控制,设

第二步的放大我们引入了引理三部分,此处必然有,因为此处的k都在一致连续的x范围之外,而且将求和项推广到整个k取值,这些项必然都是非负项,因此最终有: 由基本不等式知:

故最终有: 给定,只要控制n,n足够大时总有:

证毕。故显然的,伯恩斯坦多项式能够逼近闭区间任意连续的f(x),这也是魏尔斯特拉斯逼近定理的经典证明,但二次项多项式收敛速度太慢,工程上一般不使用伯恩斯坦多项式作为函数逼近多项式。

函数逼近与函数空间数学理论

在后续的章节涉及逼近理论的一些术语,这里先按原书概念和额外拓展记录一些定义。

范数与赋范线性空间

为了定义线性空间中向量的长度,需要引入范数的概念,因为是描述向量长度的,它应该满足非负线性延申两点直线最短等直观的几何性质,从代数语言的角度,范数的标准定义是:

  • 若S为线性空间,,若存在唯一实数(这个符号只是代表占位符,既可以是一个变量或者函数)满足条件
  1. 正定性(非负的、不能无中生有,有又变无):,当且仅当时,

  2. 齐次性(延申是线性的):

  3. 三角不等式(直线距离最短):

其中为对应的向量空间,称线性空间S上的范数,S和一起称为赋范线性空间,记为;

简单的,对n维的(欧几里得)向量空间中的向量,有三种常用范数

  • 1-范数,即绝对值和;

  • 2-范数,即平方和开根号;

  • 范数(最大范数): ,即绝对值最大者;

连续函数空间的C[a,b], 三种范数定义为: - 1-范数,即绝对值函数积分;

-** 2-范数**:,即平方和开根号;

  • 范数(最大范数): ,即绝对值最大者;

内积与内积空间

范数与赋范线性空间是用于定义向量长度的,本身没有方向信息,因此此处还要引入内积内积空间定义角度信息

过去在线性代数中,我们定义向量空间两个向量内积定义是:

推广到线性空间内积定义满足

  1. , ;当为实数域对应的线性空间时,有

  2. , ;拓展一下,课本没有提到,,这个在下面的柯西施瓦茨不等式复数域证明就用到了。

  3. ,

  4. , 当且仅当u=0,(u,u) = 0.

柯西-施瓦茨(Cauchy-Schwarz)不等式

为内积空间,对都有: 当且仅当u、v线性相关时,等号成立。

要证明也很简单,但是书本的证明不那么严谨,以下。

当v=0时,等号成立; 当时,则,则对任意数均有:

代入得:

即:

问题出在了展开时,在复数域对应的线性空间中,(u,v)不等于(v,u),因此直接写成2是有问题的正确的证明应该为:

, 即,带入得:

故:

在复数域中此处的竖线代表模长,即令

柯西施瓦茨不等式在任意数域的线性空间成立,证明也比较抽象,如果放在二、三维空间很容易理解,例如在的向量空间:

未完待续