¿El juego" adivina el número " para números racionales arbitrarios?


Una vez recibí la siguiente pregunta en una entrevista:

Estoy pensando en un entero positivo n. Llegar a un algoritmo que puede adivinar en consultas O(lg n). Cada pregunta es un número de su elección, y voy a responder ya sea "más bajo", "más alto" o " correcto."

Este problema se puede resolver mediante una búsqueda binaria modificada, en la que se listan potencias de dos hasta encontrar una que exceda n, y luego se ejecuta una búsqueda binaria estándar sobre ese rango. Lo que pienso es tan lo bueno de esto es que puedes buscar un espacio infinito para un número en particular más rápido que solo la fuerza bruta.

La pregunta que tengo, sin embargo, es una ligera modificación de este problema. En lugar de elegir un entero positivo, supongamos que elijo un número racional arbitrario entre cero y uno. Mi pregunta es: ¿qué algoritmo se puede utilizar para determinar más eficientemente qué número racional he elegido?

En este momento, la mejor solución que tengo puede encontrar p / q en a lo sumo O (q) tiempo caminando implícitamente el árbol Stern-Brocot, un árbol binario de búsqueda sobre todos los racionales. Sin embargo, esperaba obtener un tiempo de ejecución más cercano al tiempo de ejecución que obtuvimos para el caso entero, tal vez algo como O(lg (p + q)) u O(lg pq). ¿Alguien sabe de una manera 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 falla 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 este espacio dadas comparaciones simplemente mayores/iguales/menos.

Author: GEOCHET, 2011-03-26

8 answers

Bien, aquí está mi respuesta usando fracciones continuas solo.

Primero vamos a obtener algo de terminología aquí.

Sea X = p/q la fracción desconocida.

Sea Q(X,p/q) = sign(X - p/q) 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 = [a0; a1, a2, a3, ... a k ]

= a0 + 1/(un1 + 1/(un2 + 1/(un3 + 1/( ... + 1 / a k )... )))


Seguiremos el siguiente algoritmo para 0

  1. Inicializar Y = 0 = [ 0 ], Z = 1 = [ 1 ], k = 0.

  2. Bucle exterior : Las condiciones previas son las siguientes:

    • Y y Z son fracciones continuas de términos k + 1 que son idénticos excepto en el último elemento, donde difieren en 1, de modo que Y = [y0; y1, y2, y3, ... yk] y Z = [y0; y1, y2, y3, ... yk + 1]

    • (-1)k(Y-X) k(Z-X), o en términos más sencillos, para k aun, Y

  3. Extender el grado de la fracción continua por 1 paso sin cambiar los valores de los números. En general, si el último los términos son yk y yk + 1, lo cambiamos a [... yk , yk+1=∞] and [... yk, zk+1=1]. Ahora aumentar k en 1.

  4. Bucles internos : Esto es esencialmente lo mismo que la pregunta de entrevista de @templatetypedef sobre los enteros. Hacemos una búsqueda binaria de dos fases para acercarnos:

  5. Bucle Interno 1 yk = ∞, zk = a y X es entre y y Z.

  6. El último término de Double Z: Calcula M = Z pero con mk = 2*a = 2*zk.

  7. Consulta el número desconocido: q = Q (X, M).

  8. Si q = 0, tenemos nuestra respuesta y vamos al paso 17 .

  9. Si q y Q (X,Y) tienen signos opuestos, significa que X está entre Y y M, por lo que establece Z = M e ir al paso 5.

  10. De lo contrario, establezca Y = M y vaya al siguiente paso:

  11. Bucle Interno 2. y k = b, zk = a, y X está entre Y y Z.

  12. Si a y b difieren en 1, cambie Y y Z, vaya al paso 2.

  13. Realice una búsqueda binaria: calcule M donde m k = floor((a+b)/2, y consulte q = Q(X,M).

  14. Si q = 0, hemos terminado y vamos al paso 17.

  15. Si q y Q (X,Y) tienen signos opuestos, significa que X está entre Y y M, por lo que establece Z = M e ir al paso 11.

  16. De lo contrario, q y Q (X,Z) tienen signos opuestos, significa que X está entre Z y M, por lo que establece Y = M e ir al paso 11.

  17. Hecho: X = M.

Un ejemplo concreto para X = 16/113 = 0.14159292

Y = 0 = [0], Z = 1 = [1], k = 0

k = 1:
Y = 0 = [0; &#8734;] < X, Z = 1 = [0; 1] > X, M = [0; 2] = 1/2 > X.
Y = 0 = [0; &#8734;], Z = 1/2 = [0; 2], M = [0; 4] = 1/4 > X.
Y = 0 = [0; &#8734;], Z = 1/4 = [0; 4], M = [0; 8] = 1/8 < X.
Y = 1/8 = [0; 8], Z = 1/4 = [0; 4], M = [0; 6] = 1/6 > X.
Y = 1/8 = [0; 8], Z = 1/6 = [0; 6], M = [0; 7] = 1/7 > X.
Y = 1/8 = [0; 8], Z = 1/7 = [0; 7] 
  --> the two last terms differ by one, so swap and repeat outer loop.

k = 2:
Y = 1/7 = [0; 7, &#8734;] > X, Z = 1/8 = [0; 7, 1] < X,
    M = [0; 7, 2] = 2/15 < X
Y = 1/7 = [0; 7, &#8734;], Z = 2/15 = [0; 7, 2],
    M = [0; 7, 4] = 4/29 < X
Y = 1/7 = [0; 7, &#8734;], Z = 4/29 = [0; 7, 4], 
    M = [0; 7, 8] = 8/57 < X
Y = 1/7 = [0; 7, &#8734;], Z = 8/57 = [0; 7, 8],
    M = [0; 7, 16] = 16/113 = X 
    --> done!

En cada paso de calcular M, el rango del intervalo se reduce. Probablemente es 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 demostraría que este algoritmo es O (log q) pasos.

Tenga en cuenta que esto se puede combinar con la pregunta original de la entrevista de templatetypedef y se aplica hacia cualquier número racional p/q, no solo entre 0 y 1, calculando primero Q(X,0), luego para cualquiera de los enteros positivos/negativos, delimitando entre dos enteros consecutivos, y luego utilizando el algoritmo anterior para la parte fraccionaria.

Cuando tenga la oportunidad siguiente, publicaré un programa python que implementa este algoritmo.

Edit : además, tenga en cuenta que no tiene que calcular la fracción continua cada paso (que sería O (k), hay aproximantes parciales a fracciones continuas que pueden calcular el siguiente paso del paso anterior en O(1).)

Editar 2 : Definición recursiva de aproximantes parciales:

If A k =[a0; a1, a2, a3, ... unk] = pk/pk, entonces pk = akpk-1 + pk-2, y qk = akpk-1 + q k-2 . (Fuente: Niven & Zuckerman, 4th ed, Teoremas 7.3-7.5. Véase también Wikipedia )

Ejemplo: [0] = 0/1 = p0/q0, [0; 7] = 1/7 = p1/q1; así [0; 7, 16] = (16*1+0)/(16*7+1) = 16/113 = p2/q2.

Esto significa que si dos fracciones continuas Y y Z tienen los mismos términos excepto la última, y la fracción continua excluyendo el último término es pk-1/q k-1 , entonces podemos escribir Y = (ykpk-1 + pk-2) / (ykpk-1 + pk-2) y Z = (zkpk-1 + pk-2) / (zkpk-1 + pk-2). Debería ser posible demostrar a partir de esto que |Y-Z| disminuye por 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:

import math

# Return a function that returns Q(p0/q0,p/q) 
#   = sign(p0/q0-p/q) = sign(p0q-q0p)*sign(q0*q)
# If p/q < p0/q0, then Q() = 1; if p/q < p0/q0, then Q() = -1; otherwise Q()=0.
def makeQ(p0,q0):
  def Q(p,q):
    return cmp(q0*p,p0*q)*cmp(q0*q,0)
  return Q

def strsign(s):
  return '<' if s<0 else '>' if s>0 else '=='

def cfnext(p1,q1,p2,q2,a):
  return [a*p1+p2,a*q1+q2]

def ratguess(Q, doprint, kmax):
# p2/q2 = p[k-2]/q[k-2]
  p2 = 1
  q2 = 0
# p1/q1 = p[k-1]/q[k-1]
  p1 = 0
  q1 = 1
  k = 0
  cf = [0]
  done = False
  while not done and (not kmax or k < kmax):
    if doprint:
      print 'p/q='+str(cf)+'='+str(p1)+'/'+str(q1)
# extend continued fraction
    k = k + 1
    [py,qy] = [p1,q1]
    [pz,qz] = cfnext(p1,q1,p2,q2,1)
    ay = None
    az = 1
    sy = Q(py,qy)
    sz = Q(pz,qz)
    while not done:
      if doprint:
        out = str(py)+'/'+str(qy)+' '+strsign(sy)+' X '
        out += strsign(-sz)+' '+str(pz)+'/'+str(qz)
        out += ', interval='+str(abs(1.0*py/qy-1.0*pz/qz))
      if ay:
        if (ay - az == 1):
          [p0,q0,a0] = [pz,qz,az]
          break
        am = (ay+az)/2
      else:
        am = az * 2
      [pm,qm] = cfnext(p1,q1,p2,q2,am)
      sm = Q(pm,qm)
      if doprint:
        out = str(ay)+':'+str(am)+':'+str(az) + '   ' + out + ';  M='+str(pm)+'/'+str(qm)+' '+strsign(sm)+' X '
        print out
      if (sm == 0):
        [p0,q0,a0] = [pm,qm,am]
        done = True
        break
      elif (sm == sy):
        [py,qy,ay,sy] = [pm,qm,am,sm]
      else:
        [pz,qz,az,sz] = [pm,qm,am,sm]     

    [p2,q2] = [p1,q1]
    [p1,q1] = [p0,q0]    
    cf += [a0]

  print 'p/q='+str(cf)+'='+str(p1)+'/'+str(q1)
  return [p1,q1]

Y a muestra de salida para ratguess(makeQ(33102,113017), True, 20):

p/q=[0]=0/1
None:2:1   0/1 < X < 1/1, interval=1.0;  M=1/2 > X 
None:4:2   0/1 < X < 1/2, interval=0.5;  M=1/4 < X 
4:3:2   1/4 < X < 1/2, interval=0.25;  M=1/3 > X 
p/q=[0, 3]=1/3
None:2:1   1/3 > X > 1/4, interval=0.0833333333333;  M=2/7 < X 
None:4:2   1/3 > X > 2/7, interval=0.047619047619;  M=4/13 > X 
4:3:2   4/13 > X > 2/7, interval=0.021978021978;  M=3/10 > X 
p/q=[0, 3, 2]=2/7
None:2:1   2/7 < X < 3/10, interval=0.0142857142857;  M=5/17 > X 
None:4:2   2/7 < X < 5/17, interval=0.00840336134454;  M=9/31 < X 
4:3:2   9/31 < X < 5/17, interval=0.00379506641366;  M=7/24 < X 
p/q=[0, 3, 2, 2]=5/17
None:2:1   5/17 > X > 7/24, interval=0.00245098039216;  M=12/41 < X 
None:4:2   5/17 > X > 12/41, interval=0.00143472022956;  M=22/75 > X 
4:3:2   22/75 > X > 12/41, interval=0.000650406504065;  M=17/58 > X 
p/q=[0, 3, 2, 2, 2]=12/41
None:2:1   12/41 < X < 17/58, interval=0.000420521446594;  M=29/99 > X 
None:4:2   12/41 < X < 29/99, interval=0.000246366100025;  M=53/181 < X 
4:3:2   53/181 < X < 29/99, interval=0.000111613371282;  M=41/140 < X 
p/q=[0, 3, 2, 2, 2, 2]=29/99
None:2:1   29/99 > X > 41/140, interval=7.21500721501e-05;  M=70/239 < X 
None:4:2   29/99 > X > 70/239, interval=4.226364059e-05;  M=128/437 > X 
4:3:2   128/437 > X > 70/239, interval=1.91492009996e-05;  M=99/338 > X 
p/q=[0, 3, 2, 2, 2, 2, 2]=70/239
None:2:1   70/239 < X < 99/338, interval=1.23789953207e-05;  M=169/577 > X 
None:4:2   70/239 < X < 169/577, interval=7.2514738621e-06;  M=309/1055 < X 
4:3:2   309/1055 < X < 169/577, interval=3.28550190148e-06;  M=239/816 < X 
p/q=[0, 3, 2, 2, 2, 2, 2, 2]=169/577
None:2:1   169/577 > X > 239/816, interval=2.12389981991e-06;  M=408/1393 < X 
None:4:2   169/577 > X > 408/1393, interval=1.24415093544e-06;  M=746/2547 < X 
None:8:4   169/577 > X > 746/2547, interval=6.80448470014e-07;  M=1422/4855 < X 
None:16:8   169/577 > X > 1422/4855, interval=3.56972657711e-07;  M=2774/9471 > X 
16:12:8   2774/9471 > X > 1422/4855, interval=1.73982239227e-07;  M=2098/7163 > X 
12:10:8   2098/7163 > X > 1422/4855, interval=1.15020646951e-07;  M=1760/6009 > X 
10:9:8   1760/6009 > X > 1422/4855, interval=6.85549088053e-08;  M=1591/5432 < X 
p/q=[0, 3, 2, 2, 2, 2, 2, 2, 9]=1591/5432
None:2:1   1591/5432 < X < 1760/6009, interval=3.06364213998e-08;  M=3351/11441 < X 
p/q=[0, 3, 2, 2, 2, 2, 2, 2, 9, 1]=1760/6009
None:2:1   1760/6009 > X > 3351/11441, interval=1.45456726663e-08;  M=5111/17450 < X 
None:4:2   1760/6009 > X > 5111/17450, interval=9.53679318849e-09;  M=8631/29468 < X 
None:8:4   1760/6009 > X > 8631/29468, interval=5.6473816179e-09;  M=15671/53504 < X 
None:16:8   1760/6009 > X > 15671/53504, interval=3.11036635336e-09;  M=29751/101576 > X 
16:12:8   29751/101576 > X > 15671/53504, interval=1.47201634215e-09;  M=22711/77540 > X 
12:10:8   22711/77540 > X > 15671/53504, interval=9.64157420569e-10;  M=19191/65522 > X 
10:9:8   19191/65522 > X > 15671/53504, interval=5.70501257346e-10;  M=17431/59513 > X 
p/q=[0, 3, 2, 2, 2, 2, 2, 2, 9, 1, 8]=15671/53504
None:2:1   15671/53504 < X < 17431/59513, interval=3.14052228667e-10;  M=33102/113017 == X

Dado que Python maneja las matemáticas de biginteger desde el principio, y este programa solo usa matemáticas de enteros (excepto para los cálculos de intervalos), debería funcionar para racionales arbitrarios.


Editar 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 nk 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)): es b(a_k) medidas para ampliar la "red" de la búsqueda binaria, y b(a_k)-1 pasos para reducir). Vea el ejemplo anterior, notará que el # 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 probar el resultado deseado.

Vamos a dejarlo en esta manera: que el valor de q después de la Nk = sum(nk) pasos requerido para alcanzar el término kth tiene un mínimo: q > = A * 2cN para algunas constantes fijas A, c. (así que para invertir, obtendríamos que el # de pasos N es 2 (q / A) = O (log q).)

Casos base:

  • k = 0: q = 1, N= 0, entonces q > = 2 N
  • k=1: N = 2b-1 pasos, q = a1 >= 2b-1 = 2(N-1)/2 = 2N/2/sqrt(2).

Esto implica que A = 1, c = 1/2 podría proporcionar límites deseados. En realidad, q puede no 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, qk = akpk-1 + pk-2. De nuevo, para el nk = 2b-1 pasos necesarios para este término, unk >= 2b-1 = 2(nk-1)/2.

    So a k q k-1 >= 2(Nk-1)/2 * pk-1 >= 2(nk-1)/2 * A*2Nk-1/4 = A*2Nk/4/sqrt(2)*2nk/4.

Argh the la parte difícil aquí es que si ak = 1, q puede no aumentar mucho para ese término, y necesitamos usar qk-2 pero eso puede ser mucho menor que qk-1.

 49
Author: Jason S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-27 15:07:33

Tomemos los números racionales, en forma reducida, y escríbalos en orden primero del denominador, luego del numerador.

1/2, 1/3, 2/3, 1/4, 3/4, 1/5, 2/5, 3/5, 4/5, 1/6, 5/6, ...

Nuestra primera suposición será 1/2. Luego iremos a lo largo de la lista hasta que tengamos 3 en nuestro rango. Entonces tomaremos 2 conjeturas para buscar esa lista. Luego iremos a lo largo de la lista hasta que tengamos 7 en nuestro rango restante. Luego tomaremos 3 conjeturas para buscar en esa lista. Y así sucesivamente.

En n pasos cubriremos las primeras 2O(n) posibilidades, que está en el orden de magnitud de la eficiencia que estabas buscando.

Actualización: La gente no entendió el razonamiento detrás de esto. El razonamiento es simple. Sabemos cómo caminar un árbol binario de manera eficiente. Hay O(n2) fracciones con máximo denominador n. Por lo tanto, podríamos buscar hasta cualquier tamaño de denominador en pasos O(2*log(n)) = O(log(n)). El problema es que tenemos un número infinito de posibles racionales para buscar. Así que no podemos alinearlos, ordenarlos., y empezar a buscar.

Por lo tanto, mi idea era alinear unos pocos, buscar, alinear más, buscar, y así sucesivamente. Cada vez que nos alineamos más nos alineamos alrededor del doble de lo que hicimos la última vez. Así que necesitamos una conjetura más que la última vez. Por lo tanto, nuestro primer pase utiliza 1 conjetura para atravesar 1 racional posible. Nuestro segundo utiliza 2 conjeturas para atravesar 3 posibles racionales. Nuestra tercera usa 3 conjeturas para atravesar 7 posibles racionales. Y nuestro k'th utiliza k conjeturas para atravesar 2k-1 posible racional. Para cualquier racional m/n en particular, eventualmente terminará poniendo ese racional en una lista bastante grande que sabe cómo hacer una búsqueda binaria de manera eficiente.

Si hiciéramos búsquedas binarias, y luego ignoráramos todo lo que habíamos aprendido cuando agarramos más racionales, entonces pondríamos todos los racionales hasta e incluyendo m/n en O(log(n)) pases. (Eso es porque en ese punto vamos a llegar a un pase con suficientes racionales para incluir todos los racionales hasta e incluyendo m/n.) Pero cada uno pass toma más conjeturas, por lo que sería O(log(n)2) conjeturas.

Sin embargo en realidad lo hacemos mucho mejor que eso. Con nuestra primera suposición, eliminamos la mitad de los racionales en nuestra lista como demasiado grandes o pequeños. Nuestras próximas dos conjeturas no cortan el espacio en cuartos, pero no se alejan demasiado de él. Nuestras próximas 3 conjeturas de nuevo no cortan el espacio en octavos, pero no se alejan demasiado de él. Y así sucesivamente. Cuando lo juntas, estoy convencido de que el resultado es que encuentras m/n en O(log(n)) pasos. Aunque en realidad no tengo pruebas.

Pruébelo: Aquí está el código para generar las conjeturas para que pueda jugar y ver qué tan eficiente es.

#! /usr/bin/python

from fractions import Fraction
import heapq
import readline
import sys

def generate_next_guesses (low, high, limit):
    upcoming = [(low.denominator + high.denominator,
                 low.numerator + high.numerator,
                 low.denominator, low.numerator,
                 high.denominator, high.numerator)]
    guesses = []
    while len(guesses) < limit:
        (mid_d, mid_n, low_d, low_n, high_d, high_n) = upcoming[0]
        guesses.append(Fraction(mid_n, mid_d))
        heapq.heappushpop(upcoming, (low_d + mid_d, low_n + mid_n,
                                     low_d, low_n, mid_d, mid_n))
        heapq.heappush(upcoming, (mid_d + high_d, mid_n + high_n,
                                  mid_d, mid_n, high_d, high_n))
    guesses.sort()
    return guesses

def ask (num):
    while True:
        print "Next guess: {0} ({1})".format(num, float(num))
        if 1 < len(sys.argv):
            wanted = Fraction(sys.argv[1])
            if wanted < num:
                print "too high"
                return 1
            elif num < wanted:
                print "too low"
                return -1
            else:
                print "correct"
                return 0

        answer = raw_input("Is this (h)igh, (l)ow, or (c)orrect? ")
        if answer == "h":
            return 1
        elif answer == "l":
            return -1
        elif answer == "c":
            return 0
        else:
            print "Not understood.  Please say one of (l, c, h)"

guess_size_bound = 2
low = Fraction(0)
high = Fraction(1)
guesses = [Fraction(1,2)]
required_guesses = 0
answer = -1
while 0 != answer:
    if 0 == len(guesses):
        guess_size_bound *= 2
        guesses = generate_next_guesses(low, high, guess_size_bound - 1)
    #print (low, high, guesses)
    guess = guesses[len(guesses)/2]
    answer = ask(guess)
    required_guesses += 1
    if 0 == answer:
        print "Thanks for playing!"
        print "I needed %d guesses" % required_guesses
    elif 1 == answer:
        high = guess
        guesses[len(guesses)/2:] = []
    else:
        low = guess
        guesses[0:len(guesses)/2 + 1] = []

Como ejemplo para probarlo probé 101/1024 (0.0986328125) y encontré que tomó 20 conjeturas para encontrar la respuesta. Intenté 0.98765 y me tomó 45 conjeturas. Probé 0.0123456789 y necesité 66 conjeturas y alrededor de un segundo para generarlas. (Nota, si llama al programa con un número racional como argumento, llenará todas las conjeturas para usted. Esta es una conveniencia muy útil.)

 6
Author: btilly,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-26 17:07:52

¡Lo tengo! Lo que necesita hacer es usar 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 de ejecutarlos en paralelo es la siguiente.

En cada paso, tienes l y u siendo los límites inferior y superior de la bisección. La idea es, usted tiene una opción entre la reducción a la mitad del rango de bisección, y la adición de un término adicional como una representación de fracción continua. Cuando tanto l como u tienen el mismo término siguiente como una fracción continua, entonces toma el siguiente paso en la búsqueda de fracciones continuas y realiza una consulta usando la fracción continua. De lo contrario, se reduce a la mitad el rango usando bisección.

Dado que ambos métodos aumentan el denominador por al menos un factor constante (la bisección pasa por factores de 2, las fracciones continuas pasan por al menos una factor de phi = (1+sqrt(5))/2), esto significa que su búsqueda debe ser O(log(q)). (Puede haber cálculos continuos repetidos de fracciones, por lo que puede terminar como O(log(q)^2).)

Nuestra búsqueda continua de fracciones necesita redondear al entero más cercano, no usar piso (esto es más claro a continuación).

Lo anterior es una especie de manual. Usemos un ejemplo concreto de r = 1/31:

  1. L = 0, u = 1, query = 1/2. 0 no es expresable como una fracción continua, por lo que usamos la búsqueda binaria hasta que l != 0.

  2. L = 0, u = 1/2, query = 1/4.

  3. L = 0, u = 1/4, query = 1/8.

  4. L = 0, u = 1/8, query = 1/16.

  5. L = 0, u = 1/16, query = 1/32.

  6. L = 1/32, u = 1/16. Ahora 1 / l = 32, 1 / u = 16, estos tienen diferentes repeticiones cfrac, así que sigue bisectando., query = 3/64.

  7. L = 1/32, u = 3/64, consulta = 5/128 = 1/25.6

  8. L = 1/32, u = 5/128, query = 9/256 = 1/28.4444....

  9. L = 1/32, u = 9/256, query = 17/512 = 1/30.1176... (redondear a 1/30)

  10. L = 1/32, u = 17/512, query = 33/1024 = 1/31.0303... (redondear a 1/31)

  11. L = 33/1024, u = 17/512, query = 67/2048 = 1/30.5672... (redondear a 1/31)

  12. L = 33/1024, u = 67/2048. En este punto, tanto l como u tienen el mismo término de fracción continua 31, por lo que ahora usamos una suposición de fracción continua. consulta = 1/31.

¡ÉXITO!

Para otro ejemplo usemos 16/113 (= 355/113 - 3 donde 355/113 es bastante cercano a pi).

[para continuar, tengo que ir a alguna parte]


En una reflexión posterior, las fracciones continuas son el camino a seguir, no importa la bisección, excepto para determinar el siguiente término. Más cuando vuelva.

 4
Author: Jason S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-19 18:49:04

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 conjetura, y el retador responde "más grande" o "más pequeño". Esto me permite reservar la palabra "adivinar" para otra cosa, una conjetura para p + q que no se le pide directamente al retador.

La idea es encontrar primero p + q, usando el algoritmo que describes en tu pregunta: adivina un valor k, si k es demasiado pequeño, duplícalo y intentarlo. Luego, una vez que tenga un límite superior e inferior, haga una búsqueda binaria estándar. Esto toma consultas O(log (p+q)T), donde T es un límite superior para el número de consultas que se necesita para verificar una conjetura. Vamos a encontrar T.

Queremos comprobar todas las fracciones r/s con r + s

Nunca vamos a adivinar un valor de k mayor que 2(p + q). Por lo tanto podemos tomar T = O(log(p+q)).

Cuando adivinemos el valor correcto para k (es decir, k = p + q), enviaremos la consulta p/q al retador en el curso de verificar nuestra conjetura para k, y ganaremos el juego.

El número total de consultas es entonces O(log^2(p + q)).

 3
Author: Seth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-26 08:25:50

Bien, creo que me di cuenta de una O(lg2 q) algoritmo para este problema que se basa en la visión más excelente de Jason S sobre el uso de fracciones continuas. Pensé en desarrollar el algoritmo hasta 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

A0 + 1 / (a1 + 1 / (a2 + 1 / (a3 + 1 / ...))

Para las opciones apropiadas de a i. Esto se llama fracción continua. Más importante aún, aunque estos ai 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 once veces cero, por lo que una aproximación bruta de 11/14 sería

0 = 0

Ahora, supongamos que tomamos la recíproco de esta fracción para obtener 14/11 = 1 3/11. Así que si escribimos

0 + (1 / 1) = 1

Obtenemos una aproximación ligeramente mejor a 11/14. Ahora que nos quedamos con 3 / 11, podemos tomar el recíproco de nuevo para obtener 11/3 = 3 2/3, por lo que podemos considerar

0 + (1 / (1 + 1/3)) = 3/4

Que es otra buena aproximación a 11/14. Ahora, tenemos 2/3, así que considere el recíproco, que es 3/2 = 1 1/2. Si luego escribimos

0 + (1 / (1 + 1/(3 + 1/1))) = 5/6

Obtenemos otra buena aproximación a 11/14. Finalmente, nos quedamos con 1/2, cuyo recíproco es 2/1. Si finalmente escribimos

0 + (1 / (1 + 1/(3 + 1/(1 + 1/2)))) = (1 / (1 + 1/(3 + 1/(3/2)))) = (1 / (1 + 1/(3 + 2/3)))) = (1 / (1 + 1/(11/3)))) = (1 / (1 + 3/11)) = 1 / (14/11) = 11/14

Que es exactamente la fracción que queríamos. Por otra parte, mira el secuencia de coeficientes que terminamos usando. Si se ejecuta el algoritmo euclidiano extendido en 11 y 14, se obtiene que

11 = 0 x 14 + 11 > > a0 = 0 14 = 1 x 11 + 3 > > a1 = 1 11 = 3 x 3 + 2 > > a2 = 3 3 = 2 x 1 + 1 > > a3 = 2

Resulta que (usando más matemáticas de las que actualmente sé 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:

  1. Puede haber como máximo coeficientes O(lg (p + q)), porque el algoritmo euclidiano siempre termina en estos muchos pasos, y
  2. Cada coeficiente es máximo{p, q}.

Dados estos dos hechos, podemos llegar a 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 un1 para que 1 / a1 está lo más cerca posible de p / q y a1 es un entero. Para hacer esto, podemos ejecutar nuestro algoritmo para adivinar arbitrariamente enteros, tomando el recíproco cada vez. Después de hacer esto, una de dos cosas habrá sucedido. Primero, podríamos por pura coincidencia descubrir que p / q = 1 / k para algún entero k, en cuyo caso hemos terminado. Si no, encontraremos que p / q está intercalado entre 1/(a1 - 1) y 1 / a0 para algunos un1. Cuando hacemos esto, entonces empezamos a trabajar en la fracción continua un nivel más profundo mediante la búsqueda de la a2 tal que p / q está entre 1 / (a1 + 1/a2) y 1 / (a)1 + 1/( a2 + 1)). Si mágicamente encontramos p / q, eso es genial! De lo contrario, vamos un nivel más abajo 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 (lg2(p + q)) operaciones aritméticas y sondas para recuperar p/q.

Un detalle que quiero señalar es que tenemos que hacer un seguimiento de si estamos en un nivel impar o un nivel par al hacer la búsqueda porque cuando emparedamos p/q entre dos fracciones continuas, necesitamos saber si el coeficiente que estábamos buscando era la fracción superior o inferior. Declararé sin prueba que para un i con i impar quieres usar la parte superior de los dos números, y con un i incluso usas la parte inferior de los dos números.

Estoy casi 100% seguro de que este algoritmo funciona. Me voy para tratar de escribir una prueba más formal de esto en la que llene todos los vacíos en este razonamiento, y cuando lo haga, publicaré un enlace aquí.

Gracias a todos por contribuir con la información necesaria para que esta solución funcione, especialmente Jason S por sugerir una búsqueda binaria sobre fracciones continuas.

 3
Author: templatetypedef,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-19 18:40:12

Recuerde que cualquier número racional en (0, 1) puede representarse 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 directa.

 2
Author: Mick,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-26 12:45:58

Aquí hay otra manera 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:

low = 0
high = 1
bound = 2
answer = -1
while 0 != answer:
    mid = best_continued_fraction((low + high)/2, bound)
    while mid == low or mid == high:
        bound += bound
        mid = best_continued_fraction((low + high)/2, bound)
    answer = ask(mid)
    if -1 == answer:
        low = mid
    elif 1 == answer:
        high = mid
    else:
        print_success_message(mid)

Y aquí está la explicación. Lo que best_continued_fraction(x, bound) debe hacer es encontrar la última fracción continua aproximación a x con el denominador a lo sumo bound. Este algoritmo tomará pasos de polylog para completar y encuentra muy bueno (aunque no siempre la mejor) aproximaciones. Así que para cada bound obtendremos algo cercano 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 que deberíamos, pero no estaremos lejos.

Así que ahí lo tienen. Un número logarítmico de preguntas encontradas con el trabajo de polylog.

Actualización: Y código de trabajo completo.

#! /usr/bin/python

from fractions import Fraction
import readline
import sys

operations = [0]

def calculate_continued_fraction(terms):
    i = len(terms) - 1
    result = Fraction(terms[i])
    while 0 < i:
        i -= 1
        operations[0] += 1
        result = terms[i] + 1/result
    return result

def best_continued_fraction (x, bound):
    error = x - int(x)
    terms = [int(x)]
    last_estimate = estimate = Fraction(0)
    while 0 != error and estimate.numerator < bound:
        operations[0] += 1
        error = 1/error
        term = int(error)
        terms.append(term)
        error -= term
        last_estimate = estimate
        estimate = calculate_continued_fraction(terms)
    if estimate.numerator < bound:
        return estimate
    else:
        return last_estimate

def ask (num):
    while True:
        print "Next guess: {0} ({1})".format(num, float(num))
        if 1 < len(sys.argv):
            wanted = Fraction(sys.argv[1])
            if wanted < num:
                print "too high"
                return 1
            elif num < wanted:
                print "too low"
                return -1
            else:
                print "correct"
                return 0

        answer = raw_input("Is this (h)igh, (l)ow, or (c)orrect? ")
        if answer == "h":
            return 1
        elif answer == "l":
            return -1
        elif answer == "c":
            return 0
        else:
            print "Not understood.  Please say one of (l, c, h)"

ow = Fraction(0)
high = Fraction(1)
bound = 2
answer = -1
guesses = 0
while 0 != answer:
    mid = best_continued_fraction((low + high)/2, bound)
    guesses += 1
    while mid == low or mid == high:
        bound += bound
        mid = best_continued_fraction((low + high)/2, bound)
    answer = ask(mid)
    if -1 == answer:
        low = mid
    elif 1 == answer:
        high = mid
    else:
        print "Thanks for playing!"
        print "I needed %d guesses and %d operations" % (guesses, operations[0])

Parece un poco más eficiente en conjeturas que el solución anterior, y hace muchas menos operaciones. Para 101/1024 requirió 19 conjeturas y 251 operaciones. Para .98765 necesitaba 27 conjeturas y 623 operaciones. Para 0.0123456789 requirió 66 conjeturas y 889 operaciones. Y para risitas y sonrisas, para 0.0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 (eso es 10 copias de la anterior) se requiere 665 conjeturas y 23289 operaciones.

 2
Author: btilly,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-27 01:38:56

Puede ordenar los números racionales en un intervalo dado por ejemplo el par (denominador, numerador). A continuación, para jugar el juego se puede

  1. Encuentre el intervalo [0, N] usando el enfoque de paso doble
  2. Dado un intervalo [a, b] dispara por el racional con el denominador más pequeño en el intervalo que es el más cercano al centro del intervalo

Esto es sin embargo probablemente todavía O(log(num/den) + den) (no estoy seguro y es demasiado temprano en la mañana aquí para hacerme pensar claramente ;-) )

 0
Author: 6502,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-26 07:37:53