Гра «вгадай число» для довільних раціональних чисел?


77

Одного разу я отримав таке запитання для співбесіди:

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

Цю проблему можна вирішити за допомогою модифікованого двійкового пошуку, в якому ви перераховуєте повноваження двох, поки не знайдете той, що перевищує n, а потім запускаєте стандартний двійковий пошук за цим діапазоном. На мою думку, це круто в тому, що ви можете шукати нескінченний простір для певного числа швидше, ніж просто груба сила.

Однак у мене питання полягає в невеликій модифікації цієї проблеми. Замість того, щоб вибрати натуральне ціле число, припустимо, що я вибираю довільне раціональне число між нулем та одиницею. Моє запитання: за яким алгоритмом ви можете найбільш ефективно визначити, яке раціональне число я вибрав?

Зараз найкраще рішення, яке я маю, може знайти p / q щонайбільше за час O (q) шляхом неявного проходження дерева Стерна-Броко , бінарного дерева пошуку за всіма обґрунтуваннями. Однак я сподівався наблизити час виконання до часу виконання, який ми отримали для цілочисельного випадку, можливо щось на зразок O (lg (p + q)) або O (lg pq). Хто-небудь знає спосіб отримати такий тип роботи?

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


3
Хм, ти усвідомлюєш, що без певних обмежень це неможливо, оскільки в будь-якому діапазоні існує нескінченно багато раціональних чисел? Ви також не можете шукати необмежене ціле число; припустимо, число було якимсь випадковим числом із 10 ^ 1000 цифр? «100» - «вище». "1000" - "вище". "Один мільйон" - "вище". - Один трильйон? "Вища". - Гугол ? "Вище!"
Tom Zych

16
@Tom - Враховуючи будь-яке число (наприклад, 10 ^ 1000), алгоритм знайде його за скінчену кількість часу (навіть якщо це дуже тривалий час). Це відрізняється від того, що сказати, що алгоритм може вгадати будь-яке число за t кроків (для деякого фіксованого значення t), але ніхто не заявляв цього твердження.
Сет

6
@Tom Zych- якщо ви вибрали будь-яке кінцеве ціле число, врешті-решт я зможу його знайти, неодноразово подвоюючи. Це може зайняти смішний проміжок часу, але я все одно можу зробити це в часі, пропорційно логарифму вашого числа. У цьому я припускаю, що людина, яка відповідає на запитання, чесно представляє число, а не просто ухиляється, відповідаючи так, що ніколи не закінчується.
templatetypedef

Цікавий алгоритм. Всі обгрунтування зі знаменником N розташовані до рівня N дерева (або в ньому), тому O (q) однозначно можливий
доктор Белісаріус

4
@ Усі: Я просто хотів би сказати, що це було цікаве питання з деякими акуратними відповідями та обговореннями. Мій внутрішній ботанік з математики щасливий.
Сет

Відповіді:


49

Гаразд, ось моя відповідь, використовуючи лише продовжені дроби .

Спочатку давайте розберемося з деякою термінологією.

Нехай X = p / q - невідома частка.

Нехай Q (X, p / q) = знак (X - p / q) є функцією запиту: якщо воно дорівнює 0, ми вгадали число, а якщо це +/- 1, що повідомляє нам про ознаку помилки .

Звичайні позначення для ланцюгових дробів А = [а 0 ; a 1 , a 2 , a 3 , ... a k ]

= a 0 + 1 / (a 1 + 1 / (a 2 + 1 / (a 3 + 1 / (... + 1 / a k ) ...)))


Ми дотримуватимемось наступного алгоритму для 0 <p / q <1.

  1. Ініціалізуємо Y = 0 = [0], Z = 1 = [1], k = 0.

  2. Зовнішній цикл : передумовами є те, що:

    • Y і Z є продовженими частками k + 1 доданків, які ідентичні, за винятком останнього елемента, де вони відрізняються на 1, так що Y = [y 0 ; y 1 , y 2 , y 3 , ... y k ] і Z = [y 0 ; y 1 , y 2 , y 3 , ... y k + 1]

    • (-1) k (YX) <0 <(-1) k (ZX), або простіше кажучи, для k парних, Y <X <Z і для k непарних, Z <X <Y.

  3. Розширте ступінь продовження дробу на 1 крок, не змінюючи значень чисел. Загалом, якщо останніми членами є y k та y k + 1, ми змінюємо це на [... y k , y k + 1 = ∞] та [... y k , z k + 1 = 1]. Тепер збільште k на 1.

  4. Внутрішні цикли : Це, по суті, те саме, що запитання інтерв'ю @ templatetypedef про цілі числа. Ми робимо двофазний двійковий пошук, щоб стати ближче:

  5. Внутрішня петля 1 : y k = ∞, z k = a, а X знаходиться між Y і Z.

  6. Останній доданок подвійного Z: Обчисли M = Z, але з m k = 2 * a = 2 * z k .

  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. y k = b, z k = a, а X знаходиться між Y і Z.

  12. Якщо a і b відрізняються на 1, поміняйте місцями Y та Z, перейдіть до кроку 2.

  13. Виконайте двійковий пошук: обчисліть M, де m k = поверх ((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!

На кожному кроці обчислення М діапазон інтервалу зменшується. Ймовірно, досить легко довести (хоча я цього робити не буду), що інтервал зменшується в два рази щонайменше на 1 / sqrt (5) на кожному кроці, що свідчить про те, що цей алгоритм є O (log q) кроками.

Зверніть увагу, що це можна поєднати з оригінальним запитанням інтерв'ю templatetypedef і застосувати до будь-якого раціонального числа p / q, а не лише від 0 до 1, спочатку обчислюючи Q (X, 0), потім для позитивних / негативних цілих чисел, обмежуючи між двома послідовними цілих чисел, а потім використовуючи наведений алгоритм для дробової частини.

Коли у мене буде шанс наступного, я опублікую програму python, яка реалізує цей алгоритм.

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

редагування 2 : Рекурсивне визначення часткових наближень:

Якщо A k = [a 0 ; a 1 , a 2 , a 3 , ... a k ] = p k / q k , тоді p k = a k p k-1 + p k-2 і q k = a k q k-1 + q k-2 . (Джерело: Niven & Zuckerman, 4-е видання, теореми 7.3-7.5. Див. Також Вікіпедію )

Приклад: [0] = 0/1 = p 0 / q 0 , [0; 7] = 1/7 = p 1 / q 1 ; так [0; 7, 16] = (16 * 1 + 0) / (16 * 7 + 1) = 16/113 = р 2 / q 2 .

Це означає, що якщо два продовжених дробу Y і Z мають однакові доданки, крім останнього, а продовжений дріб, за винятком останнього доданка, є p k-1 / q k-1 , то ми можемо записати Y = (y k p k- 1 + p k-2 ) / (y k q k-1 + q k-2 ) і Z = (z k p k-1 + p k-2 ) / (z k q k-1 + q k-2 ). З цього повинно бути можливо показати, що | YZ | зменшується щонайменше в 1 раз на квадратний інтервал часу (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 обробляє математику великих цілих чисел з самого початку, і ця програма використовує лише цілочисельну математику (крім інтервальних обчислень), вона повинна працювати для довільних обґрунтувань.


редагувати 3 : Контур доказу, що це O (log q), а не O (log ^ 2 q):

Спочатку зауважимо, що поки не знайдено раціональне число, кількість кроків n k для кожного нового терміну, що продовжується, дорівнює рівно 2b (a_k) -1, де b (a_k) - це кількість бітів, необхідних для представлення a_k = ceil (log2 (a_k )): це b (a_k) кроки, щоб розширити "мережу" двійкового пошуку, і b (a_k) -1 кроки, щоб звузити його). Див. Приклад вище, ви зауважите, що кількість кроків завжди становить 1, 3, 7, 15 тощо.

Тепер ми можемо використовувати рекуррентне відношення q k = a k q k-1 + q k-2 та індукцію, щоб довести бажаний результат.

Сформулюємо це так: що значення q після N k = sum (n k ) кроків, необхідних для досягнення k-го члена, має мінімум: q> = A * 2 cN для деяких фіксованих констант A, c. (щоб інвертувати, ми отримаємо, що кількість кроків N дорівнює <= (1 / c) * log 2 (q / A) = O (log q).)

Базові випадки:

  • k = 0: q = 1, N = 0, отже, q> = 2 N
  • k = 1: для N = 2b-1 кроків, q = a 1 > = 2 b-1 = 2 (N-1) / 2 = 2 N / 2 / sqrt (2).

Це означає, що A = 1, c = 1/2 може забезпечити бажані межі. Насправді q не може подвоювати кожен термін (контрприклад: [0; 1, 1, 1, 1, 1] має коефіцієнт зростання phi = (1 + sqrt (5)) / 2), тож давайте використовувати c = 1 / 4.

Індукція:

  • для терміну k, q k = a k q k-1 + q k-2 . Знову ж таки, для n k = 2b-1 кроків, необхідних для цього терміну, a k > = 2 b-1 = 2 (n k -1) / 2 .

    Отже, a k q k-1 > = 2 (N k -1) / 2 * q k-1 > = 2 (n k -1) / 2 * A * 2 N k-1 /4 = A * 2 N k / 4 / sqrt (2) * 2 n k / 4 .

Арг - важка частина тут полягає в тому, що якщо k = 1, q може не сильно збільшитися за цей один член, і нам потрібно використовувати q k-2, але це може бути набагато менше, ніж q k-1 .


Отже, це виглядає дійсно чудово, але я не думаю, що це O (lg q). Будь-яка окрема ітерація внутрішнього циклу виконується з кроками O (lg q), коли ви використовуєте модифікований двійковий пошук для відновлення наступного числа продовжуваної дроби, але пам’ятайте, що існують O (lg q) ітерації цього циклу, оскільки є ( у гіршому випадку) O (lg q) числа в частковій частці. Це змушує мене думати, що натомість це час O (lg ^ 2 q). Однак це все-таки відмінне рішення проблеми, і незалежно від того, чи це час O (lg q) або O (lg ^ 2 q), це все одно експоненціально краще, ніж те, що я мав раніше.
templatetypedef

Я знаю, що це виглядає як O (lg ^ 2 q) через дві петлі, але це, мабуть, консервативно. Я спробую це довести.
Jason S

+1: Не перевіряв деталі, але зараз я вірю, що підхід МВ працює.

Вам потрібно спробувати показати, що | YZ | зменшується геометрично. Як зазначає typedeftemplate у своїй відповіді, у CF є терміни O (log q), кожен із розміром не більше q. Отже, якщо ви зробите кроки O (log q), щоб збільшити "ступінь" вашого CF на 1, ви берете загальну кількість O (log ^ 2 q).

Ні, ви не можете оцінити його як O (log ^ 2 q) через дві петлі; це надмірно консервативно. Якщо ви зробите кроки O (log q) для збільшення числа доданків дробу, що продовжується, то термін цієї частки дуже великий, а інтервал буде дуже малим. Кожна ітерація внутрішнього циклу також зменшує інтервал, а не лише збільшення довжини дробу, що продовжується.
Jason S

6

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

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)nO(2*log(n)) = O(log(n))

Тому моя ідея полягала в тому, щоб скласти кілька, шукати, шикувати більше, шукати тощо. Кожного разу, коли ми вишикуємо більше, ми вишиковуємось приблизно вдвічі більше, ніж у минулий раз. Тож нам потрібна ще одна здогадка, ніж минулого разу. Тому наш перший прохід використовує 1 здогад, щоб пройти 1 можливий раціонал. Наш другий використовує 2 здогадки, щоб пройти 3 можливі обґрунтування. Наш третій використовує 3 здогадки, щоб пройти 7 можливих обґрунтувань. І наш k'' використовує kздогади, щоб обернути можливі обгрунтування. Для будь-якого конкретного раціонального2k-1m/n , врешті-решт, він закінчиться внесенням цього раціонального до досить великого списку, для якого він знає, як ефективно виконувати двійковий пошук.

Якщо ми виконували двійкові пошуки, а потім проігнорували все, про що дізналися, коли взяли більше обґрунтувань, тоді б розмістили всі обгрунтування до включно 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 здогадок і приблизно секунду. (Зверніть увагу, якщо ви називаєте програму раціональним числом як аргумент, вона заповнить усі ваші здогади. Це дуже корисна зручність.)


Я не впевнений, що розумію, що ти кажеш. Ви можете це пояснити?
templatetypedef

@templatetypedef: Що незрозуміло? Перше наше припущення - це завжди 1/2. Припустимо, що відповідь повертається нижче. Наступні 3 числа у списку, що відповідають умові, - це 1/3, 1/4та 1/5. Тож ми здогадуємось 1/4далі, потім або 1/3або 1/5за наступною здогадкою. Якщо ми продовжимо, ми захопимо 7 чисел у нашому діапазоні і встановимо наступні 3 здогадки. Після цього ми візьмемо 15 і встановимо наступні 4 здогадки. тощо. Що в цьому незрозуміло? Я зараз лягаю спати. Якщо ви все ще не розумієте вранці, я напишу програму для вгадування, і ви побачите, як це працює.
btilly

3
@btilly: Куди входять 7-3, 15-4, (31-5?)? Яка логіка, що стоїть перед такою чергою цифр "вгадуй далі"?
stakx - більше не вносить вклад

2
@Btilly: +1, але схоже, ви насправді не вирішили головну проблему. Ви генеруєте обгрунтування Theta (q) і виконуєте двійковий пошук за ними. Отже, час виконання - Omega (q), навіть якщо ви приймаєте запити O (log ^ 2 q). Насправді у Сет дуже подібний алгоритм (і якщо ви уважно читаєте, він не запитує p + q). ІМО, основною проблемою, яку слід вирішити тут, є генерація обгрунтування O (полілог (q)), а не спроба зберегти кількість запитів O (полілог (q)) незалежно від інших накладних витрат на ведення бухгалтерії.

1
@Seth: Це по-маленькому, а не Біллі :-) q або p + q, не має значення, як p <q.

4

Я отримав його! Що вам потрібно зробити, це використовувати паралельний пошук з бісекцією та безперервними дробами .

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

Як ви запускаєте їх паралельно, виглядає наступним чином.

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

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

Наш постійний пошук дробу повинен округляти до найближчого цілого числа, а не використовувати мінімум (це зрозуміліше нижче).

Вище виглядає як ручний рух. Давайте використаємо конкретний приклад 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, тож продовжуйте ділити на дві частини, query = 3/64.

  7. l = 1/32, u = 3/64, запит = 5/128 = 1/25.6

  8. l = 1/32, u = 5/128, запит = 9/256 = 1 / 28.4444 ....

  9. l = 1/32, u = 9/256, запит = 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).

[продовження, я повинен кудись поїхати]


Подальший роздум - продовження дробів - це шлях, не зважаючи на розділення, окрім як визначити наступний термін. Більше, коли я повернусь.


Ви точно щось намітили тут. Я думаю, що підхід може полягати лише у використанні загального алгоритму "Я думаю про ціле число" для обчислення одного доданка тривалого дробу за раз. Я не фахівець у частинах, що продовжуються, але, на мою думку, у поданні є лише логарифмічно багато термінів, і якщо ця процедура спрацює, це буде спосіб генерувати кожен із цих термінів по одному, використовуючи логарифмічний час для кожного з їх. Я все подумаю.
templatetypedef

Так, я повністю згоден - CF - це найпростіша і, мабуть, найефективніша відповідь, просто використовуючи цілочисельний пошук для кожного терміна. Я збирався поставити це як власну відповідь, але @Jason побив мене.
мокус

1
Не існує чітко визначеного найближчого раціонального числа до даного дійсного (крім нього самого). Що робить цей підхід, не зовсім зрозуміло, можливо, його потрібно більше докласти.

@Moron: Ознайомтесь із наближеннями продовжуваних дробів. (наприклад, Теорія чисел, Нівен та Цукерман) Вони утворюють найближчі раціональні числа для обмеженого знаменника, а саме: якщо p / q - наближення дробу дійсного числа r, то | r - (p / q) | <= C / (q ^ 2), де я забуваю, що таке C, я думаю, це 1/5 або 1 / sqrt (5).
Jason S

Наприклад, lі uмати однаковий МВ до певного моменту не обов'язково означає, що число, про яке ви здогадуєтесь, також має однакові збіжні ... (якщо я правильно зрозумів ваш підхід).

3

Здається, я знайшов алгоритм O (log ^ 2 (p + q)).

Щоб уникнути плутанини в наступному абзаці, під "запитом" мається на увазі, коли вгадувач дає претендентові здогадку, а той, хто відповідає, відповідає "більшим" або "меншим". Це дозволяє мені зарезервувати слово "здогадка" для чогось іншого, припущення для p + q, яке не задається безпосередньо претенденту.

Ідея полягає в тому, щоб спочатку знайти p + q, використовуючи алгоритм, який ви описуєте у своєму запитанні: відгадайте значення k, якщо k занадто мало, подвойте його та спробуйте ще раз. Потім, як тільки у вас є верхня та нижня межі, виконайте стандартний двійковий пошук. Для цього приймаються запити O (log (p + q) T), де T - верхня межа кількості запитів, необхідних для перевірки здогадки. Знайдемо Т.

Ми хочемо перевірити всі дроби r / s з r + s <= k, і подвоїти k, поки k не буде достатньо великим. Зверніть увагу, що є дроби O (k ^ 2), які потрібно перевірити на наявність значення k. Створіть збалансоване двійкове дерево пошуку, що містить усі ці значення, а потім виконайте пошук, щоб визначити, чи є p / q у дереві. Для підтвердження того, що p / q відсутній у дереві, потрібні запити O (log k ^ 2) = O (log k).

Ми ніколи не вгадаємо значення k, яке перевищує 2 (p + q). Звідси ми можемо взяти T = O (log (p + q)).

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

Загальна кількість запитів тоді O (log ^ 2 (p + q)).


Насправді побудова дерева пошуку займе K ^ 2log K часу. Можливо, вам слід покращити цей крок, щоб дійсно зайняти час O (log k). Крім того, як тільки у вас є кандидат k, ви повинні повернути "більший / менший" за нього, а не просто "існує / не існує". Як ви це робите?
Eyal Schneider

Будь ласка, нехтуйте другою частиною мого попереднього коментаря;) Якщо зовнішній цикл виконує подвоєння, то внутрішня частина повинна перевірити лише відповідність / відсутність відповідності.
Eyal Schneider

Це хороший алгоритм для #gueses, що мають значення O (log ^ 2 (p + q)), але не для складності часу обчислення O (log ^ 2 (p + q)). Який тип складності вимагає ОП?
Eyal Schneider

Я шукаю щось (в ідеалі) з обома властивостями. Це, безумовно, хороший початок з точки зору мінімізації кількості запитів, хоча в ідеалі я хотів би чогось, що також мінімізує обчислення. Знову ж таки, це може бути теоретично оптимальним!
templatetypedef

1
@billy: Алгоритм не задає безпосередньо p + q питань. Для даного k він перевіряє (використовуючи двійковий пошук) усі дроби r / s з r + s <= k. Якщо p + q <= k, воно знаходить відповідь; інакше ми знаємо p + q> k, тому подвоюємо k.
Сет

3

Гаразд, я думаю, що я розгадав алгоритм O (lg 2 q) для цієї проблеми, який базується на найкращому розумінні Джейсона С. про використання безлічі дробів. Я думав, що допрацюю алгоритм до кінця прямо тут, щоб ми отримали повне рішення, разом з аналізом виконання.

Інтуїція алгоритму полягає в тому, що будь-яке раціональне число p / q в межах діапазону можна записати як

a 0 + 1 / (a 1 + 1 / (a 2 + 1 / (a 3 + 1 / ...))

Для відповідного вибору i . Це називається дробом, що продовжується . Що ще важливіше, хоча ці i i можна отримати, запустивши алгоритм Евкліда на чисельнику та знаменнику. Наприклад, припустимо, що ми хочемо представити 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. Кожен коефіцієнт не більше максимуму {p, q}.

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

Спочатку припустимо, що ми хочемо знайти найкраще значення 1 так, щоб 1 / a 1 було якомога ближче до p / q, а 1 - цілим числом. Для цього ми можемо просто запустити наш алгоритм вгадування довільних цілих чисел, беручи зворотне кожен раз. Після цього відбудеться одна з двох речей. По-перше, ми за чистим збігом обставин можемо виявити, що p / q = 1 / k для деякого цілого числа k, і в цьому випадку ми закінчили. Якщо ні, ми виявимо, що p / q перебуває між 1 / (a 1 - 1) та 1 / a 0 для деяких a 1 . Коли ми це робимо, то починаємо працювати над продовжуваною часткою на один рівень глибше, знаходячи a 2 таким, що p / q знаходиться між 1 / (a ) та 1 / (a 1 + 1 / a2 1 + 1 / (a 2 + 1)). Якщо ми чарівно знаходимо p / q, це чудово! В іншому випадку ми просуваємось на один рівень далі у продовжуваному дробі. Зрештою, ми знайдемо номер таким чином, і це не може зайняти занадто багато часу. Кожен двійковий пошук, щоб знайти коефіцієнт, займає не більше O (lg (p + q)) часу, і для пошуку існує не більше O (lg (p + q)) рівнів, тому нам потрібен лише O (lg 2 (p + q)) арифметичні операції та зонди для відновлення p / q.

Одну деталь, яку я хочу зазначити, полягає в тому, що нам потрібно відстежувати, чи знаходимось ми на непарному чи парному рівні, коли виконуємо пошук, тому що коли ми складаємо p / q між двома продовжуваними дробами, ми повинні знати, чи коефіцієнт ми шукали верхню або нижню фракцію. Я без доказів зазначу, що для i з i непарним ви хочете використовувати верхнє з двох чисел, а з i навіть ви використовуєте нижнє з двох чисел.

Я майже на 100% впевнений, що цей алгоритм працює. Я спробую написати більш офіційний доказ цього, в якому я заповнюю всі прогалини в цих міркуваннях, і коли я це зроблю, я розміщу тут посилання.

Дякуємо усім за внесок у інформацію, необхідну для того, щоб це рішення запрацювало, особливо Джейсону С за пропозицію двійкового пошуку за продовженими дробами.


Щойно побачив це, не мав можливості уважно прочитати це, але ти, мабуть, маєш рацію.
Jason S

... хоча я думаю, що це log (q), а не log ^ 2 (q).
Jason S

Я вважаю, що це правильно. Для підтвердження див. Мій коментар до першої відповіді Джейсона.

Насправді, я думаю, що ми маємо доказ, що це O (log q). Дивіться мій коментар до другої відповіді Джейсона.

2

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


2
Не могли б ви детальніше пояснити, як алгоритм використовував би цей факт?
Сет

Ви говорите про єгипетські фракції?
Гейб,

2

Ось ще один спосіб зробити це. Якщо буде достатньо інтересу, я спробую заповнити деталі сьогодні ввечері, але наразі не можу, бо я маю сімейні обов’язки. Ось заглушка реалізації, яка повинна пояснити алгоритм:

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,0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 (це 10 примірників попередньої), потрібно було 665 здогадок і 23289 операцій.


@ jason-s: Краще заповнити зараз. Я з нетерпінням чекаю порівняння з вашим, коли у вас є код. Твоїм, безумовно, буде потрібно менше операцій, я не маю сенсу, чиї вимагатимуть менше здогадок.
btilly

0

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

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

однак це, мабуть, все ще O(log(num/den) + den)(не впевнений і тут рано вранці, щоб змусити чітко подумати ;-))

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.