C++ 近似搜索的工作原理

C++ 近似搜索的工作原理,c++,math,approximation,C++,Math,Approximation,[序言] 这个问答旨在更清楚地解释我第一次在这里发布的近似搜索类的内部工作 已经有几次(出于各种原因)要求我提供关于这方面的更详细信息,因此我决定写一篇关于这方面的Q&A风格的文章,我以后可以很容易地参考,不需要反复解释 [问题] 如何在实域(double)中近似值/参数,以实现多项式拟合、参数函数拟合或求解(困难的)方程(如超越) 限制 实域(doubleprecision) C++语言 可配置的近似精度 已知搜索间隔 拟合值/参数不是严格单调的或根本不是函数 近似搜索 这类似于二进制

[序言]

这个问答旨在更清楚地解释我第一次在这里发布的近似搜索类的内部工作

已经有几次(出于各种原因)要求我提供关于这方面的更详细信息,因此我决定写一篇关于这方面的Q&A风格的文章,我以后可以很容易地参考,不需要反复解释

[问题]

如何在实域(
double
)中近似值/参数,以实现多项式拟合、参数函数拟合或求解(困难的)方程(如超越)

限制

  • 实域(
    double
    precision)
  • C++语言
  • 可配置的近似精度
  • 已知搜索间隔
  • 拟合值/参数不是严格单调的或根本不是函数

近似搜索

这类似于二进制搜索,但不受其限制,即在共享
O(log(n))
复杂性时,搜索的函数/值/参数必须是严格单调函数

例如,假设以下问题

我们已经知道函数
y=f(x)
,想要找到
x0
,这样
y0=f(x0)
。这基本上可以通过对
f
的反函数来实现,但有许多函数我们不知道如何计算对它的反函数。那么在这种情况下如何计算呢

knowns

  • y=f(x)
    -输入功能
  • y0
    -想要的点
    y
  • a0、a1
    -解决方案
    x
    间隔范围
未知数

  • x0
    -想要的点
    x
    值必须在范围
    x0内=

    左侧显示了初始搜索(项目符号#1、#2、#3、#4)。在右侧的下一个递归搜索(bullet#5)。这将递归循环,直到达到所需的精度(递归次数)。每次递归都会将精确度提高10倍(
    0.1*da
    )。灰色垂直线表示探测的
    x(i)

    <强>这里的C++源代码:>/P>

    //---------------------------------------------------------------------------
    //--- approx ver: 1.01 ------------------------------------------------------
    //---------------------------------------------------------------------------
    #ifndef _approx_h
    #define _approx_h
    #include <math.h>
    //---------------------------------------------------------------------------
    class approx
        {
    public:
        double a,aa,a0,a1,da,*e,e0;
        int i,n;
        bool done,stop;
    
        approx()            { a=0.0; aa=0.0; a0=0.0; a1=1.0; da=0.1; e=NULL; e0=NULL; i=0; n=5; done=true; }
        approx(approx& a)   { *this=a; }
        ~approx()           {}
        approx* operator = (const approx *a) { *this=*a; return this; }
        //approx* operator = (const approx &a) { ...copy... return this; }
    
        void init(double _a0,double _a1,double _da,int _n,double *_e)
            {
            if (_a0<=_a1) { a0=_a0; a1=_a1; }
            else          { a0=_a1; a1=_a0; }
            da=fabs(_da);
            n =_n ;
            e =_e ;
            e0=-1.0;
            i=0; a=a0; aa=a0;
            done=false; stop=false;
            }
        void step()
            {
            if ((e0<0.0)||(e0>*e)) { e0=*e; aa=a; }         // better solution
            if (stop)                                       // increase accuracy
                {
                i++; if (i>=n) { done=true; a=aa; return; } // final solution
                a0=aa-fabs(da);
                a1=aa+fabs(da);
                a=a0; da*=0.1;
                a0+=da; a1-=da;
                stop=false;
                }
            else{
                a+=da; if (a>a1) { a=a1; stop=true; }       // next point
                }
            }
        };
    //---------------------------------------------------------------------------
    #endif
    //---------------------------------------------------------------------------
    
    approx aa;
    double ee,x,y,x0,y0=here_your_known_value;
    //            a0,  a1, da,n, ee  
    for (aa.init(0.0,10.0,0.1,6,&ee); !aa.done; aa.step())
        {
        x = aa.a;        // this is x(i)
        y = f(x)         // here compute the y value for whatever you want to fit
        ee = fabs(y-y0); // compute error of solution for the approximation search
        }
    
    在上面的rem中,
    for(aa.init(…
    是命名的操作数。
    a0,a1
    是探测
    x(i)
    的间隔,
    da
    x(i)之间的初始步骤
    n
    是递归数。因此,如果
    n=6
    da=0.1
    x
    fit的最终最大误差将是
    ~0.1/10^6=0.0000001
    &ee
    是指向将计算实际误差的变量的指针。我选择了指针,因此嵌套时不会发生冲突,对于s也是如此将peed作为参数传递给频繁使用的函数会创建堆垃圾

    [notes]

    此近似搜索可以嵌套到任何维度(但对于粗略搜索,您需要注意速度),请参见一些示例

    在非函数拟合和需要获得“所有”解的情况下,可以在找到解后使用搜索间隔的递归细分来检查另一个解。参见示例:

    你应该注意什么?

    您必须仔细选择搜索间隔
    ,使其包含解决方案,但不会太宽(否则会太慢)。此外,初始步骤
    da
    非常重要,如果搜索间隔太大,您可能会错过局部最小/最大解,或者如果搜索间隔太小,则搜索速度会太慢(特别是对于嵌套多维拟合).

    结合(使用,但请参见底部的更正)和方法会更好:

    我们通过割线找到根近似值,并将根括在括号中,如同在二等分中一样

    始终保持间隔的两条边,以便一条边上的增量为负,另一条边上的增量为正,从而保证根位于内部;并且使用割线方法代替减半

    伪代码:

    given a function f
    given two points a, b, such that a < b and sign(f(a)) /= sign(f(b))
    given tolerance tol
    find root z of f such that abs(f(z)) < tol     -- stop_condition
    
    DO:
        x = root of f by linear interpolation of f between a and b
        m = midpoint between a and b
    
        if stop_condition holds at x or m, set z and STOP
    
        [a,b] := [a,x,m,b].sort.choose_shortest_interval_with_
                                       _opposite_signs_at_its_ends
    
    给定一个函数f
    给定两点a,b,使得a
    很明显,这将使间隔减少一半,或者在每次迭代中做得更好;因此,除非函数的行为非常糟糕(比如说,
    sin(1/x)
    接近
    x=0
    ),否则这将非常快地收敛,对于每个迭代步骤,最多只需对
    f
    进行两次求值

    我们可以通过检查
    b-a
    是否变得太小来检测不良行为案例(特别是当我们以有限的精度工作时,如双倍)


    更新:这实际上是一种带括号的割线方法,如上面的伪代码所述。在平分中增加中间点可以确保即使在最病态的情况下也会收敛。

    Dippy问题:为什么Runge-Kutta或Newton-Raphson方法在这里不适用?@ScottM它们不局限于函数吗?@Spektre:如果你在计算一阶导数(δy),那么通过Runge-Kutta积分或通过Newton-Raphson近似可能是一个更好的选择。还有自适应步长变量。我投票将这个问题作为离题题来结束,因为问答也是如此,而不是讨论。“非单调”很好;“不是函数”毫无意义
    approx aa;
    double ee,x,y,x0,y0=here_your_known_value;
    //            a0,  a1, da,n, ee  
    for (aa.init(0.0,10.0,0.1,6,&ee); !aa.done; aa.step())
        {
        x = aa.a;        // this is x(i)
        y = f(x)         // here compute the y value for whatever you want to fit
        ee = fabs(y-y0); // compute error of solution for the approximation search
        }
    
    given a function f
    given two points a, b, such that a < b and sign(f(a)) /= sign(f(b))
    given tolerance tol
    find root z of f such that abs(f(z)) < tol     -- stop_condition
    
    DO:
        x = root of f by linear interpolation of f between a and b
        m = midpoint between a and b
    
        if stop_condition holds at x or m, set z and STOP
    
        [a,b] := [a,x,m,b].sort.choose_shortest_interval_with_
                                       _opposite_signs_at_its_ends