Búsqueda ternaria
Se nos da una función que es unimodal en un intervalo . Por función unimodal entendemos uno de estos dos comportamientos:
-
La función crece estrictamente primero, alcanza un máximo (en un solo punto o en un intervalo) y después decrece estrictamente.
-
La función decrece estrictamente primero, alcanza un mínimo y después crece estrictamente.
En este artículo asumiremos el primer escenario. El segundo es completamente simétrico al primero.
La tarea es encontrar el máximo de la función en el intervalo .
Algoritmo
Consideremos cualesquiera 2 puntos y en este intervalo: . Evaluamos la función en y , es decir, encontramos los valores de y . Ahora tenemos una de tres opciones:
-
El máximo deseado no puede estar a la izquierda de , es decir, en el intervalo , porque o ambos puntos y o solo pertenecen a la zona donde la función crece. En cualquiera de los dos casos, esto significa que hay que buscar el máximo en el segmento .
-
Esta situación es simétrica a la anterior: el máximo no puede estar a la derecha de , es decir, en el intervalo , y el espacio de búsqueda se reduce al segmento .
-
Podemos ver que o ambos puntos pertenecen a la zona donde el valor de la función se maximiza, o está en la zona de valores crecientes y en la zona de valores decrecientes (acá usamos la estrictez del crecimiento/decrecimiento). Así, el espacio de búsqueda se reduce a . Para simplificar el código, este caso se puede combinar con cualquiera de los anteriores.
Así, a partir de la comparación de los valores en los dos puntos interiores, podemos reemplazar el intervalo actual por un intervalo nuevo, más corto, . Aplicando repetidamente el procedimiento descrito al intervalo, podemos obtener un intervalo arbitrariamente corto. Eventualmente, su longitud será menor que una constante predefinida (precisión), y el proceso se puede detener. Este es un método numérico, así que podemos asumir que después de eso la función alcanza su máximo en todos los puntos del último intervalo . Sin pérdida de generalidad, podemos tomar como valor de retorno.
No impusimos ninguna restricción sobre la elección de los puntos y . Esa elección definirá la tasa de convergencia y la precisión de la implementación. La forma más común es elegir los puntos de modo que dividan el intervalo en tres partes iguales. Así, tenemos
Si y se eligen más cerca uno del otro, la tasa de convergencia aumenta un poco.
Análisis del tiempo de ejecución
Se puede visualizar así: cada vez que evaluamos la función en los puntos y , esencialmente ignoramos alrededor de un tercio del intervalo, el izquierdo o el derecho. Así, el tamaño del espacio de búsqueda es del original.
Aplicando el teorema maestro , obtenemos la estimación de complejidad deseada.
El caso de argumentos enteros
Si toma un parámetro entero, el intervalo se vuelve discreto. Como no impusimos restricciones sobre la elección de y , la corrección del algoritmo no se ve afectada. y se pueden seguir eligiendo para dividir en 3 partes aproximadamente iguales.
La diferencia aparece en el criterio de parada del algoritmo. La búsqueda ternaria tendrá que detenerse cuando , porque en ese caso ya no podemos elegir y distintos entre sí y de y , y eso puede causar un bucle infinito. Una vez que , el conjunto restante de puntos candidatos hay que chequearlo para encontrar el punto que produce el valor máximo .
Búsqueda de la sección áurea
En algunos casos computar puede ser bastante lento, pero reducir el número de iteraciones es inviable por temas de precisión. Por suerte, es posible computar solo una vez en cada iteración (excepto la primera).
Para ver cómo, volvamos al método de selección de y . Supongamos que seleccionamos y en de modo que donde es alguna constante. Para reducir la cantidad de cómputo, queremos elegir un tal que en la siguiente iteración uno de los nuevos puntos de evaluación , coincida con o , para reutilizar el valor de la función ya computado.
Ahora supongamos que después de la iteración actual ponemos . Entonces el punto satisfará . Queremos que este punto coincida con , es decir, .
Multiplicando ambos lados de por obtenemos . Nótese que y . Sustituyendo eso y multiplicando por , obtenemos la siguiente ecuación:
Esta es la conocida ecuación de la sección áurea. Resolverla da . Como debe ser positivo, obtenemos . Aplicando la misma lógica al caso en que ponemos y queremos que coincida con , obtenemos el mismo valor de . Así, si elegimos y , en cada iteración podemos reutilizar uno de los valores computados en la iteración anterior.
Implementación
double ternary_search(double l, double r) {
double eps = 1e-9; //set the error limit here
while (r - l > eps) {
double m1 = l + (r - l) / 3;
double m2 = r - (r - l) / 3;
double f1 = f(m1); //evaluates the function at m1
double f2 = f(m2); //evaluates the function at m2
if (f1 < f2)
l = m1;
else
r = m2;
}
return f(l); //return the maximum of f(x) in [l, r]
}Acá eps es de hecho el error absoluto (sin tener en cuenta errores por el cálculo inexacto de la función).
En lugar del criterio r - l > eps, podemos elegir un número constante de iteraciones como criterio de parada. El número de iteraciones debería elegirse para asegurar la precisión requerida. Típicamente, en la mayoría de los challenges de programación el límite de error es y por lo tanto 200 - 300 iteraciones alcanzan. Además, el número de iteraciones no depende de los valores de y , así que el número de iteraciones corresponde al error relativo requerido.
Problemas de práctica
- Codeforces - New Bakery
- Codechef - Race time
- Hackerearth - Rescuer
- Spoj - Building Construction
- Codeforces - Weakness and Poorness
- LOJ - Closest Distance
- GYM - Dome of Circus (D)
- UVA - Galactic Taxes
- GYM - Chasing the Cheetahs (A)
- UVA - 12197 - Trick or Treat
- SPOJ - Building Construction
- Codeforces - Devu and his Brother
- Codechef - Is This JEE
- Codeforces - Restorer Distance
- TIMUS 1058 Chocolate
- TIMUS 1436 Billboard
- TIMUS 1451 Beerhouse Tale
- TIMUS 1719 Kill the Shaitan-Boss
- TIMUS 1913 Titan Ruins: Alignment of Forces