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


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

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

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

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

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

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

8 75

8 ответов:

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

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

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

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

The условные обозначения для непрерывных дробей 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. внешний цикл: The предпосылки разве что:

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

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

  3. расширить степень непрерывной дроби на 1 шаг без изменения значений чисел. В общем случае, если последние члены yk и yk + 1, мы переходим на [... гk, yk+1=∞] и [... гk, 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 что реализует данный алгоритм.

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

edit 2: рекурсивное определение частных аппроксимаций:

Если Ak = [a0; a1, a2, а3, ... аk] = pk / qk, затем pk = akpk-1 + pк-2, и qk = akqk-1 + qк-2. (Источник: Niven & Zuckerman, 4th ed, теоремы 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 имеют одинаковые члены, кроме последнего, а непрерывная дробь, исключая последний член, равна pk-1 / qk-1, то мы можем написать Y = (ykpk-1 + pк-2) / (ykqk-1 + вопроск-2) и Z = (zkpk-1 + pк-2) / (zkqk-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):

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

теперь мы можем использовать рекуррентное отношение qk = akqk-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 = akqk-1 + qк-2. Опять же, для nk = 2b-1 шаги, необходимые для этого термина, ak >= 2Б-1 = 2(nk-1)/2.

    такkqk-1 >= 2(Nk-1)/2 * qk-1 >= 2(nk-1)/2 * A * 2Nk-1/4 = В*2Nk/4/sqrt(2)*2nk/4.

Argh -- трудная часть здесь заключается в том, что еслиk = 1, q не может сильно увеличиться для этого одного термина, и нам нужно использовать qк-2 но это может быть гораздо меньше, чем Qk-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)) действия. Проблема в том, что у нас есть бесконечное число возможных рациональных решений для поиска. Так мы не можем просто выстроить их всех, заказать и начать поиск.

поэтому моя идея состояла в том, чтобы выстроить несколько, поиск, выстроить больше, поиск и так далее. Каждый раз, когда мы выстраиваемся в очередь, мы выстраиваемся примерно вдвое больше, чем в прошлый раз. Так что нам нужно еще одно предположение, чем в прошлый раз. Поэтому наш первый проход использует 1 догадку, чтобы пересечь 1 возможный рациональный. Наш второй использует 2 догадки для пересечения 3 возможных рациональных значений. Наш третий использует 3 догадки, чтобы пересечь 7 возможных рациональных значений. И наши 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 догадок и около секунды, чтобы сгенерировать их. (Обратите внимание, если вы вызываете программу с рациональным числом в качестве аргумента, она заполнит все догадки для вас. Это очень полезное удобство.)

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

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

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

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

так как оба метода увеличивают знаменатель по крайней мере на постоянный коэффициент (деление идет по факторам 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, запрос = 1/4.

  3. l = 0, u = 1/4, запрос = 1/8.

  4. l = 0, u = 1/8, запрос = 1/16.

  5. l = 0, u = 1/16, запрос = 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, запрос = 33/1024 = 1/31. 0303... (круг 1/31)

  11. l = 33/1024, u = 17/512, запрос = 67/2048 = 1/30. 5672... (круглый к 1/31)

  12. l = 33/1024, u = 67/2048. В этот момент и l, и u имеют один и тот же член непрерывной дроби 31, поэтому теперь мы используем предположение о непрерывной дроби. запрос = 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 / ...))

для соответствующих вариантов aЯ. Это называется продолжение фракция. Что еще более важно, хотя этиЯ может быть получен путем выполнения евклидова алгоритма на числителе и знаменателе. Например, предположим, что мы хотим представить 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 и a1 - целое число. Для этого мы можем просто запустить наш алгоритм для угадывания произвольных целых чисел, принимая взаимное каждый раз. После этого произойдет одна из двух вещей. Во-первых, мы могли бы по чистой случайности обнаружить, что 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 с(Р + Q)) время, и там в лучшем случае за o(LG с(Р + Q)) уровни на поиск, так что нам нужно только o(ГПВ2(p + q)) арифметические операции и зонды для восстановления p/q.

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

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

спасибо всем, кто внес свой вклад в понимание, необходимое для работы этого решения, особенно Джейсону С за предложение бинарного поиска по непрерывным дробям.

помните, что любое рациональное число в (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)

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

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

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

#! /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.0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 (то есть 10 копий предыдущего) потребовалось 665 догадок и 23289 оперативный.

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

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

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