Cómo funciona la búsqueda de aproximación
[Prólogo]
Estas preguntas y respuestas están destinadas a explicar más claramente el funcionamiento interno de mi clase de búsqueda de aproximaciones que publiqué por primera vez aquí.
- Aumento de la precisión de la solución de la ecuación trascendental.
Ya me pidieron información más detallada sobre esto varias veces (por varias razones), así que decidí escribir un tema de estilo de preguntas y respuestas sobre esto al que puedo hacer referencia fácilmente en el futuro y no necesito explicarlo una y otra vez.
[Pregunta]
¿Cómo aproximar valores/parámetros en el dominio real ( double
) para lograr el ajuste de polinomios, funciones paramétricas o resolver ecuaciones (difíciles) (como trascendentales)?
Restricciones
- dominio real (
double
precisión) - lenguaje C++
- precisión de aproximación configurable
- intervalo conocido para la búsqueda
- El valor/parámetro ajustado no es estrictamente monótono o no funciona en absoluto.
búsqueda de aproximación
Esto es una analogía con la búsqueda binaria, pero sin sus restricciones de que la función/valor/parámetro buscado debe ser una función estrictamente monótona y al mismo tiempo compartir la O(log(n))
complejidad.
Por ejemplo, supongamos el siguiente problema.
Hemos conocido la función y=f(x)
y queremos encontrar x0
tal que y0=f(x0)
. Esto se puede hacer básicamente mediante una función inversa, f
pero hay muchas funciones que no sabemos cómo calcularla inversamente. Entonces, ¿cómo calcular esto en tal caso?
sabe
y=f(x)
- función de entraday0
-y
valor del punto deseadoa0,a1
-x
rango de intervalo de solución
Desconocidos
x0
- el valor del punto deseadox
debe estar dentro del rangox0=<a0,a1>
Algoritmo
Sondee algunos puntos
x(i)=<a0,a1>
uniformemente dispersos a lo largo del rango con algún paso.da
Así por ejemplo
x(i)=a0+i*da
dondei={ 0,1,2,3... }
para cada uno
x(i)
calcular la distancia/erroree
dely=f(x(i))
Esto se puede calcular, por ejemplo, de esta manera:
ee=fabs(f(x(i))-y0)
pero también se puede utilizar cualquier otra métrica.recordar el punto
aa=x(i)
con distancia/error mínimoee
detenerse cuando
x(i)>a1
aumentar recursivamente la precisión
así que primero restrinja el rango para buscar solo alrededor de la solución encontrada, por ejemplo:
a0'=aa-da; a1'=aa+da;
luego aumente la precisión de la búsqueda reduciendo el paso de búsqueda:
da'=0.1*da;
si
da'
no es demasiado pequeño o si no se alcanza el número máximo de recursiones, vaya al n.° 1la solución encontrada está en
aa
Esto es lo que tengo en mente:
En el lado izquierdo se ilustra la búsqueda inicial (puntos #1,#2,#3,#4 ). En el lado derecho, la siguiente búsqueda recursiva (viñeta n.° 5 ). Esto se repetirá de forma recursiva hasta alcanzar la precisión deseada (número de recursiones). Cada recursión aumenta los 10
tiempos de precisión ( 0.1*da
). Las líneas verticales grises representan x(i)
puntos sondeados.
Aquí el código fuente C++ para esto:
//---------------------------------------------------------------------------
//--- 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
//---------------------------------------------------------------------------
Así es como se usa:
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
}
en el rem de arriba for (aa.init(...
está el operando nombrado. Es a0,a1
el intervalo en el que se x(i)
prueba, da
es el paso inicial entre x(i)
y n
es el número de recursiones. entonces si n=6
y da=0.1
el error de ajuste máximo final x
será ~0.1/10^6=0.0000001
. Es &ee
un puntero a la variable donde se calculará el error real. Elijo el puntero para que no haya colisiones al anidar esto y también para la velocidad, ya que pasar un parámetro a una función muy utilizada crea un montón de basura.
[notas]
Esta búsqueda de aproximación se puede anidar en cualquier dimensionalidad (pero, en general, hay que tener cuidado con la velocidad). Vea algunos ejemplos.
- Aproximación de n puntos a la curva con mejor ajuste
- Ajuste de curva con puntos y en posiciones x repetidas (brazos Galaxy Spiral)
- Aumento de la precisión de la solución de la ecuación trascendental.
- Encuentre una elipse de área mínima que encierre un conjunto de puntos en C ++
- 2D TDoA Diferencia horaria de llegada
- Diferencia horaria de llegada 3D TDoA
En caso de que no se ajuste la función y sea necesario obtener "todas" las soluciones, puede utilizar la subdivisión recursiva del intervalo de búsqueda después de encontrar la solución para buscar otra solución. Ver ejemplo:
- Dada una coordenada X, ¿cómo calculo la coordenada Y de un punto para que descanse sobre una curva de Bézier?
¿Qué debes tener en cuenta?
hay que elegir cuidadosamente el intervalo de búsqueda <a0,a1>
para que contenga la solución pero no sea demasiado amplio (o sería lento). Además, el paso inicial da
es muy importante, si es demasiado grande, se pueden perder las soluciones locales mínimas y máximas o, si es demasiado pequeño, el proceso se volverá demasiado lento (especialmente para ajustes multidimensionales anidados).
Una combinación de secante (con corchetes , pero vea la corrección en la parte inferior) y método de bisección es mucho mejor (el crédito por los gráficos originales, por supuesto, se debe al usuario Spektre en su respuesta anterior):
Encontramos aproximaciones de raíces mediante secantes y mantenemos la raíz entre corchetes como en la bisección.
mantenga siempre los dos bordes del intervalo de modo que el delta en un borde sea negativo y en el otro sea positivo, por lo que se garantiza que la raíz estará dentro; y en lugar de reducir a la mitad, utilice el método de la secante.
Pseudocódigo:
Given a function f,
Given two points a, b, such that a < b and sign(f(a)) /= sign(f(b)),
Given tolerance tol,
TO 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
Obviamente, esto reduce a la mitad el intervalo [a,b]
, o incluso lo hace mejor, en cada iteración; entonces, a menos que la función se comporte extremadamente mal (como, por ejemplo, sin(1/x)
near x=0
), esto convergerá muy rápidamente, tomando solo dos evaluaciones de f
como máximo, para cada paso de iteración.
Y podemos detectar los casos de mal comportamiento comprobando que b-a
no sea demasiado pequeño (especialmente si trabajamos con precisión finita, como en dobles).
actualización: aparentemente este es en realidad un método de doble posición falsa , que es secante con corchetes, como se describe en el pseudocódigo anterior. Aumentarlo en el punto medio como en la bisección asegura la convergencia incluso en los casos más patológicos.