任意有理数的“猜数”游戏?

时间:2011-03-26 06:19:20

标签: algorithm math puzzle rational-numbers

我曾经接受过以下面试问题:

  

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

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

但我的问题是对这个问题稍作修改。假设我在0和1之间选择任意有理数,而不是选择正整数。我的问题是:您可以使用什么算法来最有效地确定我选择了哪个有理数?

现在,我所拥有的最佳解决方案是在最多O(q)时间内通过隐式地遍历所有有理数的Stern-Brocot tree二元搜索树来找到p / q。但是,我希望运行时更接近我们为整数情况得到的运行时,可能是O(lg(p + q))或O(lg pq)。有没有人知道如何获得这种运行时?

我最初考虑使用区间[0,1]的标准二进制搜索,但这只会找到具有非重复二进制表示的有理数,这几乎错过了所有的有理数。我还考虑过使用其他一些方法来列举有理数,但我似乎找不到一种方法来搜索这个空间给出更大/更小/更少的比较。

8 个答案:

答案 0 :(得分:49)

答案 1 :(得分: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)个分数,最大分母为n。因此,我们可以在O(2*log(n)) = O(log(n))步骤中搜索任何特定的分母大小。问题是我们有无数可能的搜索理由。所以我们不能把它们排成一行,订购它们,然后开始搜索。

因此我的想法是排队,搜索,排队,搜索等等。每次我们排队时,我们排队的比例是我们上次的两倍。所以我们需要比上次更多猜测。因此,我们的第一遍使用1猜测来遍历1个可能的理性。我们的第二个使用2个猜测来遍历3个可能的有理数。我们的第三个使用3个猜测来遍历7个可能的有理数。我们的k'使用k猜测来遍历2k-1可能的理性。对于任何特定的理性m/n,最终它会把这个理性放在一个相当大的列表上,它知道如何有效地进行二进制搜索。

如果我们进行了二元搜索,然后忽略了我们在获得更多理性时所学到的所有内容,那么我们就会将所有有理数放在m/n O(log(n))次传递中。 (那是因为到那时我们将通过足够的理性来包括每个理由,包括m/n。)但每次传递需要更多的猜测,因此这将是O(log(n)2)猜测。 / p>

然而我们实际上做得比这更好。通过我们的第一次猜测,我们将列表中的一半理性消除为太大或太小。我们接下来的两个猜测并没有将空间缩小到几分之一,但它们并没有太远。我们接下来的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次猜测,大约需要一秒钟来生成它们。 (注意,如果你用一个有理数字作为参数调用程序,它将为你填写所有的猜测。这是一个非常有用的便利。)

答案 2 :(得分:4)

我知道了!您需要做的是使用具有二分的并行搜索和continued fractions

Bisection将为您提供特定实数的限制,表示为2的幂,连续分数将取实数并找到最接近的有理数。

如何并行运行它们如下。

在每一步中,lu是二分的下限和上限。这个想法是,您可以选择将二分范围减半,并添加一个附加项作为连续分数表示。当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.   
  3. l = 0,u = 1/2,查询= 1/4。

  4.   
  5. l = 0,u = 1/4,查询= 1/8。

  6.   
  7. l = 0,u = 1/8,查询= 1/16。

  8.   
  9. l = 0,u = 1/16,查询= 1/32。

  10.   
  11. l = 1/32,u = 1/16。现在1 / l = 32,1 / u = 16,这些有不同的cfrac代表,所以保持二等分。,query = 3/64。

  12.   
  13. l = 1/32,u = 3/64,查询= 5/128 = 1 / 25.6

  14.   
  15. l = 1/32,u = 5/128,查询= 9/256 = 1 / 28.4444 ....

  16.   
  17. l = 1/32,u = 9/256,查询= 17/512 = 1 / 30.1176 ......(回合到1/30)

  18.   
  19. l = 1/32,u = 17/512,查询= 33/1024 = 1 / 31.0303 ......(回合到1/31)

  20.   
  21. l = 33/1024,u = 17/512,查询= 67/2048 = 1 / 30.5672 ......(回合到1/31)

  22.   
  23. l = 33/1024,u = 67/2048。此时l和u都具有相同的连续分数项31,所以现在我们使用连续的分数猜测。   query = 1/31。

  24.   

SUCCESS!

另一个例子让我们使用16/113(= 355/113 - 3,其中355/113非常接近pi)。

[继续,我必须去某个地方]


进一步反思,继续分数是要走的路,除了确定下一个任期外,不要介意二分。当我回来时更多。

答案 3 :(得分:3)

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

为了避免在下一段中出现混淆,“查询”指的是猜测者给挑战者一个猜测,并且挑战者响应“更大”或“更小”。这允许我为其他东西保留单词“guess”,猜测p + q不会直接向挑战者询问。

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

我们想用r + s&lt; = k检查所有分数r / s,并且双k直到k足够大。请注意,您需要检查给定k值的O(k ^ 2)个分数。构建包含所有这些值的平衡二叉搜索树,然后搜索它以确定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))。

答案 4 :(得分:3)

答案 5 :(得分:2)

请记住,(0,1)中的任何有理数都可以表示为不同(正或负)单位分数的有限和。例如,2/3 = 1/2 + 1/6和2/5 = 1/2 - 1/10。您可以使用它来执行直接二进制搜索。

答案 6 :(得分: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,我们将通过该大小的所有可能分数得到接近二进制搜索的内容。偶尔我们不会找到一个特定的分数,直到我们增加的界限比我们应该的更远,但我们不会太遥远。

所以你有它。使用polylog工作时发现的对数个问题。

更新:以及完整的工作代码。

#! /usr/bin/python

from fractions import Fraction
import readline
import sys

operations = [0]

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

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

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

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

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

在猜测中看起来比先前的解决方案稍微有效,并且操作的次数要少得多。对于101/1024,它需要19次猜测和251次操作。对于.98765,它需要27次猜测和623次操作。对于0.0123456789,它需要66次猜测和889次操作。对于咯咯和笑容,对于0.0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(这是前一个的10个副本),它需要665个猜测和23289个操作。

答案 7 :(得分:0)

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

  1. 使用倍增步法
  2. 查找间隔[0, N]
  3. 在最接近间隔中心的区间内给出最小分母的理性间隔[a, b]
  4. 然而,这可能仍然O(log(num/den) + den)(不确定,早上太早,让我想清楚;-))