Una vez recibí lo siguiente como pregunta de entrevista:
Estoy pensando en un número entero positivo n. Cree un algoritmo que pueda adivinarlo en consultas O (lg n). Cada consulta es un número de su elección y responderé "más bajo", "más alto" o "correcto".
Este problema puede resolverse mediante una búsqueda binaria modificada, en la que enumera potencias de dos hasta encontrar una que exceda n, luego ejecuta una búsqueda binaria estándar sobre ese rango. Lo que creo que es tan bueno de esto es que puedes buscar un número en particular en un espacio infinito más rápido que solo la fuerza bruta.
Sin embargo, la pregunta que tengo es una pequeña modificación de este problema. En lugar de elegir un número entero positivo, suponga que elijo un número racional arbitrario entre cero y uno. Mi pregunta es: ¿qué algoritmo puede usar para determinar de manera más eficiente qué número racional he elegido?
En este momento, la mejor solución que tengo puede encontrar p / q en un tiempo máximo de O (q) caminando implícitamente en el árbol Stern-Brocot , un árbol de búsqueda binaria sobre todos los racionales. Sin embargo, esperaba conseguir un tiempo de ejecución más cercano al tiempo de ejecución que obtuvimos para el caso de números enteros, tal vez algo como O (lg (p + q)) u O (lg pq). ¿Alguien sabe alguna forma de obtener este tipo de tiempo de ejecución?
Inicialmente consideré usar una búsqueda binaria estándar del intervalo [0, 1], pero esto solo encontrará números racionales con una representación binaria no repetida, que pierde casi todos los racionales. También pensé en usar alguna otra forma de enumerar los racionales, pero parece que no puedo encontrar una manera de buscar en este espacio dadas solo comparaciones mayores / iguales / menores.
fuente
Respuestas:
Bien, aquí está mi respuesta usando solo fracciones continuas .
Primero, obtengamos algo de terminología aquí.
Sea X = p / q la fracción desconocida.
Deje que Q (X, p / q) = sign (X - p / q) sea la función de consulta: si es 0, hemos adivinado el número, y si es +/- 1 que nos dice el signo de nuestro error .
La notación convencional para fracciones continuas es A = [a 0 ; a 1 , a 2 , a 3 , ... a k ]
= a 0 + 1 / (a 1 + 1 / (a 2 + 1 / (a 3 + 1 / (... + 1 / a k ) ...)))
Seguiremos el siguiente algoritmo para 0 <p / q <1.
Inicializar Y = 0 = [0], Z = 1 = [1], k = 0.
Bucle externo : las condiciones previas son las siguientes:
Y y Z son fracciones continuas de k + 1 términos que son idénticos excepto en el último elemento, donde difieren en 1, de modo que Y = [y 0 ; y 1 , y 2 , y 3 , ... y k ] y Z = [y 0 ; y 1 , y 2 , y 3 , ... y k + 1]
(-1) k (YX) <0 <(-1) k (ZX), o en términos más simples, para k par, Y <X <Z y para k impar, Z <X <Y.
Extiende el grado de la fracción continua en 1 paso sin cambiar los valores de los números. En general, si los últimos términos son y k y y k + 1, lo cambiamos a [... y k , y k + 1 = ∞] y [... y k , z k + 1 = 1]. Ahora aumente k en 1.
Bucles internos : esto es esencialmente lo mismo que la pregunta de la entrevista de @ templatetypedef sobre los números enteros. Hacemos una búsqueda binaria de dos fases para acercarnos:
Bucle interior 1 : y k = ∞, z k = a, y X está entre Y y Z.
Último término de Double Z: Calcule M = Z pero con m k = 2 * a = 2 * z k .
Consultar el número desconocido: q = Q (X, M).
Si q = 0, tenemos nuestra respuesta y vamos al paso 17.
Si q y Q (X, Y) tienen signos opuestos, significa que X está entre Y y M, así que configure Z = M y vaya al paso 5.
De lo contrario, configure Y = M y vaya al siguiente paso:
Bucle interior 2. y k = b, z k = a, y X está entre Y y Z.
Si ayb difieren en 1, intercambie Y y Z, vaya al paso 2.
Realice una búsqueda binaria: calcule M donde m k = piso ((a + b) / 2, y consulte q = Q (X, M).
Si q = 0, terminamos y vamos al paso 17.
Si q y Q (X, Y) tienen signos opuestos, significa que X está entre Y y M, así que configure Z = M y vaya al paso 11.
De lo contrario, q y Q (X, Z) tienen signos opuestos, significa que X está entre Z y M, así que configure Y = M y vaya al paso 11.
Hecho: X = M.
Un ejemplo concreto para X = 16/113 = 0.14159292
En cada paso del cálculo de M, el rango del intervalo se reduce. Probablemente sea bastante fácil probar (aunque no haré esto) que el intervalo se reduce en un factor de al menos 1 / sqrt (5) en cada paso, lo que mostraría que este algoritmo es O (log q) pasos.
Tenga en cuenta que esto puede combinarse con la pregunta de entrevista original de templatetypedef y aplicarse a cualquier número racional p / q, no solo entre 0 y 1, calculando primero Q (X, 0), luego para enteros positivos / negativos, delimitando entre dos enteros, y luego usando el algoritmo anterior para la parte fraccionaria.
La próxima vez que tenga la oportunidad, publicaré un programa de Python que implemente este algoritmo.
editar : también, tenga en cuenta que no tiene que calcular la fracción continua en cada paso (que sería O (k), hay aproximaciones parciales a las fracciones continuas que pueden calcular el siguiente paso del paso anterior en O (1). )
edición 2 : Definición recursiva de aproximaciones parciales:
Si A k = [a 0 ; a 1 , a 2 , a 3 , ... a k ] = p k / q k , entonces p k = a k p k-1 + p k-2 , y q k = a k q k-1 + q k-2 . (Fuente: Niven & Zuckerman, 4th ed, Theorems 7.3-7.5. Ver también Wikipedia )
Ejemplo: [0] = 0/1 = p 0 / q 0 , [0; 7] = 1/7 = p 1 / q 1 ; entonces [0; 7, 16] = (16 * 1 + 0) / (16 * 7 + 1) = 16/113 = p 2 / q 2 .
Esto significa que si dos fracciones continuas Y y Z tienen los mismos términos excepto el último, y la fracción continua que excluye el último término es p k-1 / q k-1 , entonces podemos escribir Y = (y k p k- 1 + p k-2 ) / (y k q k-1 + q k-2 ) y Z = (z k p k-1 + p k-2 ) / (z k q k-1 + q k-2 ). Debería ser posible demostrar a partir de esto que | YZ | disminuye en al menos un factor de 1 / sqrt (5) en cada intervalo más pequeño producido por este algoritmo, pero el álgebra parece estar más allá de mí en este momento. :-(
Aquí está mi programa Python:
y una salida de muestra para
ratguess(makeQ(33102,113017), True, 20)
:Dado que Python maneja matemáticas de números enteros desde el principio, y este programa solo usa matemáticas de números enteros (excepto para los cálculos de intervalo), debería funcionar para racionales arbitrarias.
edición 3 : Esquema de la prueba de que esto es O (log q), no O (log ^ 2 q):
Primero tenga en cuenta que hasta que se encuentre el número racional, el # de pasos n k para cada nuevo término de fracción continua es exactamente 2b (a_k) -1 donde b (a_k) es el # de bits necesarios para representar a_k = ceil (log2 (a_k )): son b (a_k) pasos para ampliar la "red" de la búsqueda binaria, y b (a_k) -1 pasos para reducirla). Vea el ejemplo anterior, observará que el número de pasos es siempre 1, 3, 7, 15, etc.
Ahora podemos usar la relación de recurrencia q k = a k q k-1 + q k-2 y la inducción para demostrar el resultado deseado.
Digámoslo de esta manera: que el valor de q después de los N k = suma (n k ) pasos requeridos para alcanzar el k-ésimo término tiene un mínimo: q> = A * 2 cN para algunas constantes fijas A, c. (para invertir, obtendríamos que el # de pasos N es <= (1 / c) * log 2 (q / A) = O (log q).)
Casos base:
Esto implica que A = 1, c = 1/2 podría proporcionar los límites deseados. En realidad, q no puede duplicar cada término (contraejemplo: [0; 1, 1, 1, 1, 1] tiene un factor de crecimiento de phi = (1 + sqrt (5)) / 2) así que usemos c = 1 / 4.
Inducción:
para el término k, q k = a k q k-1 + q k-2 . Nuevamente, para los n k = 2b-1 pasos necesarios para este término, a k > = 2 b-1 = 2 (n k -1) / 2 .
Entonces a k q k-1 > = 2 (N k -1) / 2 * q k-1 > = 2 (n k -1) / 2 * A * 2 N k-1 /4 = A * 2 N k / 4 / raíz cuadrada (2) * 2 n k / 4 .
Argh, la parte difícil aquí es que si a k = 1, es posible que q no aumente mucho para ese término, y necesitamos usar q k-2 pero eso puede ser mucho más pequeño que q k-1 .
fuente
Tomemos los números racionales, en forma reducida, y escribamos primero en el orden del denominador, luego del numerador.
Nuestra primera suposición será
1/2
. Luego seguiremos la lista hasta que tengamos 3 en nuestro rango. Luego, realizaremos 2 intentos para buscar en esa lista. Luego seguiremos la lista hasta que tengamos 7 en nuestro rango restante. Luego, realizaremos 3 intentos para buscar en esa lista. Y así.En
n
pasos, cubriremos las primeras posibilidades, que se encuentran en el orden de magnitud de eficiencia que estaba buscando.2O(n)
Actualización: la gente no entendió el razonamiento detrás de esto. El razonamiento es simple. Sabemos cómo caminar por un árbol binario de manera eficiente. Hay fracciones con máximo denominador . Por lo tanto, podríamos buscar hasta cualquier tamaño de denominador particular en pasos. El problema es que tenemos un número infinito de posibles racionales para buscar. Así que no podemos simplemente alinearlos todos, ordenarlos y comenzar a buscar.
O(n2)
n
O(2*log(n)) = O(log(n))
Por lo tanto, mi idea era alinear algunos, buscar, alinear más, buscar, etc. Cada vez que hacemos más fila, nos alineamos aproximadamente el doble de lo que hicimos la última vez. Así que necesitamos una suposición más que la última vez. Por lo tanto, nuestra primera pasada usa 1 suposición para recorrer 1 posible racional. Nuestro segundo usa 2 conjeturas para atravesar 3 posibles racionales. Nuestro tercero usa 3 conjeturas para atravesar 7 posibles racionales. Y nuestro
k
'th usak
conjeturas para atravesar posibles racionales. Para cualquier racional en particular , eventualmente terminará colocando ese racional en una lista bastante grande en la que sabe cómo hacer una búsqueda binaria de manera eficiente.2k-1
m/n
Si hiciéramos búsquedas binarias, luego ignoramos todo lo que aprendimos cuando obtenemos más racionales, entonces pondríamos todos los racionales hasta e inclusive
m/n
enO(log(n))
pases. (Eso es porque en ese punto llegaremos a un paso con suficientes racionales para incluir cada racional hasta e inclusivem/n
). Pero cada paso requiere más conjeturas, por lo que serían conjeturas.O(log(n)2)
Sin embargo, lo hacemos mucho mejor que eso. Con nuestra primera suposición, eliminamos la mitad de los racionales de nuestra lista por ser demasiado grandes o pequeños. Nuestras siguientes dos conjeturas no cortan el espacio en cuartos, pero no se alejan demasiado de él. Nuestras siguientes 3 conjeturas nuevamente no cortan el espacio en octavos, pero no se alejan demasiado de él. Y así. Cuando lo juntas, estoy convencido de que el resultado es que encuentras
m/n
enO(log(n))
pasos. Aunque en realidad no tengo una prueba.Pruébelo: aquí hay un código para generar las conjeturas para que pueda jugar y ver qué tan eficiente es.
Como ejemplo para probarlo, probé 101/1024 (0.0986328125) y descubrí que se necesitaron 20 conjeturas para encontrar la respuesta. Probé 0.98765 y me tomó 45 intentos. Probé 0.0123456789 y necesité 66 conjeturas y alrededor de un segundo para generarlas. (Tenga en cuenta que si llama al programa con un número racional como argumento, completará todas las suposiciones por usted. Esta es una conveniencia muy útil).
fuente
1/2
. Supongamos que la respuesta vuelve más baja. Los siguientes 3 números de la lista que se ajustan a la condición son1/3
,1/4
y1/5
. Así que adivinamos a1/4
continuación, luego1/3
o1/5
en la siguiente suposición. Si continuamos, tomamos 7 números en nuestro rango y configuramos las siguientes 3 conjeturas. Después de eso, tomaremos 15 y configuraremos las siguientes 4 conjeturas. etc. ¿Qué no está claro sobre eso? Me voy a la cama ahora. Si todavía no lo entiendes por la mañana, escribiré un programa para hacer las adivinanzas y podrás ver cómo funciona.¡Lo tengo! Lo que debe hacer es utilizar una búsqueda paralela con bisección y fracciones continuas .
La bisección le dará un límite hacia un número real específico, representado como una potencia de dos, y las fracciones continuas tomarán el número real y encontrarán el número racional más cercano.
La forma en que los ejecuta en paralelo es la siguiente.
En cada paso, tienes
l
yu
siendo los límites superior e inferior de la bisección. La idea es que puede elegir entre dividir a la mitad el rango de bisección y agregar un término adicional como una representación de fracción continua. Cuando ambosl
yu
tienen el mismo término siguiente como fracción continua, entonces da el siguiente paso en la búsqueda de fracción continua y realiza una consulta utilizando la fracción continua. De lo contrario, reduce a la mitad el rango mediante bisección.Dado que ambos métodos aumentan el denominador en al menos un factor constante (la bisección va por factores de 2, las fracciones continuas van por al menos un factor de phi = (1 + sqrt (5)) / 2), esto significa que su búsqueda debe ser O (registro (q)). (Puede haber cálculos repetidos de fracciones continuas, por lo que puede terminar como O (log (q) ^ 2).)
Nuestra búsqueda continua de fracciones debe redondearse al número entero más cercano, no usar piso (esto es más claro a continuación).
Lo anterior es algo ondulado. Usemos un ejemplo concreto de r = 1/31:
¡ÉXITO!
Para otro ejemplo, usemos 16/113 (= 355/113 - 3 donde 355/113 está bastante cerca de pi).
[para continuar, tengo que ir a algún lado]
Reflexionando más, las fracciones continuas son el camino a seguir, sin importar la bisección, excepto para determinar el siguiente término. Más cuando vuelva.
fuente
l
yu
tener la misma CF hasta un cierto punto, no implica necesariamente que el número al que está adivinando también tiene el mismo convergente ... (si he entendido su enfoque correctamente).Creo que encontré un algoritmo O (log ^ 2 (p + q)).
Para evitar confusiones en el siguiente párrafo, una "consulta" se refiere a cuando el adivino le da al retador una suposición y el retador responde "más grande" o "más pequeño". Esto me permite reservar la palabra "adivinar" para otra cosa, una suposición para p + q que no se le pide directamente al retador.
La idea es encontrar primero p + q, usando el algoritmo que describe en su pregunta: adivine un valor k, si k es demasiado pequeño, duplíquelo y vuelva a intentarlo. Luego, una vez que tenga un límite superior e inferior, realice una búsqueda binaria estándar. Esto requiere consultas O (log (p + q) T), donde T es un límite superior para el número de consultas necesarias para verificar una suposición. Busquemos a T.
Queremos comprobar todas las fracciones r / s con r + s <= k, y duplicar k hasta que k sea lo suficientemente grande. Tenga en cuenta que hay O (k ^ 2) fracciones que debe verificar para un valor dado de k. Construya un árbol de búsqueda binario balanceado que contenga todos estos valores, luego búsquelo para determinar si p / q está en el árbol. Se necesitan consultas O (log k ^ 2) = O (log k) para confirmar que p / q no está en el árbol.
Nunca adivinaremos un valor de k mayor que 2 (p + q). Por tanto, podemos tomar T = O (log (p + q)).
Cuando adivinamos el valor correcto de k (es decir, k = p + q), enviaremos la consulta p / q al retador en el curso de verificar nuestra suposición de k, y ganaremos el juego.
El número total de consultas es entonces O (log ^ 2 (p + q)).
fuente
De acuerdo, creo que descubrí un algoritmo O (lg 2 q) para este problema que se basa en el conocimiento más excelente de Jason S sobre el uso de fracciones continuas. Pensé en desarrollar el algoritmo hasta el final aquí para que tengamos una solución completa, junto con un análisis de tiempo de ejecución.
La intuición detrás del algoritmo es que cualquier número racional p / q dentro del rango se puede escribir como
Para las elecciones adecuadas de un i . A esto se le llama fracción continua . Más importante aún, aunque estos a i se pueden derivar ejecutando el algoritmo euclidiano en el numerador y denominador. Por ejemplo, supongamos que queremos representar 11/14 de esta manera. Comenzamos señalando que 14 entra en once cero veces, por lo que una aproximación burda de 11/14 sería
Ahora, suponga que tomamos el recíproco de esta fracción para obtener 14/11 = 1 3 / 11 . Entonces si escribimos
Obtenemos una aproximación ligeramente mejor al 14/11. Ahora que nos quedamos con 3/11, podemos tomar el recíproco nuevamente para obtener 11/3 = 3 2 / 3 , por lo que podemos considerar
Que es otra buena aproximación al 14/11. Ahora, tenemos 2/3, por lo que considerar el recíproco, que es 3/2 = 1 1 / 2 . Si luego escribimos
Obtenemos otra buena aproximación al 14/11. Finalmente, nos queda 1/2, cuyo recíproco es 2/1. Si finalmente escribimos
que es exactamente la fracción que queríamos. Además, mire la secuencia de coeficientes que terminamos usando. Si ejecuta el algoritmo euclidiano extendido en 11 y 14, obtiene eso
Resulta que (¡usando más matemáticas de las que actualmente sé cómo hacer!) Que esto no es una coincidencia y que los coeficientes en la fracción continua de p / q siempre se forman usando el algoritmo euclidiano extendido. Esto es genial, porque nos dice dos cosas:
Dados estos dos hechos, podemos idear un algoritmo para recuperar cualquier número racional p / q, no solo aquellos entre 0 y 1, aplicando el algoritmo general para adivinar enteros arbitrarios n uno a la vez para recuperar todos los coeficientes en la fracción continua para p / q. Por ahora, sin embargo, solo nos preocuparemos por los números en el rango (0, 1], ya que la lógica para manejar números racionales arbitrarios se puede hacer fácilmente dado esto como una subrutina.
Como primer paso, supongamos que queremos encontrar el mejor valor de a 1 para que 1 / a 1 esté lo más cerca posible de p / qy que 1 sea un número entero. Para hacer esto, simplemente podemos ejecutar nuestro algoritmo para adivinar enteros arbitrarios, tomando el recíproco cada vez. Después de hacer esto, habrá sucedido una de dos cosas. Primero, podríamos descubrir por pura coincidencia que p / q = 1 / k para algún número entero k, en cuyo caso hemos terminado. De lo contrario, encontraremos que p / q se intercala entre 1 / (a 1 - 1) y 1 / a 0 para algunos a 1 . Cuando hacemos esto, comenzamos a trabajar en la fracción continua un nivel más profundo encontrando el a 2 tal que p / q esté entre 1 / (a 2 1 + 1 / a) y 1 / (a 1 + 1 / (a 2 + 1)). Si mágicamente encontramos p / q, ¡genial! De lo contrario, bajamos un nivel más en la fracción continua. Eventualmente, encontraremos el número de esta manera, y no puede tomar mucho tiempo. Cada búsqueda binaria para encontrar un coeficiente toma como máximo O (lg (p + q)) tiempo, y hay como máximo O (lg (p + q)) niveles para la búsqueda, por lo que solo necesitamos O (lg 2 (p + q)) operaciones aritméticas y sondeos para recuperar p / q.
Un detalle que quiero señalar es que necesitamos hacer un seguimiento de si estamos en un nivel impar o en un nivel par cuando hacemos la búsqueda porque cuando intercalamos p / q entre dos fracciones continuas, necesitamos saber si el coeficiente que estábamos buscando era la fracción superior o la inferior. Declararé sin pruebas que para una i con una i impar, desea usar la parte superior de los dos números y con una i par, usa la menor de los dos números.
Estoy casi 100% seguro de que este algoritmo funciona. Voy a intentar escribir una prueba más formal de esto en la que llenaré todos los vacíos en este razonamiento, y cuando lo haga, publicaré un enlace aquí.
Gracias a todos por contribuir con los conocimientos necesarios para que esta solución funcione, especialmente a Jason S por sugerir una búsqueda binaria sobre fracciones continuas.
fuente
Recuerde que cualquier número racional en (0, 1) se puede representar como una suma finita de fracciones unitarias distintas (positivas o negativas). Por ejemplo, 2/3 = 1/2 + 1/6 y 2/5 = 1/2 - 1/10. Puede usar esto para realizar una búsqueda binaria sencilla.
fuente
Aquí hay otra forma de hacerlo. Si hay suficiente interés, intentaré completar los detalles esta noche, pero no puedo ahora porque tengo responsabilidades familiares. Aquí hay un fragmento de una implementación que debería explicar el algoritmo:
Y aquí está la explicación. Lo que
best_continued_fraction(x, bound)
debe hacer es encontrar la última aproximación de fracción continuax
con el denominador como máximobound
. Este algoritmo tomará pasos de polylog para completar y encuentra muy buenas (aunque no siempre las mejores) aproximaciones. Entonces para cadabound
obtendremos algo parecido a una búsqueda binaria a través de todas las fracciones posibles de ese tamaño. Ocasionalmente no encontraremos una fracción en particular hasta que aumentemos el límite más de lo debido, pero no estaremos muy lejos.Así que ahí lo tienes. Número logarítmico de preguntas encontradas con el trabajo polylog.
Actualización: Y código de trabajo completo.
Parece un poco más eficiente en las conjeturas que la solución anterior y realiza muchas menos operaciones. Para 101/1024 requirió 19 conjeturas y 251 operaciones. Para .98765 necesitó 27 suposiciones y 623 operaciones. Para 0.0123456789 requirió 66 suposiciones y 889 operaciones. Y para risitas y sonrisas, para 0.0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 (eso es 10 copias del anterior) requirió 665 suposiciones y 23289 operaciones.
fuente
Puede ordenar números racionales en un intervalo dado, por ejemplo, por el par (denominador, numerador). Entonces para jugar el juego puedes
[0, N]
utilizando el método de duplicación[a, b]
apunte al racional con el denominador más pequeño en el intervalo más cercano al centro del intervalo.Sin embargo, esto probablemente sea todavía
O(log(num/den) + den)
(no estoy seguro y es demasiado temprano en la mañana aquí para hacerme pensar con claridad ;-))fuente