“猜数字”游戏中是否存在任意有理数?


76

我曾经有以下问题作为面试问题:

我正在考虑一个正整数n。提出一种可以在O(lg n)查询中进行猜测的算法。每个查询都是您选择的数字,我将回答“较低”,“较高”或“正确”。

此问题可以通过修改后的二进制搜索来解决,在该搜索中,您列出了2的幂,直到找到一个超过n的幂,然后在该范围内运行标准的二进制搜索。我认为这很酷,因为您可以比暴力破解更快地搜索无限数量的特定数字。

不过,我的问题是对此问题进行了轻微的修改。假设不选择一个正整数,而是选择一个介于零和一之间的任意有理数。我的问题是:您可以使用哪种算法最有效地确定我选择了哪个有理数?

现在,我拥有的最佳解决方案可以在最多O(q)的时间内隐式遍历所有有理数的二叉搜索树Stern-Brocot树,从而找到p / q 。但是,我希望使运行时更接近整数情况下的运行时,例如O(lg(p + q))或O(lg pq)。有人知道获得这种运行时的方法吗?

我最初考虑使用间隔[0,1]的标准二进制搜索,但这只会找到具有非重复二进制表示形式的有理数,而这几乎错过了所有有理数。我还考虑过使用其他方式来列举有理数,但是仅凭比较大/相等/少的比较,我似乎找不到找到这种空间的方法。


3
嗯,您确实意识到,没有任何限制是不可能的,因为在任何范围内都有无限多个有理数?您也不能真正搜索无限的整数。假设该数字是某个随机数字,具有10 ^ 1000个数字?“ 100”-“更高”。“ 1000”-“更高”。“一百万”-“更高”。“一万亿?” “更高。” “一个googol?” “更高!”
Tom Zych

16
@Tom-给定任何数字(例如10 ^ 1000),该算法将在有限的时间内找到它(即使时间很长)。这与说算法可以在t步内猜出任何数字(对于t的某个固定值)不同,但是没有人提出这一要求。
塞斯(Seth)

6
@Tom Zych-如果选择任何有限整数,最终我可以通过反复加倍找到它。这可能会花费很多时间,但是我仍然可以按与您的数字对数成正比的时间进行处理。在这种情况下,我假设回答问题的人诚实地表示人数,并且通过以永不终止的方式进行回答而不仅仅是回避。
templatetypedef

有趣的算法。分母为N的所有有理数都位于树的N之前(或之中),因此显然有可能实现O(q)
belisarius博士2011年

4
@Everyone:我想说的是,这是一个有趣的问题,有一些简洁的响应和讨论。我内心的数学书呆子很高兴。
塞斯,

Answers:


49

好的,这是我仅使用连续分数的答案。

首先让我们在这里获得一些术语。

令X = p / q为未知分数。

令Q(X,p / q)= sign(X-p / q)为查询函数:如果为0,则我们猜出了这个数字,而如果它是+/- 1,则告诉我们错误的迹象。

连续分数常规表示法是A = [a 0 ; a 1,a 2,a 3,... a k ]

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


对于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. Double 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 = 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:部分近似的递归定义:

如果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,第四版,定理7.3-7.5。另请参见Wikipedia

示例:[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 = p 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 / 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数学,并且该程序仅使用整数数学(区间计算除外),因此它应适用于任意有理数。


编辑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)步扩大二进制搜索的“ net”,b(a_k)-1步缩小它。参见上面的示例,您会注意到步骤数始终为1、3、7、15等。

现在,我们可以使用递归关系q k = a k q k-1 + q k-2和归纳法证明所需的结果。

让我们以这种方式陈述它:达到第k个项所需的N k = sum(n k)个步骤之后的q值具有最小值:对于某些固定常数A,c,q> = A * 2 cN。(因此,反过来,我们将得到步骤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,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

Argh-这里最困难的部分是,如果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),但这可能是保守的。我会尝试证明这一点。
詹森·S

+1:没有检查细节,但是我现在相信CF方法会起作用。

您需要尝试证明| YZ | 几何减小。正如typedeftemplate在他的回答中提到的,CF中有O(log q)个项,每个项的大小最多为q。因此,如果采取O(log q)步骤将CF的“度”增加1,则总共采取O(log ^ 2 q)步骤。

不,由于两个循环,您不能将其评估为O(log ^ 2 q);这太保守了。如果您采取O(log q)步骤来增加连续分数的项数,则该分数的项将非常大,并且间隔将非常小。内循环的每次迭代也会减小间隔,而不仅是连续分数长度的增加。
杰森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个可能的理性。我们kk猜测是使用猜测来遍历可能的理性。对于任何特定的有理数,最终它将最终将该有理数放到一个很大的列表中,它知道如何高效地执行二进制搜索。2k-1m/n

如果我们这样做的二进制搜索,则忽略,当我们攫取更多的有理数,我们会学到的一切,那么我们就会把所有的有理数直至并包括m/nO(log(n))通行证。(这是因为到那时为止,我们将获得具有足够的理性的通行证,以包括直到并包括在内的所有理性m/n。)但是,每一次通行证都需要更多的猜测,所以这就是猜测。O(log(n)2)

但是,我们实际上做得比这好得多。有了我们的第一个猜测,我们就消除了清单上一半的过大或过小的理性。接下来的两个猜测并未将空间缩小到四分之一,但是距离它们并不太远。接下来的3个猜测并没有将空间缩小到八分之一,但是距离它们并不太远。等等。当你把它放在一起,我相信结果是你找到m/nO(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/31/41/5。所以我们猜测1/4未来,那么无论1/31/5以下的猜测。如果继续,我们将在我们的范围内获取7个数字并设置下3个猜测。之后,我们将抓取15个并设置接下来的4个猜测。等。还有什么不清楚的地方?我要去睡觉了。如果您仍然早上不明白,我将编写一个程序进行猜测,您将了解其工作原理。
btilly 2011年

3
@btilly:7-3、15-4(31-5)从哪里来?这样的“猜测-下一个”数字队列背后的逻辑是什么?
stakx-不再贡献

2
@Btilly:+1,但看来您尚未真正解决主要问题。您生成Theta(q)有理并对其进行二元搜索。因此,即使您执行O(log ^ 2 q)查询,运行时也是Omega(q)。实际上,塞思有一个非常相似的算法(如果您仔细阅读,他并不是在查询p + q)。IMO,这里要解决的主要问题是生成O(polylog(q))有理数,而不是尝试保持查询数O(polylog(q))而不考虑其他簿记开销。

1
@Seth:btilly,不是Billy :-) q或p + q没关系,因为p <q。

4

我懂了!您需要做的是使用带有二等分和连续分数的并行搜索。

二等分将给您一个特定的实数的极限,以2的幂表示,而连续的分数将取该实数并找到最接近的有理数。

您如何并行运行它们如下。

在每一步中,您都具有l并且u是二等分的上下边界。这个想法是,您可以在将二等分的范围减半和添加一个附加项作为连续分数表示之间进行选择。当lu都具有相同的下一个词作为连续分数时,则需要进行连续分数搜索的下一步,并使用连续分数进行查询。否则,请使用二等分将范围减半。

由于这两种方法都会使分母至少增加一个恒定因子(二等分变为2,连续分数至少达到phi =(1 + sqrt(5))/ 2),这意味着您的搜索应为O (log(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次数,因此继续平分。查询= 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击败了我。
mokus 2011年

1
对于给定的实数(除自身之外),没有明确定义的最接近的有理数。这种方法的作用还不是很清楚,也许需要更多的阐述。

@Moron:继续阅读分数近似值。(例如,数论,Niven和Zuckerman)它们形成了约束分母的最接近有理数,即,如果p / q是实数r的连续分数近似,则| r-(p / q)| <= C /(q ^ 2)我忘了C是什么,我认为它是1/5或1 / sqrt(5)。
杰森S

例如,l并且u具有相同的CF高达某些时候并不一定意味着你也猜数有相同的收敛...(如果我理解正确的方法)。

3

我想我找到了O(log ^ 2(p + q))算法。

为了避免在下一段中造成混淆,“查询”是指猜测者向挑战者进行猜测,而挑战者则回答“更大”或“较小”。这使我可以将“ guess”这个词保留给别的东西,即p + q的猜测,而不会直接询问挑战者。

想法是首先使用问题中描述的算法找到p + q:猜测值k,如果k太小,则将其加倍并重试。然后,一旦有了上限和下限,就进行标准的二进制搜索。这需要O(log(p + q)T)个查询,其中T是检查猜测所用查询数量的上限。让我们找到T。

我们要检查r + s <= k的所有分数r / s,并将k加倍,直到k足够大。请注意,您需要检查O(k ^ 2)个分数,以查看k的给定值。构建一个包含所有这些值的平衡二进制搜索树,然后对其进行搜索以确定p / q是否在树中。它需要O(log k ^ 2)= O(log k)查询来确认p / q不在树中。

我们永远不会猜到k的值大于2(p + q)。因此我们可以取T = O(log(p + q))。

当我们猜测k的正确值(即k = p + q)时,我们将在检查我们对k的猜测的过程中将查询p / q提交给挑战者,并赢得比赛。

那么查询总数为O(log ^ 2(p + q))。


实际上,构建搜索树将花费K ^ 2log K的时间。也许您应该改进此步骤以真正花费O(log k)的时间。另外,一旦有了候选k,就应该为其返回“更大/更小”,而不仅仅是“存在/不存在”。你怎么做呢?
Eyal Schneider

请忽略我先前评论的第二部分;)如果外部循环执行加倍操作,则内​​部部分仅需要检查是否匹配。
Eyal Schneider

这对于#guesses是O(log ^ 2(p + q))是一个很好的算法,但对于计算时间复杂度O(log ^ 2(p + q))来说却不是。OP要求哪种类型的复杂性?
Eyal Schneider

我正在寻找具有两个属性的东西(理想情况下)。从最小化查询数量的角度来看,这当然是一个不错的开始,尽管理想情况下,我希望也可以使所涉及的计算量最小化。再说一次,这在理论上可能是最佳的!
templatetypedef

1
@billy:该算法不会直接询问p + q问题。对于给定的k,它将检查(使用二进制搜索)r + s <= k的所有分数r / s。如果p + q <= k,则找到答案。否则我们知道p + q> k,所以我们将k加倍。
塞斯(Seth)

3

好的,我想我基于Jason S关于使用连续分数的最出色见解,针对这个问题找到了O(lg 2 q)算法。我以为我会在这里完全充实算法,以便我们拥有完整的解决方案以及运行时分析。

该算法的直觉是该范围内的任何有理数p / q都可以写成

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

对于合适的选择。这称为连续分数。更重要的是,尽管这些a i可以通过在分子和分母上运行欧几里得算法来得出。例如,假设我们要用这种方式表示11/14。我们首先注意到14变成11个零次,所以粗略的近似值为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的另一个很好的近似。现在,我们有三分之二,因此考虑的倒数,其为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连续分数中的系数始终是通过使用扩展的Euclidean算法形成的。这很棒,因为它告诉我们两件事:

  1. 最多可以有O(lg(p + q))个系数,因为欧几里得算法总是在许多步骤中终止,并且
  2. 每个系数最大为max {p,q}。

鉴于这两个事实,我们可以提出一种算法,通过应用通用算法一次猜测任意整数n一次来恢复所有有理数p / q,而不仅仅是恢复0到1之间的有理数p / q。 p / q的连续分数。不过,现在,我们只需要担心(0,1]范围内的数字,因为处理任意有理数的逻辑可以很容易地作为子例程来完成。

作为第一步,让我们假设我们想找到一个最佳值1,使得1 / A 1是尽可能接近到P / Q和1是整数。为此,我们可以运行我们的算法来猜测任意整数,每次取倒数。完成此操作后,将发生两件事之一。首先,我们可能完全偶然地发现,对于某些整数k,p / q = 1 / k,在这种情况下我们就完成了。如果不是,我们会发现,P / Q为1 /(一间夹1 - 1)和1 / A 0对一些1。当我们这样做时,然后我们通过找到a 2使p / q在1 /(a 1 + 1 / a之间)来更深地研究连续分数。2)和1 /(a 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建议对连续分数进行二进制搜索。


刚看到这个,还没有机会仔细阅读它,但是您可能是对的。
杰森S

...虽然我认为是log(q),而不是log ^ 2(q)。
詹森·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。该算法将采取polylog步骤完成并找到非常好的(尽管并不总是最好的)近似值。因此,对于每种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.0123456789012345678901234567890123456789012345678901234567890123456789012345678934563456012789789(这是前一个的10个副本),它需要665次猜测和23289次操作。


@Moron:这是给你的。
btilly 2011年

@ jason-s:现在最好填写一下。当您拥有代码时,我期待与您进行比较。您的肯定需要更少的操作,我不知道谁会需要更少的猜测。
btilly 2011年

0

您可以按给定间隔(例如,对(分母,分子))对有理数进行排序。然后玩游戏就可以

  1. [0, N]使用加倍方法找到间隔
  2. 给定有间隔[a, b]最小分母的有理区间,该区间是最接近区间中心的

但是,这可能仍然是O(log(num/den) + den)(不确定,现在太早了,无法让我清楚地思考;-))

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.