私はかつて面接の質問として以下を受け取りました:
私は正の整数nを考えています。 O(lg n)クエリでそれを推測できるアルゴリズムを考え出します。それぞれの質問はあなたが選んだ数であり、私は「低い」、「高い」、または「正しい」のいずれかに答えます。
この問題は、修正された二分探索によって解決できます。この探索では、nを超えるものが見つかるまで2の累乗をリストし、その範囲で標準の二分探索を実行します。これについて私がとてもクールだと思うのは、ブルートフォースよりも速く特定の数の無限空間を検索できることです。
しかし、私が持っている質問は、この問題のわずかな修正です。正の整数を選択する代わりに、0と1の間の任意の有理数を選択するとします。私の質問は、私が選んだ有理数を最も効率的に決定するためにどのアルゴリズムを使用できるかということです。
今のところ、私が持っている最善の解決策は、二分探索木である Stern-Brocot tree を暗黙的に歩くことにより、最大でO(q)時間でp/qを見つけることができます。しかし、私は、整数の場合に得られたランタイムに近いランタイム、おそらくO(lg(p + q))やO(lg pq)のようなものを取得したいと思っていました。この種のランタイムを取得する方法は?
私は当初、区間[0、1]の標準的な二分探索を使用することを検討しましたが、これは、ほとんどすべての有理数を見逃している、繰り返されない二分表現を持つ有理数のみを見つけます。有理数を列挙する他の方法を使用することも考えましたが、比較が大きい/等しい/小さいだけでは、このスペースを検索する方法が見つからないようです。
さて、これが 連分数 だけを使った私の答えです。
まず、ここでいくつかの用語を取得しましょう。
X = p/qを未知の分数とします。
Q(X、p/q)= sign(X-p/q)をクエリ関数とします。0の場合は数値を推測し、+ /-1の場合はエラーの兆候を示します。 。
連分数の従来の表記法 はA = [a; a1、2、3、... ak]
= a + 1 /(a1 + 1 /(a2 + 1 /(a3 + 1 /(... + 1/ak)...)))
0 <p/q <1の場合、次のアルゴリズムに従います。
Y = 0 = [0]、Z = 1 = [1]、k = 0を初期化します。
外部ループ:前提条件は次のとおりです。
YとZは、k + 1項の連分数であり、最後の要素が1だけ異なることを除いて同一であるため、Y = [y; y1、y2、y3、... yk]およびZ = [y; y1、y2、y3、... yk + 1]
(-1)k(Y-X)<0 <(-1)k(Z-X)、または簡単に言えば、k偶数の場合はY <X <Z、k奇数の場合はZ <X <Y。
数値の値を変更せずに、連分数の次数を1ステップ拡張します。一般に、最後の項がyの場合k およびyk + 1、これを[... yに変更しますk、yk + 1=∞]および[... yk、zk + 1= 1]。ここで、kを1増やします。
内部ループ:これは本質的に整数に関する@templatetypedefのインタビューの質問と同じです。近づくために2段階の二分探索を行います。
内部ループ1:yk =∞、zk = aであり、XはYとZの間にあります。
ダブルZの最後の項:M = Zを計算しますが、mを使用しますk = 2 * a = 2 * zk。
不明な数を照会します:q = Q(X、M)。
Q = 0の場合、答えがあり、ステップ17に進みます。
QとQ(X、Y)の符号が反対の場合、XがYとMの間にあることを意味するため、Z = Mに設定して、手順5に進みます。
それ以外の場合は、Y = Mに設定し、次の手順に進みます。
内部ループ2。yk = b、zk = aであり、XはYとZの間にあります。
Aとbが1異なる場合は、YとZを交換して、手順2に進みます。
二分探索を実行します。Mを計算します。ここでmk = floor((a + b)/ 2、およびクエリq = Q(X、M)。
Q = 0の場合、これで完了です。手順17に進みます。
QとQ(X、Y)の符号が反対の場合、XがYとMの間にあることを意味するため、Z = Mに設定して、手順11に進みます。
それ以外の場合、qとQ(X、Z)の符号は逆になります。つまり、XはZとMの間にあるため、Y = Mに設定して、手順11に進みます。
完了:X = M。
X = 16/113 = 0.14159292の具体例
_Y = 0 = [0], Z = 1 = [1], k = 0
k = 1:
Y = 0 = [0; ∞] < X, Z = 1 = [0; 1] > X, M = [0; 2] = 1/2 > X.
Y = 0 = [0; ∞], Z = 1/2 = [0; 2], M = [0; 4] = 1/4 > X.
Y = 0 = [0; ∞], 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, ∞] > X, Z = 1/8 = [0; 7, 1] < X,
M = [0; 7, 2] = 2/15 < X
Y = 1/7 = [0; 7, ∞], Z = 2/15 = [0; 7, 2],
M = [0; 7, 4] = 4/29 < X
Y = 1/7 = [0; 7, ∞], Z = 4/29 = [0; 7, 4],
M = [0; 7, 8] = 8/57 < X
Y = 1/7 = [0; 7, ∞], Z = 8/57 = [0; 7, 8],
M = [0; 7, 16] = 16/113 = X
--> done!
_
Mを計算する各ステップで、間隔の範囲が狭くなります。間隔が各ステップで少なくとも1/sqrt(5)の係数で減少することを証明するのはおそらくかなり簡単です(これは行いませんが)。これは、このアルゴリズムがO(log q)ステップであることを示しています。
これは、templatetypedefの元のインタビューの質問と組み合わせて、最初にQ(X、0)を計算し、次にQ(X、0)を計算することにより、0と1の間だけでなく、 any 有理数p/qに適用できることに注意してください。正/負の整数の場合、2つの連続する整数間の境界を設定し、小数部分に上記のアルゴリズムを使用します。
次にチャンスがあれば、このアルゴリズムを実装するpythonプログラムを投稿します。
edit:また、各ステップで連分数を計算する必要がないことに注意してください(これはO(k)であり、部分的な近似がありますO(1)の前のステップから次のステップを計算できる連分数。)
編集2:部分近似の再帰的定義:
もしk = [a; a1、2、3、... ak] = pk/ qk、次にpk = akpk-1 + pk-2、およびqk = akqk-1 + qk-2。 (出典:Niven&Zuckerman、第4版、定理7.3-7.5。参照 Wikipedia )
例:[0] = 0/1 = p/ q、[0; 7] = 1/7 = p1/ q1;だから[0; 7、16] =(16 * 1 + 0)/(16 * 7 + 1)= 16/113 = p2/ q2。
これは、2つの連分数YとZが最後の項を除いて同じ項を持ち、最後の項を除いた連分数がpである場合を意味します。k-1/ qk-1、次にY =(ykpk-1 + pk-2)/(ykqk-1 + qk-2)およびZ =(zkpk-1 + pk-2)/(zkqk-1 + qk-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数学を処理し、このプログラムは整数数学のみを使用するため(区間計算を除く)、任意の有理数で機能するはずです。
編集3:これがO(log ^ 2 q)ではなくO(log q)であることの証明の概要:
最初に、有理数が見つかるまで、ステップ数nに注意してください。k 新しい連分数の項はそれぞれ exactly 2b(a_k)-1です。ここでb(a_k)は、a_k = ceil(log2(a_k))を表すために必要なビット数です。 (a_k)バイナリ検索の「ネット」を広げるためのステップ、およびb(a_k)-1ステップで狭めるためのステップ)。上記の例を参照してください。ステップ数は常に1、3、7、15などであることに注意してください。
これで、漸化式qを使用できます。k = akqk-1 + qk-2 望ましい結果を証明するための帰納。
このように述べましょう:Nの後のqの値k = sum(nk)k番目の項に到達するために必要なステップは最小です:q> = A * 2cN 一部の固定定数A、cの場合。 (逆にすると、ステップ数Nは<=(1/c)* logになります。2 (q/A)= O(log q))
基本ケース:
これは、A = 1、c = 1/2が望ましい境界を提供できることを意味します。実際には、qは各項を not 2倍にすることができます(反例:[0; 1、1、1、1、1]の成長因子はphi =(1 + sqrt(5)) )/ 2)では、c = 1/4を使用しましょう。
誘導:
項k、qk = akqk-1 + qk-2。繰り返しますが、nk = 2b-この用語に必要な1ステップ、ak > = 2b-1 = 2(nk-1)/ 2。
だからkqk-1 > = 2(Nk-1)/ 2 * qk-1 > = 2(nk-1)/ 2 * A * 2Nk-1/ 4 = A * 2Nk/ 4/ sqrt(2)* 2nk/ 4。
ああ-ここで難しいのは、k = 1、qはその1つの項であまり増加しない可能性があるため、qを使用する必要がありますk-2 しかし、それはqよりはるかに小さいかもしれませんk-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)
の可能性について説明します。これは、探していた効率の大きさのオーダーです。
更新:人々はこの背後にある理由を理解していませんでした。推論は簡単です。二分木を効率的に歩く方法を知っています。最大分母がn
のO(n2)
分数があります。したがって、O(2*log(n)) = O(log(n))
ステップで特定の分母サイズまで検索できます。問題は、検索する可能性のある有理数が無限にあることです。したがって、それらをすべて並べて注文し、検索を開始することはできません。
したがって、私の考えは、いくつかを並べて、検索し、さらに並べて、検索するなどです。整列するたびに、前回の約2倍の整列になります。したがって、前回よりも1つ多く推測する必要があります。したがって、最初のパスでは1つの推測を使用して、1つの可能な有理数をトラバースします。 2つ目は、2つの推測を使用して、3つの可能な有理数をトラバースします。 3つ目は、3つの推測を使用して、7つの可能な有理数をトラバースします。そして、私たちのk
'thは、k
の推測を使用して、_2k-1
_の可能な有理数をトラバースします。特定の有理数_m/n
_の場合、最終的には、その有理数をかなり大きなリストに入れて、バイナリ検索を効率的に行う方法を知っていることになります。
二分探索を行い、さらに有理数を取得するときに学んだすべてを無視した場合、O(log(n))
パスに_m/n
_までのすべての有理数を入れます。 (それは、その時点までに、_m/n
_までのすべての有理数を含めるのに十分な有理数を持つパスに到達するためです。)しかし、各パスはより多くの推測を必要とするため、O(log(n)2)
推測になります。 。
しかし実際にはそれよりもはるかに優れています。最初の推測では、リストにある有理数の半分が大きすぎるか小さすぎるかを排除します。次の2つの推測では、スペースを4分の1に完全に分割することはできませんが、スペースからそれほど遠くはありません。次の3つの推測でも、スペースを8分の1に完全に削減することはできませんが、スペースからそれほど遠くはありません。等々。まとめると、O(log(n))
ステップで_m/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回の推測と、それらを生成するのに約1秒かかりました。 (有理数を引数としてプログラムを呼び出すと、すべての推測が入力されます。これは非常に便利です。)
私はそれを持っている!あなたがする必要があるのは、二分法と 連分数 で並列検索を使用することです。
二分法は、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を持っているので、ここで連分数推定を使用します。クエリ= 1/31。
成功!
別の例として、16/113(= 355/113-3、355/113は円周率にかなり近い)を使用してみましょう。
[続けるには、どこかに行かなければならない]
さらに考えてみると、連分数が進むべき道であり、次の用語を決定することを除いて二分法を気にしないでください。私が戻ったときにもっと。
O(log ^ 2(p + q))アルゴリズムを見つけたと思います。
次の段落での混乱を避けるために、「クエリ」とは、推測者がチャレンジャーに推測を与えるときを指し、チャレンジャーは「大きい」または「小さい」と応答します。これにより、「推測」という言葉を他の何か、つまりチャレンジャーに直接尋ねられないp + qの推測のために予約することができます。
アイデアは、質問で説明したアルゴリズムを使用して、最初にp + qを見つけることです。値kを推測し、kが小さすぎる場合は、2倍にして再試行します。次に、上限と下限が決まったら、標準の二分探索を実行します。これにはO(log(p + q)T)クエリが必要です。ここで、Tは、推測をチェックするために必要なクエリ数の上限です。 Tを見つけましょう。
R + s <= kのすべての分数r/sをチェックし、kが十分に大きくなるまでkを2倍にします。 kの特定の値をチェックする必要があるO(k ^ 2)分数があることに注意してください。これらすべての値を含む平衡二分探索木を構築し、それを検索して、p/qがツリー内にあるかどうかを判断します。 p/qがツリーにないことを確認するには、O(log k ^ 2)= O(log k)クエリが必要です。
2(p + q)より大きいkの値を推測することは決してありません。したがって、T = O(log(p + q))を取ることができます。
Kの正しい値を推測すると(つまり、k = p + q)、kの推測を確認する過程で、クエリp/qをチャレンジャーに送信し、ゲームに勝ちます。
その場合、クエリの総数はO(log ^ 2(p + q))になります。
さて、私はO(lg2 q)連分数の使用に関するJasonSの最も優れた洞察に基づくこの問題のアルゴリズム。実行時の分析とともに完全なソリューションが得られるように、ここでアルゴリズムを完全に具体化すると思いました。
アルゴリズムの背後にある直感は、範囲内の任意の有理数p/qは次のように書くことができるということです。
a + 1 /(a1 + 1 /(a2 + 1 /(a3 + 1/...))
の適切な選択のために私。これは連分数と呼ばれます。さらに重要なのは、これらは私 分子と分母でユークリッドアルゴリズムを実行することで導出できます。たとえば、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のもう1つの良い近似です。これで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の連分数の係数は常に拡張ユークリッドアルゴリズムを使用して形成されることがわかります。これは素晴らしいことです。2つのことを教えてくれるからです。
これらの2つの事実を踏まえると、0から1の間だけでなく、任意の整数nを一度に1つずつ推測する一般的なアルゴリズムを適用して、 p/qの連分数。ただし、今のところ、(0、1]の範囲の数値についてのみ心配します。これは、任意の有理数を処理するためのロジックを、これをサブルーチンとして簡単に実行できるためです。
最初のステップとして、の最良の値を見つけたいとしましょう。1 そのため1/a1 p/qに可能な限り近いと1 は整数です。これを行うには、アルゴリズムを実行して任意の整数を推測し、毎回逆数を取ります。これを行った後、2つのことのうちの1つが起こります。まず、偶然にも、ある整数kに対してp/q = 1/kであることがわかるかもしれません。その場合、これで完了です。そうでない場合は、p/qが1 /(a1 -1)および1/a いくつかのために1。これを行うと、次を見つけることによって、1レベル深い連分数の作業を開始します。2 p/qが1 /(a1 + 1/a2)および1 /(a1 + 1 /(a2 + 1))。魔法のようにp/qを見つけたら、それは素晴らしいことです。それ以外の場合は、連分数でさらに1レベル下に移動します。最終的には、この方法で番号を見つけることができ、それほど長くはかからないでしょう。係数を見つけるための各二分探索には最大でO(lg(p + q))時間がかかり、検索には最大でO(lg(p + q))レベルがあるため、必要なのはO(lg)だけです。2(p + q))p/qを回復するための算術演算とプローブ。
私が指摘したい詳細の1つは、検索を実行するときに、奇数レベルか偶数レベルかを追跡する必要があることです。これは、2つの連分数の間にp/qを挟むときに、係数が私たちが探していたのは、上位または下位の割合でした。私はそれを証明せずに述べます私 私が奇妙な場合は、2つの数値の上限を使用し、私 2つの数値のうち低い方を使用しても。
私はこのアルゴリズムが機能することをほぼ100%確信しています。私はこの推論のすべてのギャップを埋める、これのより正式な証明を書き込もうとしています。そうしたら、ここにリンクを投稿します。
このソリューションを機能させるために必要な洞察を提供してくれたすべての人に感謝します。特に、連分数のバイナリ検索を提案してくれたJasonSに感謝します。
(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.0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(前のものの10コピー)の場合、665回の推測と23289回の操作が必要でした。
たとえば、ペア(分母、分子)によって、指定された間隔で有理数を並べ替えることができます。次に、ゲームをプレイすることができます
[0, N]
_を見つけます[a, b]
_が与えられた場合、区間の中心に最も近い区間で最小の分母を持つ有理数を撃ちますただし、これはおそらくまだO(log(num/den) + den)
です(わからないので、ここでは早すぎてはっきりと考えられません;-))