Игра "угадай число" для произвольных рациональных чисел?

однажды я получил следующий вопрос интервью:

Я имею в виду положительное целое число n. Придумайте алгоритм, который может угадать его в o (lg n) запросах. Каждый запрос-это номер по вашему выбору, и я отвечу либо "ниже", либо "выше", либо "правильно"."

эта проблема может быть решена путем модифицированного двоичного поиска, в котором вы перечисляете полномочия двух, пока не найдете тот, который превышает n, а затем запустите стандартный двоичный поиск по этому диапазону. Я думаю, что это так круто, что вы можете искать бесконечное пространство для определенного числа быстрее, чем просто грубая сила.

вопрос, который у меня есть, является небольшим изменением этой проблемы. Вместо выбора положительного целого числа предположим, что я выбираю произвольное рациональное число между нулем и единицей. Мой вопрос: какой алгоритм вы можете использовать для наиболее эффективного определения того, какое рациональное число я выбрал?

прямо сейчас, лучшие решение я могу найти p/q в более o(Щ) время неявно гулять Stern-Brocot tree, бинарное дерево поиска по всем рациональным. Тем не менее, я надеялся получить среду выполнения ближе к среде выполнения, которую мы получили для целого случая, возможно, что-то вроде O(lg (p + q)) или O(lg pq). Кто-нибудь знает способ получить такую среду выполнения?

первоначально я рассматривал использование стандартного двоичного поиска интервала [0, 1], но это будет только находить рациональные числа с неповторяющееся двоичное представление, которое пропускает почти все рационалы. Я также думал об использовании другого способа перечисления рациональных, но я не могу найти способ поиска этого пространства, учитывая только большее/равное / меньшее сравнение.

8 ответов


хорошо, вот мой ответ, используя продолжение фракций.

сначала давайте получим некоторую терминологию здесь.

пусть X = p / q-неизвестная дробь.

пусть Q(X,p/q) = знак (X - p/q) - функция запроса: если это 0, мы угадали число, и если это + / - 1, который сообщает нам знак нашей ошибки.

на обычная нотация для непрерывных дробей is a = [a0; a1, а2, a3, ... аk]

= a0 + 1/(a1 + 1/(a2 + 1/(a3 + 1/( ... + 1 / ak) ... )))


мы будем следовать следующему алгоритму Для 0

  1. инициализировать Y = 0 = [ 0 ], Z = 1 = [ 1], k = 0.

  2. внешний цикл: элемент предпосылки заключаются в том, что:

    • Y и Z-непрерывные дроби K+1 членов, которые идентичны, за исключением последнего элемента, где они отличаются на 1, так что Y = [y0; y1, y2, y3, ... yk] и Z = [y0; y1, y2, y3, ... yk + 1]

    • (-1)k(Y-X) k(Z-X), или проще говоря, для K четных, Y

  3. расширьте степень непрерывной дроби на 1 шаг без изменения значений чисел. В общем случае, если последние термины yk и yk + 1, мы переходим на [... yk, yk+1=∞] и [... yk, zk+1=1]. Теперь увеличьте k на 1.

  4. внутренние петли: это по существу то же самое, что и вопрос интервью @templatetypedef о целых числах. Мы делаем двухфазный двоичный поиск, чтобы приблизиться:

  5. внутренняя петля 1: yk = ∞, zk = a, а X находится между Y и Z.

  6. последний член Double Z: вычислить M = Z, но с mk = 2 * a = 2 * zk.

  7. запрос неизвестного числа: q = Q (X,M).

  8. если q = 0, у нас есть ответ и перейдите к шагу 17 .

  9. если q и Q(X,Y) имеют противоположные знаки, это означает, что X находится между Y и M, поэтому установите Z = M и перейдите к шагу 5.

  10. в противном случае установите Y = M и перейдите к следующему шагу:

  11. внутренняя петля 2. yk = b, zk = a, а X находится между Y и Z.

  12. если a и b отличаются на 1, Замените Y и Z, перейдите к Шагу 2.

  13. выполнить двоичный поиск: вычислить M, где mk = floor ((a+b)/2, и запрос q = Q (X,M).

  14. если q = 0, мы закончили и переходим к шагу 17.

  15. если q и Q (X, Y) имеют противоположные знаки, это означает, что X находится между Y и M, поэтому установите Z = M и перейдите к шагу 11.

  16. в противном случае q и Q(X,Z) имеют противоположные знаки, это означает, что X находится между Z и M, поэтому установите Y = M и перейдите к шагу 11.

  17. Готово: X = M.

конкретный пример для 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!

на каждом шаге вычисления M диапазон интервала уменьшается. Вероятно, довольно легко доказать (хотя я не буду этого делать), что интервал уменьшается по крайней мере в 1/sqrt(5) на каждом шаг, который показал бы, что этот алгоритм является o(log q) шагами.

обратите внимание, что это может быть объединено с оригинальным вопросом интервью templatetypedef и применяться к любой рациональное число p/q, а не только между 0 и 1, Сначала вычисляя Q(X,0), затем для положительных / отрицательных целых чисел, ограничивая между двумя последовательными целыми числами, а затем используя приведенный выше алгоритм для дробной части.

когда у меня будет следующий шанс, я опубликую программу python который реализует этот алгоритм.

редактировать: кроме того, обратите внимание, что вам не нужно вычислять непрерывную дробь на каждом шаге (который будет O(k), есть частичные аппроксиманты к непрерывным дробям, которые могут вычислить следующий шаг из предыдущего шага в O(1).)

Изменить 2: рекурсивное определение частичных приближений:

Еслиk = [a0; a1, a2, а3, ... аk] = pk / qk, затем pk = akpк-1 + pк-2, и qk = akqк-1 + qк-2. (Источник: Niven & Zuckerman, 4th ed, Theorems 7.3-7.5. См. также Википедия)

пример: [0] = 0/1 = p0 / q0, [0; 7] = 1/7 = p1 / q1; Итак [0; 7, 16] = (16*1+0)/(16*7+1) = 16/113 = p2 / q2.

это означает, что если две непрерывные дроби Y и Z имеют одинаковые члены, кроме последнего, а непрерывная дробь, исключая последний член, равна pк-1 / qк-1, тогда мы можем написать Y = (ykpк-1 + pк-2) / (ykqк-1 + qк-2) и Z = (zkpк-1 + pк-2) / (zkqк-1 + qк-2). Из этого должно быть возможно показать, что |Y-Z| уменьшается по крайней мере в 1/sqrt(5) на каждом меньшем интервале, создаваемом этим алгоритмом, но алгебра, похоже, находится за пределами меня в данный момент. :-(

вот моя программа 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]

и образец для 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

поскольку Python обрабатывает BigInteger math с самого начала, и эта программа использует только целочисленную математику (за исключением интервальных вычислений), она должна работать для произвольных рациональных значений.


редактировать 3: контур доказательства того, что это O (log q), а не O(log^2 q):

Сначала обратите внимание, что пока не найдено рациональное число, # шагов nk для каждого нового термина непрерывной дроби является ровно 2b(a_k)-1, где b(a_k) - это # битов, необходимых для представления a_k = ceil(log2(a_k)): это шаги b(a_k) для расширения "сети" двоичного поиска и шаги b (a_k)-1 для ее сужения). См. пример выше, вы заметите, что количество шагов всегда 1, 3, 7, 15 и т. д.

теперь мы можем использовать рекуррентное отношение qk = akqк-1 + qк-2 и индукции, чтобы доказать желаемый результат.

давайте сформулируем это следующим образом: значение q после Nk = sum (nk) шаги, необходимые для достижения k-го члена, имеют минимум: q > = A * 2 cN для некоторых фиксированных констант A, c. (Итак, чтобы инвертировать, мы получим, что # шагов N является 2 (q / A) = O(log q).)

базовая случаях:

  • k=0: q = 1, N = 0, поэтому q >= 2N
  • k=1: для n = 2b-1 шагов, q = A1 >= 2Б-1 = 2(N-1)/2 = 2N / 2 / sqrt (2).

это означает, что A = 1, c = 1/2 может обеспечить желаемые границы. В действительности q может не удвоить каждый член (контрпример: [0; 1, 1, 1, 1, 1] имеет коэффициент роста phi = (1+sqrt (5))/2), поэтому будем использовать c = 1/4.

индукции:

  • для термина k, qk = akqк-1 + qк-2. Опять же, для nk = 2b-1 шаги, необходимые для этого термина, ak >= 2Б-1 = 2(nk-1)/2.

    такkqк-1 >= 2(Nk-1)/2 * qк-1 >= 2(nk-1)/2 * A * 2Nк-1/4 = В*2Nk/4 / sqrt (2) * 2nk/4.

Argh -- трудная часть здесь это еслиk = 1, q не может сильно увеличиться для этого одного термина, и нам нужно использовать qк-2 но это может быть намного меньше, чем qк-1.


давайте возьмем рациональные числа в уменьшенном виде и запишем их в порядке сначала знаменателя, затем числителя.

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

наша первая догадка будет 1/2. Затем мы будем идти по списку, пока у нас не будет 3 в нашем диапазоне. Затем мы возьмем 2 догадки, чтобы найти этот список. Затем мы пойдем по списку, пока у нас не будет 7 в нашем оставшемся диапазоне. Затем мы возьмем 3 догадки, чтобы найти этот список. И так далее.

на n шаги мы рассмотрим первые 2O(n) возможности, которые находятся в порядке величины эффективности, которую вы искали.

обновление: люди не поняли причины этого. Рассуждение простое. Мы знаем, как эффективно ходить по бинарному дереву. Есть O(n2) дроби с максимальным знаменателем n. Поэтому мы могли бы искать до любого конкретного размера знаменателя в O(2*log(n)) = O(log(n)) действия. Проблема в том, что у нас есть бесконечное количество возможных рациональных поиска. Так мы не можем просто выстроить их в линию, заказать и начать поиски.

k ' th использует k догадки, чтобы пересечь 2k-1 можно рационалы. Для любого конкретного рационального m/n, в конечном итоге это приведет к тому, что rational будет помещен в довольно большой список, в котором он знает, как эффективно выполнять двоичный поиск.

если бы мы сделали бинарный поиск, а затем проигнорировали все, что мы узнали, когда мы хватаем больше рациональных, то мы бы поставили все рациональные до и включая m/n на O(log(n)) проходит. (Это потому, что к этому моменту мы доберемся до перевала с достаточно рациональных, чтобы включить все рациональные до и включая m/n.) Но каждый проход требует больше догадок, так что было бы O(log(n)2) догадки.

мы на самом деле делать намного лучше, чем это. С нашей первой догадкой мы исключаем половину рационалов из нашего списка как слишком большие или маленькие. Наши следующие две догадки не совсем разрезают пространство на четверти, но они не уходят слишком далеко от него. Наши следующие 3 догадки снова не совсем разрезают пространство на восьмые, но они не слишком далеко от него. И так далее. Когда вы собрали его вместе, я убежден, что в результате вы найдете m/n на O(log(n)) действия. Хотя у меня нет доказательств.

попробуйте: вот код для генерации догадок, чтобы вы могли играть и видеть, насколько он эффективен.

#! /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] = []

в качестве примера, чтобы попробовать, я попробовал 101/1024 (0.0986328125) и обнаружил, что потребовалось 20 догадок, чтобы найти ответ. Я попробовал 0.98765, и это заняло 45 гадает. Я попробовал 0.0123456789, и для их генерации потребовалось 66 догадок и около секунды. (Обратите внимание, если вы вызовете программу с рациональным числом в качестве аргумента, она заполнит все догадки для вас. Это очень полезное удобство.)


Я понял! Что вам нужно сделать, это использовать параллельный поиск с бисекции и продолжение фракций.

Bisection даст вам предел в сторону определенного вещественного числа, представленного как степень двух, а непрерывные дроби возьмут вещественное число и найдут ближайшее рациональное число.

как вы запускаете их параллельно следующим образом.

на каждом шагу у вас есть l и u быть нижней и верхней границами бисекция. Идея в том, что у вас есть выбор между разделением пополам диапазона деления и добавлением дополнительного члена в качестве продолжения дробного представления. Когда оба l и u имейте тот же следующий член, что и продолжение дроби, затем вы делаете следующий шаг в поиске продолжения дроби и делаете запрос, используя продолжение дроби. В противном случае вы уменьшаете диапазон вдвое с помощью bisection.

поскольку оба метода увеличивают знаменатель, по крайней мере, на постоянный фактор (деление идет по коэффициентам 2 непрерывные дроби идут по крайней мере на коэффициент phi = (1+sqrt(5))/2), это означает, что ваш поиск должен быть O(log(q)). (Могут повторяться непрерывные вычисления дробей, поэтому они могут заканчиваться как O(log (q)^2).)

наш непрерывный поиск фракции должен округляться до ближайшего целого числа, а не использовать пол (это яснее ниже).

выше вроде handwavy. Давайте используем конкретный пример r = 1/31:

  1. l = 0, u = 1, запрос = 1/2. 0 не выражается как непрерывная дробь, поэтому мы используем двоичный поиск до 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, запрос = 1/16.

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

  6. l = 1/32, u = 1/16. Теперь 1 / l = 32, 1 / u = 16, у них разные повторения cfrac, поэтому продолжайте делить пополам., запрашивать = 3/64.

  7. l = 1/32, u = 3/64, query = 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... (круглый 1/30)

  10. l = 1/32, u = 17/512, query = 33/1024 = 1/31.0303... (круг 1/31)

  11. l = 33/1024, u = 17/512, query = 67/2048 = 1/30.5672... (круг 1/31)

  12. l = 33/1024, u = 67/2048. В этот момент как l, так и u имеют одинаковый непрерывный член фракции 31, поэтому теперь мы используем непрерывное предположение фракции. query = 1/31.

успехов!

для другого примера давайте используем 16/113 (=355/113 - 3, где 355/113 довольно близко к pi).

[чтобы быть продолженным, я должен пойти куда-то]


при дальнейшем размышлении продолжение дробей путь, забудь бисекции кроме определить следующий срок. Больше, когда я вернусь.


Я думаю, что нашел алгоритм O(log^2(p + q)).

чтобы избежать путаницы в следующем абзаце, "запрос "относится к тому, когда гадатель дает претенденту догадку, и претендент отвечает" больше "или"меньше". Это позволяет мне зарезервировать слово "догадка" для чего-то еще, догадка для p + q, которая не задается непосредственно претенденту.

идея состоит в том, чтобы сначала найти p + q, используя алгоритм, который вы описываете в своем вопросе: угадайте значение k, если k слишком мало, удвойте и повторите попытку. Затем, как только у вас есть верхняя и нижняя граница, выполните стандартный двоичный поиск. Это требует o(log (p+q)t) запросов, где T-верхняя граница для количества запросов, необходимых для проверки догадки. Давайте найдем т.

мы хотим проверить все дроби r / s с r + S

мы никогда не угадаем значение k больше 2 (p + q). Следовательно, мы можем взять T = O (log (p+q)).

когда мы угадаем правильное значение для k (т. е. k = p + q), мы отправим запрос p/q претенденту в ходе проверки нашего предположения для k и выиграем игру.

общее количество запросов тогда O (log^2 (p + q)).


хорошо, я думаю, что я понял O (lg2 q) алгоритм для этой проблемы, основанный на самом отличном понимании Джейсона S об использовании непрерывных дробей. Я подумал, что я бы конкретизировал алгоритм прямо здесь, чтобы у нас было полное решение, наряду с анализом времени выполнения.

интуиция алгоритма заключается в том, что любое рациональное число p/q в диапазоне может быть записано как

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

для соответствующих вариантовЯ. Это называется продолжение фракция. Что еще более важно, хотя этиЯ можно получить, запустив евклидов алгоритм на числителе и знаменателе. Например, предположим, что мы хотим представить 11/14 таким образом. Начнем с того, что 14 переходит в одиннадцать нулевых раз, поэтому грубое приближение 11/14 будь

0 = 0

теперь предположим, что мы возьмем реципрокность этой дроби, чтобы получить 14/11 = 1 3/11. Так что если мы напишем

0 + (1 / 1) = 1

мы получаем немного лучшее приближение к 11/14. Теперь, когда у нас осталось 3/11, мы можем снова взять взаимное, чтобы получить 11/3 = 3 2/3, поэтому мы можем считать

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

Что является еще одним хорошим приближением 11/14. Теперь у нас есть 2/3, поэтому рассмотрим взаимное, которое равно 3/2 = 1 1/2. Если мы пишем

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

мы получаем еще одно хорошее приближение к 11/14. Наконец, мы остаемся с 1/2, взаимный 2/1. Если мы, наконец, написать

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

именно фракции мы хотели. Более того, посмотрите на последовательность коэффициентов, которые мы использовали. Если вы запустите расширенный евклидов алгоритм на 11 и 14, вы получите это

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

оказывается ,что (используя больше математики, чем я в настоящее время знаю, как это сделать!) что это не совпадение и что коэффициенты в непрерывной дроби p/q всегда формируются с использованием расширенного евклидова алгоритма. Это здорово, потому что это говорит нам две вещи:

  1. может быть не более o(lg (p + q)) коэффициентов, потому что евклидов алгоритм всегда заканчивается на этом множестве шагов, и
  2. каждый коэффициент не более max{p, q}.

учитывая эти два факта, мы можем придумать алгоритм для восстановления любого рационального числа p/q, а не только между 0 и 1, применяя общий алгоритм для угадывания произвольных целых чисел n по одному, чтобы восстановить все коэффициенты в непрерывной дроби для p / q. Пока, однако, мы будем просто беспокоиться о числах в диапазоне (0, 1], так как логика обработки произвольных рациональных чисел может быть легко выполнена, учитывая это как подпрограмма.

в качестве первого шага предположим, что мы хотим найти наилучшее значение a1 так что 1 / a1 как можно ближе к p/q и1 - целое число. Для этого мы можем просто запустить наш алгоритм угадывания произвольных целых чисел, каждый раз принимая обратное. После этого произойдет одно из двух. Во-первых, мы могли бы по чистому совпадению обнаружить, что p/q = 1/k для некоторого целого числа k, и в этом случае мы закончили. Если нет, мы найдем, что p/q зажат между 1 / (a1 - 1) и 1/a0 на1. Когда мы делаем это, то мы начинаем работать на продолжении фракции на один уровень глубже, находя a2 такой, что p/q находится между 1 / (a1 + 1 / a2) и 1/(a1 + 1/(a2 + 1)). Если мы волшебным образом найдем p / q, это здорово! В противном случае, мы затем идем на один уровень ниже в продолжающейся фракции. В конце концов, мы найдем номер таким образом, и это не займет много времени. Каждый двоичный поиск для поиска коэффициента занимает не более O(lg(p + q)) времени, и есть не более O(lg(p + q)) уровней для поиска, поэтому нам нужно только O (lg2(p + q)) арифметические операции и зонды для восстановления p/q.

одна деталь, которую я хочу отметить, заключается в том, что нам нужно отслеживать, находимся ли мы на нечетном или четном уровне при выполнении поиска, потому что, когда мы сэндвич p / q между двумя продолжая дроби, нам нужно знать, был ли коэффициент, который мы искали, верхней или нижней фракцией. Я заявляю без доказательств, что дляЯ С I нечетным вы хотите использовать верхний из двух чисел, и сЯ даже вы используете нижний из двух чисел.

Я почти на 100% уверен, что этот алгоритм работает. Я собираюсь попытаться написать более формальное доказательство этого, в котором я заполняю все пробелы в этом рассуждении, и когда я это сделаю Я отправлю ссылку здесь.

спасибо всем за вклад идеи, необходимые для того, чтобы это решение работало, особенно Jason S для предложения бинарного поиска по продолжающимся дробям.


помните, что любое рациональное число в (0, 1) может быть представлено в виде конечной суммы различных (положительных или отрицательных) дробей. Например, 2/3 = 1/2 + 1/6 и 2/5 = 1/2 - 1/10. Вы можете использовать это для выполнения прямого двоичного поиска.


вот еще один способ сделать это. Если есть достаточный интерес, я попытаюсь заполнить детали Сегодня вечером, но я не могу сейчас, потому что у меня есть семейные обязанности. Вот заглушка реализации, которая должна объяснить алгоритм:

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)

и вот объяснение. Что?!--2--> должен сделать, это найти последнее приближение непрерывной дроби к x со знаменателем самое большее bound. Этот алгоритм будет принимать шаги polylog для завершения и находит очень хорошие (хотя и не всегда лучшие) приближения. Так для каждого bound мы получим что-то близкое к двоичному поиску через все возможные фракции этого размера. Иногда мы не найдем определенной доли, пока не увеличим границу дальше, чем должны, но мы не будем далеко.

так что у вас есть. Логарифмическое число вопросов, найденных в работе polylog.

обновление: и полный рабочий код.

#! /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])

это кажется немного более эффективным в догадках, чем предыдущее решение, и выполняет намного меньше операций. Для 101/1024 потребовалось 19 догадок и 251 операция. Для.98765 потребовалось 27 догадок и 623 операции. Для 0.0123456789 потребовалось 66 догадок и 889 операций. А для хихиканья и ухмылок, для 0.01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 (это 10 копий предыдущего) потребовалось 665 догадок и 23289 оперативный.


вы можете сортировать рациональные числа в заданном интервале, например, по паре (знаменатель, числитель). Тогда играть в игру вы можете

  1. найти интервал [0, N] через удвоение-шаг подход
  2. заданного интервала [a, b] стрелять по рациональному с наименьшим знаменателем в интервале, который ближе всего к центру интервала

это, однако, вероятно, еще O(log(num/den) + den) (не знаете, и это слишком рано утром здесь чтобы я ясно думал ; -))