• 最优化 | 一维搜索与方程求根 | C++实现


    参考资料

    前言

    • 即使目标函数 f ( x ) f(x) f(x) 是一元函数,求最小值点也经常需要使用数值迭代方法。

    • 在多元目标函数优化 中,一般每次迭代从上一步的 x ( t ) \boldsymbol{x}^{(t)} x(t) 先确定一个下降方向 d ( t ) \boldsymbol{d}^{(t)} d(t) ,然后对派生出的一元函数 h ( α ) = f ( x ( t ) + α d ( t ) ) , α ≥ 0 h(\alpha)=f\left(\boldsymbol{x}^{(t)}+\alpha \boldsymbol{d}^{(t)}\right), \alpha \geq 0 h(α)=f(x(t)+αd(t)),α0 求最小值点得到下降的步长 α t \alpha_t αt ,并令 x ( t + 1 ) = x ( t ) + α t d ( t ) \boldsymbol{x}^{(t+1)}=\boldsymbol{x}^{(t)}+\alpha_t \boldsymbol{d}^{(t)} x(t+1)=x(t)+αtd(t) , 求步长 的过程称为一维搜索,搜索可以是求一元函数 h ( α ) h(\alpha) h(α) 的精确最小值点,也可以求一个使得目标函数下降足够 多的 α \alpha α 作为步长。

    • 对于多元目标函数 f ( x ) , x ∈ R d f(\boldsymbol{x}) , \boldsymbol{x} \in \mathbb{R}^d f(x)xRd, 有一种优化方法是先沿 x 0 x_0 x0 坐标轴方向搜索,再沿 x 1 x_1 x1 坐标轴方向搜索,直到沿 x d x_d xd 坐标轴方向搜索后再重复地从 x 0 x_0 x0 坐标轴方向开始, 这种方法叫做坐标循环下降法。

    • 一元函数的求根问题和优化问题使用的算法类似,下面讨论一元函数的优化和求根问题。

    所涉及C++代码均存于github仓库
    python版本代码见于github仓库

    1. 二分法求根

    优化问题与求根问题密切相关,很多优化问题会归结为一个求根问题。对一元目标函数 f ( x ) f(x) f(x) ,若 f ( x ) f(x) f(x) 连续 可微,极值点一定是 f ′ ( x ) = 0 f^{\prime}(x)=0 f(x)=0 的根。二分法是实际数值计算中一元函数求根使用最多的方法, 这种方法简单而又有比较高的收敛速度。

    设函数 f ( x ) f(x) f(x) 在闭区间 [ a , b ] [a, b] [a,b] 连续, 且 f ( a ) f ( b ) ≤ 0 f(a) f(b) \leq 0 f(a)f(b)0 ,由零点定理,至少有一个点 x ∗ ∈ [ a , b ] x^* \in[a, b] x[a,b] 使得 f ( x ∗ ) = 0 f\left(x^*\right)=0 f(x)=0 。如果 f ( x ) f(x) f(x) [ a , b ] [a, b] [a,b] 上还是严格单调函数则解 x ∗ x^* x 存在是唯一的。

    一般使用二分法求根时需要函数是单调的(不妨设单调递增)。

    1.1 [a,b]区间二分法求根

    1.1.1 原理

    二分法求根用区间 [ a , b ] [a, b] [a,b] 中点 c c c 处函数值 f ( c ) f(c) f(c) 的正负号来判断根在区间中点的左边还是右边,显然,如果 f ( c ) f(c) f(c) f ( a ) f(a) f(a) 异号,则 [ a , c ] [a, c] [a,c] 中有根;如果 f ( c ) f(c) f(c) f ( b ) f(b) f(b) 异号,则 [ c , b ] [c, b] [c,b] 中有根。根据这样的想法,可以迭代地每次用法如下:

    设真实根为 x ∗ x^* x ,第 k k k 步的区间中点为 x ( k ) x^{(k)} x(k) ,则
    ∣ x ( k ) − x ∗ ∣ ≤ ( b − a ) ( 1 2 ) k , \left|x^{(k)}-x^*\right| \leq(b-a)\left(\frac{1}{2}\right)^k, x(k)x (ba)(21)k,
    二分法具有线性收敛速度。

    1.1.2 C++实现

    求解 x 2 − ln ⁡ x = 0 x^2-\ln x=0 x2lnx=0的根。含根的区间为 [ 0 , 2 ] [0,2] [0,2]

    //
    // Created by CHH3213 on 2022/9/12.
    //
    #include 
    #include
    using namespace std;
    #define EPS 1e-6
    /*
     * 使用二分法求解方程的根:np.log(x)+x^2=0
     */
    double func(double x){
        /**
         * 构造函数
         * x:自变量
         * return:函数值
         */
        return log(x)+x*x;
    }
    
    double bisection(double a, double b){
        /**
         * 二分法求根
         * [a,b]:  含有根的区间
         */
        if(a>b)swap(a,b);//确保a < b
        double y_a = func(a),y_b =func(b);
        if(y_a==0){// 已经是根
            b=a;
            return a;
        }
        if(y_b==0){// 已经是根
            a=b;
            return b;
        }
        while(b-a>EPS){
            double x = a+(b-a)/2;
            double y = func(x);
            if(y==0){
                a=x,b=x;
                break;
            }else if(y*y_a>=0){ // 根在右半边
                a=x;
            }else{//根在左半边
                b=x;
            }
        }
        return a;
    }
    int main(){
        cout<<"answer: "<<bisection(0.0,2.0)<<"----prove: "<<func(bisection(0.0,2.0)) <<endl;
        return 0;
    };
    
    • 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

    求出的根为

    answer: 0.652918----prove: -2.20888e-006
    
    • 1

    1.2 区间右侧无穷的二分法求根

    如果 f ( x ) f(x) f(x) [ a , ∞ ) [a, \infty) [a,) 区间上的严格单调的连续函数, 且 lim ⁡ x → ∞ f ( x ) \lim _{x \rightarrow \infty} f(x) limxf(x) f ( a ) f(a) f(a) 反号,则 f ( x ) f(x) f(x) [ a , ∞ ) [a, \infty) [a,) 上存在唯 一的根, 这时需要先找到闭区间 [ a , b ] [a, b] [a,b] 使得 f ( a ) f(a) f(a) f ( b ) f(b) f(b) 反号,二分法算法需要略作调整:

    其它的情况,比如 f ( x ) f(x) f(x) 定义在 ( − ∞ , ∞ ) , ( a , b ) (-\infty, \infty),(a, b) (,),(a,b) 的情况可类似处理, 都需要先找到使得区间端点函数值符号 相反的闭区间。

    1.3 求含根区间

    如果需要找出哪个闭区间含根,需要使用求含根区间的算法,假设初始步长为 h 0 h_0 h0

    求出含根的闭区间后再使用二分法进行求根。

    2. 牛顿法求根

    2.1 原理

    假设我们要求 f ( x ) = 0 f(x)=0 f(x)=0的根,牛顿法的求解步骤如下:

    给定一 个初值 x 0 x_0 x0 ,在 x 0 x_0 x0 处用一阶泰勒展式来近似表示函数 f ( x ) f(x) f(x)
    f ( x ) = f ( x 0 ) + f ′ ( x 0 ) ( x − x 0 ) (1) \tag{1} f(x)=f\left(x_0\right)+f^{\prime}\left(x_0\right)\left(x-x_0\right) f(x)=f(x0)+f(x0)(xx0)(1)
    f ( x ) = 0 f(x)=0 f(x)=0 带入公式 ( 1 ) (1) (1) ,求得与 x x x 轴交点横坐标 x = x 0 − f ( x 0 ) / f ′ ( x 0 ) x=x_0-f\left(x_0\right) / f^{\prime}\left(x_0\right) x=x0f(x0)/f(x0) ,这个 x x x 点并 不是函数 f ( x ) f(x) f(x) 的根,但是距离真正的根更近了一点,如下图所示。

    将上一步所求的 x x x 值作为 x 1 x_1 x1 值,那么 x 1 = x 0 − f ( x 0 ) / f ′ ( x 0 ) x_1=x_0-f\left(x_0\right) / f^{\prime}\left(x_0\right) x1=x0f(x0)/f(x0) x 1 x_1 x1 值处用一阶泰勒展式:
    f ( x ) = f ( x 1 ) + f ′ ( x 1 ) ( x − x 1 ) (2) \tag{2} f(x)=f\left(x_1\right)+f^{\prime}\left(x_1\right)\left(x-x_1\right) f(x)=f(x1)+f(x1)(xx1)(2)
    f ( x ) = 0 f(x)=0 f(x)=0 带入公式 ( 2 ) (2) (2) ,求得交点 x = x 1 − f ( x 1 ) / f ′ ( x 1 ) x=x_1-f\left(x_1\right) / f^{\prime}\left(x_1\right) x=x1f(x1)/f(x1) ,依次迭代进而推出公式 (3)
    x n + 1 = x n − f ( x n ) / f ′ ( x n ) (3) \tag{3} x_{n+1}=x_n-f\left(x_n\right) / f^{\prime}\left(x_n\right) xn+1=xnf(xn)/f(xn)(3)
    最终求得的 x x x 值变化小于预先设定的精度就认为这个 x x x 值是函数 f ( x ) f(x) f(x) 的近似根,此时牛顿法收敛。

    2.2 c++实现

    求解 x 2 − ln ⁡ x = 0 x^2-\ln x=0 x2lnx=0的根。

    //
    // Created by CHH3213 on 2022/9/12.
    //
    #include
    #include 
    
    using namespace std;
    
    #define DELTA 1e-4
    #define EPS 1e-6
    /*
     * 使用牛顿法求解方程的根:-np.log(x)+x^2=0
     * 步骤:原函数->构造损失函数->对损失函数求导->进行梯度下降
     */
    double func(double x){
        /**
         * 构造函数
         * x:自变量
         * return:函数值
         */
        return log(x)+x*x;
    }
    
    double func_prime(double x){
        /**
         * 计算函数导数,以导数定义的方式求解
         */
        return func(x+DELTA)-func(x-DELTA)/(2*DELTA);
    }
    double Newton(double x0){
        double x = x0;
        double x_last=0.1;
        while(abs(x-x_last)>EPS){
            x_last=x;
            x=x_last-func(x_last)/ func_prime(x_last);
        }
        return x;
    }
    
    int main(){
        cout<<"answer: "<<Newton(0.5)<<"----prove: "<<func(Newton(0.5)) <<endl;
        return 0;
    }
    
    • 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

    求出的根为

    answer: 0.652899----prove: -5.607e-005
    
    • 1

    3. 梯度下降法求根

    梯度下降算法一般用于求解最值问题,但同样也可以求解方程 f ( x ) f(x) f(x)的根。求解方程的根即求解在哪个点 f ( x ) f(x) f(x)离0的距离最近。数学上定义两个数距离就是绝对值,但是因为绝对值不便于计算,所以将其替换成等价的差的平方,即损失函数:
    l o s s = ( f ( x ) − 0 ) 2 loss = (f(x)-0)^2 loss=(f(x)0)2

    求解方程的根,就是找到一个点使得构造的损失函数最小,这样就转化成了求最值问题。

    3.1 c++实现

    求解二次方程 x 2 + 2 x − 3 = 0 x^2+2x-3=0 x2+2x3=0的根。

    我们通过设置不同的初值找出这个二次方程的根。

    //
    // Created by CHH3213 on 2022/8/18.
    //
    #include
    #include 
    /*
     * 使用梯度下降法求解二次方程的根
     * 步骤:原函数->构造损失函数->对损失函数求导->进行梯度下降
     */
    using namespace std;
    
    #define DELTA 1e-4
    #define EPS 1e-6
    //y=ax^2+bx+c
    double func(double a,double b,double c,double x){
        return a*pow(x,2)+b*x+c;
    }
    
    //构造损失函数
    double loss_func(double a,double b,double c,double x){
        return pow(func(a,b,c,x)-0,2);
    }
    
    //构造损失函数导数
    double loss_func_prime(double a,double b,double c,double x){
        return (loss_func(a,b,c,x+DELTA)-loss_func(a,b,c,x))/DELTA;
    }
    
    double GradientDescent(double a,double b,double c,double x0,double learning_rate ){
        /**
         * 梯度下降算法
         * a,b,c为二次函数的系数
         * x0为迭代初值
         * learning_rate为学习率
         */
        double x = x0;
        while(abs(loss_func(a,b,c,x))>EPS){
            x = x-learning_rate* loss_func_prime(a,b,c,x);
        }
        return x;
    }
    int main(){
        double a = 1.0,b=2.0,c=-3.0;
        double x0=-b/(2*a)-1;//通过设置不同初值找出来
        double x1= GradientDescent(a,b,c,x0,0.01);
        x0=-b/(2*a)+1;
        double x2= GradientDescent(a,b,c,x0,0.01);
        cout<<x1<<" "<<x2<<endl;
    }
    
    
    • 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

    4. 一维搜索的区间

    4.1 一般一维搜索方法

    • 对一元连续函数 f ( x ) f(x) f(x) ,若存在 A < x ∗ < B AA<x<B 使得 f ( x ) f(x) f(x) ( A , x ∗ ] \left(A, x^*\right] (A,x] 单调下降,在 [ x ∗ , B ) \left[x^*, B\right) [x,B) 单调上升,则称 f ( x ) f(x) f(x) ( A , B ) (A, B) (A,B) 是单谷的。这时,若存在 A < a < c < b < B AA<a<c<b<B 使得 f ( c ) < f ( a ) , f ( c ) < f ( b ) f(c)f(c)<f(a),f(c)<f(b) ,则 x ∗ x^* x 必 在 ( a , b ) (a, b) (a,b) 中。这是因为如果 x ∗ ≤ a x^* \leq a xa ,则 a , c , b a, c, b a,c,b 三个点在单调上升分支, 必有 f ( a ) ≤ f ( c ) ≤ f ( b ) f(a) \leq f(c) \leq f(b) f(a)f(c)f(b) , 如果 x ∗ ≥ b x^* \geq b xb a , c , b a, c, b a,c,b 三个点在单调下降分支,必有 f ( a ) ≥ f ( c ) ≥ f ( b ) f(a) \geq f(c) \geq f(b) f(a)f(c)f(b) , 都会导致矛盾。

    • 设定义于 ( − ∞ , ∞ ) (-\infty, \infty) (,) ( 0 , ∞ ) (0, \infty) (0,) 的连续目标函数 f ( x ) f(x) f(x) 存在局部极小值点 x ∗ x^* x, 且设 f ( x ) f(x) f(x) x ∗ x^* x 的一个邻域内是单 谷的, 则只要能找到三个点 a < c < b aa<c<b 使得 f ( a ) > f ( c ) , f ( c ) < f ( b ) f(a)>f(c), f(c)f(a)>f(c),f(c)<f(b) ,则在 ( a , b ) (a, b) (a,b) 内必存在 f ( x ) f(x) f(x) 的一个极 小值点。

    • 为了求 a , b a, b a,b, 从两个初始点 x 0 , x 1 x_0, x_1 x0,x1 出发,如果 f ( x 0 ) > f ( x 1 ) f\left(x_0\right)>f\left(x_1\right) f(x0)>f(x1) ,则最小值点可能在 x 1 x_1 x1 右侧,向右侧搜索找到 一个函数值超过 f ( x 1 ) f\left(x_1\right) f(x1) 的点 x 2 x_2 x2 ,这时最小值点应该在 ( x 0 , x 2 ) \left(x_0, x_2\right) (x0,x2) 内部; 如果 f ( x 0 ) < f ( x 1 ) f\left(x_0\right)f(x0)<f(x1) ,则最小值点可 能在 x 0 x_0 x0 左侧,向左侧搜索。

    • 如果函数 f ( x ) f(x) f(x) 的定义域为 x ∈ ( 0 , ∞ ) x \in(0, \infty) x(0,) ,以上的算法应取初值 x 0 > 0 x_0>0 x0>0 ,如果出现向左搜索,可取迭代公式为 x k + 1 = 1 γ x k x_{k+1}=\frac{1}{\gamma} x_k xk+1=γ1xk

    • 如果目标函数 f ( x ) f(x) f(x) 连续可导,则求最小值点所在区间也可以通过求 a < b aa<b 使得 f ′ ( a ) < 0 , f ′ ( b ) > 0 f^{\prime}(a)<0, f^{\prime}(b)>0 f(a)<0,f(b)>0 来解决。可取初值 x 0 x_0 x0 ,如果 f ′ ( x 0 ) < 0 f^{\prime}\left(x_0\right)<0 f(x0)<0 ,则类似上面算法那样向右搜索找到 b b b 使得 f ′ ( b ) > 0 f^{\prime}(b)>0 f(b)>0 ; 如果 f ′ ( x 0 ) > 0 f^{\prime}\left(x_0\right)>0 f(x0)>0 ,则向左搜索找到 a < x 0 aa<x0 使得 f ′ ( a ) < 0 f^{\prime}(a)<0 f(a)<0 。用最后得到的两个点作为 a , b a, b a,b 。可以看出,这里描述的搜索方法适用于一元函数求根时寻找包含根的区间的问题。

    4.2 黄金分割法(0.618)

    4.2.1 原理

    0.618法(黄金分割法)是一种常用的不需要导数信息的一维搜索方法。假设 f ( x ) f(x) f(x) 在闭区间 [ a , b ] [a, b] [a,b] 内是单谷的,存在最小值点 x ∗ ∈ ( a , b ) x^* \in(a, b) x(a,b) , 这时可以用 0.618 0.618 0.618 法求最小值点。

    在区间 [ a , b ] [a, b] [a,b] 内对称地取两个点 c < d cc<d 在所谓的黄金分割点上, 即
    d − a b − a = ρ ≜ 5 − 1 2 ≈ 0.618 , b − c b − a = ρ , \frac{d-a}{b-a}=\rho \triangleq \frac{\sqrt{5}-1}{2} \approx 0.618, \quad \frac{b-c}{b-a}=\rho, bada=ρ25 10.618,babc=ρ,
    黄金分割比例 ρ \rho ρ 满足
    1 − ρ ρ = ρ , \frac{1-\rho}{\rho}=\rho, ρ1ρ=ρ,

    • 这样的比例的特点是,当区间缩小到 [ a , d ] [a, d] [a,d] 时, c c c 仍为缩小后的区间的右侧黄金分割点;当区间缩小到 [ c , b ] [c, b] [c,b] 时, d d d 仍为缩小后的区间的左侧黄金分割点。

    • 这样选了 c , d c, d c,d 两个点后,比较 f ( c ) f(c) f(c) f ( d ) f(d) f(d) 的值,如果 f ( c ) f(c) f(c) 较 小,则因为 a < c < d , f ( a ) > f ( c ) , f ( c ) < f ( d ) af(c), f(c)a<c<d,f(a)>f(c),f(c)<f(d), 最小值点 x ∗ x^* x 一定在区间 [ a , d ] [a, d] [a,d] 内,可以把搜索区间缩 小到 [ a , d ] [a, d] [a,d], 以 [ a , d ] [a, d] [a,d] 作为含有最小值点的区间再比较其中两个黄金分割点的函数值,而且这时 c c c [ a , d ] [a, d] [a,d] 中右 侧的黄金分割点,只要再添加左侧的黄金分割点就可以了。

    • 如果比较 f ( c ) f(c) f(c) f ( d ) f(d) f(d) 时发现 f ( d ) f(d) f(d) 较小,则把搜 索区间缩小到 [ c , b ] [c, b] [c,b], 这时 d d d [ c , b ] [c, b] [c,b] 内的左侧黄金分割点,只要再添加右侧黄金分割点。每次迭代使得区间长 度缩小到原来的0.618倍, 缩小后的两个黄金分割点中一个的坐标和函数值是已经算过的。

    设规定的最小值点绝对误差限为 ϵ \epsilon ϵ ,算法如下:

    0.618法具有线性收敛速度。

    0.61 8 n ( b − a ) ≤ ϵ ⇒ n ≥ ln ⁡ ϵ b − a ln ⁡ 0.618 0.618^n(b-a)\leq \epsilon\Rightarrow n\ge\frac{ \ln\frac{\epsilon}{b-a}}{\ln 0.618} 0.618n(ba)ϵnln0.618lnbaϵ

    4.2.2 c++实现

    求解函数 f ( x ) = x 2 − 5 f(x)=x^2-5 f(x)=x25迭代 n n n次后的最小值区间。每次迭代区间长度减小到原来的0.618倍。

    //
    // Created by CHH3213 on 2022/9/12.
    //
    #include 
    #include
    #include 
    
    using namespace std;
    #define EPS 1e-6
    /*
     * 使用黄金搜索法求解迭代n次后的含最小值区间,:f(x)=x^2-5
     */
    double func(double x){
        /**
         * 构造函数,单谷函数
         * x:自变量
         * return:函数值
         */
        return x*x-5;
    }
    
    vector<double> golden_section_search(double a, double b,int n){
        /**
         *  含最小值的区间[a,b]
         * n: 迭代次数
         */
        double rho = (sqrt(5)-1)/2.0;//0.618黄金分割比
        double c=rho*a+(1-rho)*b;//(a, b)中靠近a的黄金分割点
        double d = rho*b+(1-rho)*a;//(a,b)中靠近b的黄金分割点
        while(n--){
            if(func(c)>func(d)){
                //这时(c, b)是含最小值区间, d是(c, b)的中靠近c的黄金分割点, 仍用a, b表示含最小值区间,c表示靠近a的黄金分割点
                a=c;
                c=d;
                d= rho*b+(1-rho)*a;//(a,b)中靠近b的黄金分割点
            }else{
                //这时(a, d)是含最小值区间,c是区间(a,d)的靠近d的黄金分割点,仍用a, b表示含最小值区间,d表示靠近b的黄金分割点
                b=d;
                d=c;
                c=rho*a+(1-rho)*b;
            }
        }
        return {a,b};
    }
    
    int main(){
        vector<double> res = golden_section_search(-2,2,10);
        cout<<res[0]<<' '<<res[1]<<endl;
        return 0;
    }
    
    • 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

    迭代10次的结果为

    -0.0263112 0.00621124
    
    • 1

    用黄金分割法求解最小值也很简单,只要迭代次数够多,或者设置 b − a b-a ba的精度达到预设的阈值即可。

    进一步地,如果是求 f ( x ) = x 2 − 5 = 0 f(x)=x^2-5=0 f(x)=x25=0的根,那么可以构造损失函数,求解损失函数的最小值也就是求解方程的根。代码相比之前的改动很小:

    //
    // Created by CHH3213 on 2022/9/12.
    //
    #include 
    #include
    #include 
    
    using namespace std;
    #define EPS 1e-6
    /*
     * 使用黄金搜索法求解迭代n次后的方程的根所在区间:f(x)=x^2-5=0
     */
    double func(double x){
    		//构造损失函数(x^2-5)^2
        return pow(x*x-5,2);
    }
    
    vector<double> golden_section_search(double a, double b,int n){
        /**
         *  含最小值的区间[a,b]
         * n: 迭代次数
         */
        double rho = (sqrt(5)-1)/2.0;//0.618黄金分割比
        double c=rho*a+(1-rho)*b;//(a, b)中靠近a的黄金分割点
        double d = rho*b+(1-rho)*a;//(a,b)中靠近b的黄金分割点
        while(n--){
            if(func(c)>func(d)){
                //这时(c, b)是含最小值区间, d是(c, b)的中靠近c的黄金分割点, 仍用a, b表示含最小值区间,c表示靠近a的黄金分割点
                a=c;
                c=d;
                d= rho*b+(1-rho)*a;//(a,b)中靠近b的黄金分割点
            }else{
                //这时(a, d)是含最小值区间,c是区间(a,d)的靠近d的黄金分割点,仍用a, b表示含最小值区间,d表示靠近b的黄金分割点
                b=d;
                d=c;
                c=rho*a+(1-rho)*b;
            }
        }
        return {a,b};
    }
    
    int main(){
        vector<double> res = golden_section_search(-3,3,20);
        cout<<res[0]<<' '<<res[1]<<endl;
    
        return 0;
    }
    
    • 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

    4.3 抛物线法

    4.3.1 原理

    为了用二次多项式逼近 f ( x ) f(x) f(x) ,只 要有 f ( x ) f(x) f(x) 函数图像上的三个点就可以。仍假设 f ( x ) f(x) f(x) 在闭区间 [ a , b ] [a, b] [a,b] 内是单谷的,存在最小值点 x ∗ ∈ ( a , b ) x^* \in(a, b) x(a,b) 。 设 c ∈ ( a , b ) c \in(a, b) c(a,b), 利用 ( a , f ( a ) ) , ( c , f ( c ) ) , ( b , f ( b ) ) (a, f(a)),(c, f(c)),(b, f(b)) (a,f(a)),(c,f(c)),(b,f(b)) 三个点求得二次插值多项式 φ ( x ) \varphi(x) φ(x) ,用 φ ( x ) \varphi(x) φ(x) 的最小值点 作为下一个近似值。

    二次插值多项式知识点可以参考这篇博客

    设插值多项式为
    φ ( x ) = β 0 + β 1 x + β 2 x 2 , \varphi(x)=\beta_0+\beta_1 x+\beta_2 x^2, φ(x)=β0+β1x+β2x2,
    其中 β 2 > 0 \beta_2>0 β2>0, 在给定了 a < c < b aa<c<b 和相应函数值后,可以解出
    β 2 = 1 b − a [ f ( b ) − f ( c ) b − c − f ( c ) − f ( a ) c − a ] , β 1 = f ( c ) − f ( a ) c − a − β 2 ( c + a ) ,

    β2=1ba[f(b)f(c)bcf(c)f(a)ca],β1=f(c)f(a)caβ2(c+a)," role="presentation" style="position: relative;">β2=1ba[f(b)f(c)bcf(c)f(a)ca],β1=f(c)f(a)caβ2(c+a),
    β2β1=ba1[bcf(b)f(c)caf(c)f(a)],=caf(c)f(a)β2(c+a),
    从而求得 φ ( ⋅ ) \varphi(\cdot) φ() 的最小值点
    x ~ = − β 1 2 β 2 \tilde{x}=-\frac{\beta_1}{2 \beta_2} x~=2β2β1

    • 如果 x ~ < c \tilde{x}x~<c, 则以 a , x ~ , c a, \tilde{x}, c a,x~,c 为新的插值节点继续计算插值多项式并求插值多项式的最小值点;
    • 如果 x ~ > c \tilde{x}>c x~>c, 则以 c , x ~ , b c, \tilde{x}, b c,x~,b 为新的插值节点继续计算插值多项式并求插值多项式的最小值点,如此迭代直至三个插值点足够接 近。

    适当条件下抛物线法达到超线性收敛速度。

    4.3.2 c++实现

    求解函数 f ( x ) = x 2 − 5 f(x)=x^2-5 f(x)=x25的最小值点。

    //
    // Created by CHH3213 on 2022/9/12.
    //
    #include
    #include 
    /*
     * 使用抛物线法求解 x*x-5的最小值点
     */
    using namespace std;
    
    #define DELTA 1e-4
    #define EPS 1e-6
    double func(double x){
        /**
         * 构造函数
         * x:自变量
         * return:函数值
         */
        return x*x-5;
    }
    
    double quadratic_fit_search(double a,double b,double c,int n){
        /**
         * 抛物线法;
         * 区间(a,b);
         * a
         while(n--){
             double beta_1 = 1/(b-a)*((func(b)-func(c))/(b-c)-(func(c)- func(a))/(c-a));
             double beta_2 = (func(c)-func(a))/(c-a)-beta_2*(c+a);
             double x = -beta_1/(2*beta_2);
             if(x<c){
                 b=c;
                 c=x;
             }else{
                 a=c;
                 c=x;
             }
         }
         return c;
    }
    
    int main(){
        cout<<quadratic_fit_search(-3,3,1,10)<<endl;
        return 0;
    }
    
    • 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

    4.3.3 改进

    4.3.1 4.3.1 4.3.1 的算法 迭代不保证最小值点在拟合抛物线所用的三个点构成的区间内。下面是改进的做法。

    对一元单谷函数 f ( x ) f(x) f(x) , 取三个点 a < b < c aa<b<c 使得 f ( a ) > f ( b ) f(a)>f(b) f(a)>f(b), f ( c ) > f ( b ) f(c)>f(b) f(c)>f(b)

    在迭代过程中要求每一步产生的新的 a < b < c aa<b<c 都 满足 f ( a ) > f ( b ) , f ( c ) > f ( b ) f(a)>f(b), f(c)>f(b) f(a)>f(b),f(c)>f(b) , 即最小值一定在 ( a , c ) (a, c) (a,c) 内。我们可以直接利用Lagrange插值公式写出二次多项 式,并用导数等于零解出多项式的最小值点,可得从 a , b , c a, b, c a,b,c 计算下一个点的迭代公式:
    x ∗ = 1 2 y a ( b 2 − c 2 ) + y b ( c 2 − a 2 ) + y c ( a 2 − b 2 ) y a ( b − c ) + y b ( c − a ) + y c ( a − b ) x^*=\frac{1}{2} \frac{y_a\left(b^2-c^2\right)+y_b\left(c^2-a^2\right)+y_c\left(a^2-b^2\right)}{y_a(b-c)+y_b(c-a)+y_c(a-b)} x=21ya(bc)+yb(ca)+yc(ab)ya(b2c2)+yb(c2a2)+yc(a2b2)
    产生 x ∗ x^* x 后,利用 a , b , c , x ∗ a, b, c, x^* a,b,c,x 这四个点中找到含最小值的缩小的区间并仍用 a , b , c a, b, c a,b,c 三个点表示。

    4.3.4 c++实现

    求解函数 f ( x ) = x 2 − 5 f(x)=x^2-5 f(x)=x25的最小值点。

    //
    // Created by CHH3213 on 2022/9/12.
    //
    #include
    #include 
    /*
     * 使用抛物线法求解 x*x-5的最小值点
     */
    using namespace std;
    
    #define DELTA 1e-4
    #define EPS 1e-6
    double func(double x){
        /**
         * 构造函数
         * x:自变量
         * return:函数值
         */
        return x*x-5;
    }
    
    double quadratic_fit_search_2(double a,double b,double c,int n){
        /**
         * 抛物线法;
         * 区间(a,c);
         * a
        while(n--){
            double y_a=func(a),y_b=func(b),y_c=func(c);
            double x = 0.5*(y_a*(b*b-c*c)+y_b*(c*c-a*a)+y_c*(a*a-b*b))/(y_a*(b-c)+y_b*(c-a)+y_c*(a-b));
            double y_x = func(x);
            if(x>b){
                 if(y_x>y_b){   //用 a < b < x 作为新的起点
                     c=x;
                 }else{     //用 b < x < c作为新的起点
                     a=b;
                     b=x;
                 }
            }else if(x<b){
                 if(y_x>y_b){   //用 x < b < c作为新的起点
                     a=x;
                 }else{     //用 a < x < b作为新的起点
                     c=b;
                     b=x;
                 }
            }
    
         }
        return b;
    
    }
    
    int main(){
        cout<<quadratic_fit_search_2(-2,2,3,50)<<endl;
        return 0;
    }
    
    • 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

    同样地,如果要求方程的根,构造损失函数,求损失函数的最小值点就是在求方程的根。

  • 相关阅读:
    springboot 集成GRPC
    lighthouse VERTEX50对超纯水水质监测
    Linux Driver优化S4 hibernate休眠速度
    如何构建高速公路智慧机房?这一点很重要
    二维码制作教程:如何制作一个文件二维码?
    图像直方图的基础知识
    Go 命令大全:全面解析与实践
    Springboot 对于数据库字段加密方案(此方案是对字符串处理的方案)
    Java:垃圾收集 CPU 统计信息
    Linux内核驱动开发-001字符设备开发-内核中断驱动独立按键
  • 原文地址:https://blog.csdn.net/weixin_42301220/article/details/126816206