我曾经接受过以下面试问题:
我正在考虑一个正整数n。想出一个可以在O(lg n)查询中猜出它的算法。每个查询都是您选择的数字,我会回答“较低”,“较高”或“正确”。
这个问题可以通过修改后的二进制搜索来解决,在该搜索中,列出2的幂,直到找到超过n的值,然后在该范围内运行标准二进制搜索。我认为这很酷的是,你可以比无聊的力量更快地搜索一个特定数字的无限空间。
但我的问题是对这个问题稍作修改。假设我在0和1之间选择任意有理数,而不是选择正整数。我的问题是:您可以使用什么算法来最有效地确定我选择了哪个有理数?
现在,我所拥有的最佳解决方案是在最多O(q)时间内通过隐式地遍历所有有理数的Stern-Brocot tree二元搜索树来找到p / q。但是,我希望运行时更接近我们为整数情况得到的运行时,可能是O(lg(p + q))或O(lg pq)。有没有人知道如何获得这种运行时?
我最初考虑使用区间[0,1]的标准二进制搜索,但这只会找到具有非重复二进制表示的有理数,这几乎错过了所有的有理数。我还考虑过使用其他一些方法来列举有理数,但我似乎找不到一种方法来搜索这个空间给出更大/更小/更少的比较。
答案 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的幂,连续分数将取实数并找到最接近的有理数。
如何并行运行它们如下。
在每一步中,l
和u
是二分的下限和上限。这个想法是,您可以选择将二分范围减半,并添加一个附加项作为连续分数表示。当l
和u
的下一个术语与连续分数相同时,您将继续分数搜索中的下一步,并使用连续分数进行查询。否则,使用二分法将范围减半。
由于两种方法都将分母增加至少一个常数因子(二分为2,连续分数至少为因子phi =(1 + sqrt(5))/ 2),这意味着你的搜索应该是O(log(q))。 (可能会有重复的连续分数计算,因此最终可能为O(log(q)^ 2)。)
我们的连续分数搜索需要舍入到最接近的整数,而不是使用楼层(这在下面更清楚)。
以上是一种手写的。让我们使用r = 1/31的具体例子:
l = 0,u = 1,查询= 1/2。 0不能表示为连续分数,所以我们使用二分搜索直到l!= 0。
l = 0,u = 1/2,查询= 1/4。
l = 0,u = 1/4,查询= 1/8。
l = 0,u = 1/8,查询= 1/16。
l = 0,u = 1/16,查询= 1/32。
l = 1/32,u = 1/16。现在1 / l = 32,1 / u = 16,这些有不同的cfrac代表,所以保持二等分。,query = 3/64。
l = 1/32,u = 3/64,查询= 5/128 = 1 / 25.6
l = 1/32,u = 5/128,查询= 9/256 = 1 / 28.4444 ....
l = 1/32,u = 9/256,查询= 17/512 = 1 / 30.1176 ......(回合到1/30)
l = 1/32,u = 17/512,查询= 33/1024 = 1 / 31.0303 ......(回合到1/31)
l = 33/1024,u = 17/512,查询= 67/2048 = 1 / 30.5672 ......(回合到1/31)
- 醇>
l = 33/1024,u = 67/2048。此时l和u都具有相同的连续分数项31,所以现在我们使用连续的分数猜测。 query = 1/31。
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)
您可以通过例如对(分母,分子)对给定间隔中的有理数进行排序。然后你可以玩游戏
[0, N]
[a, b]
O(log(num/den) + den)
(不确定,早上太早,让我想清楚;-))