問題概略
は 12 からはじまる最初の 2 の累乗数であり,2 つ目は である。
からはじまる 2 の累乗数のうち小さい方から 番目の数を とする。を求めよ。
解説の pdf も作りました。きれいなレイアウトで読みたい方はこちらをどうぞ。
をとって評価
の首位 3 桁が 123 のとき のようにおけます。 は整数です。
\begin{align*}
&k\log_2 10+\log_2 123 < n< k\log_2 10+\log_2 124\mbox{ ……(1)}\\
&\therefore \lceil k\log_2 10+\log_2 123\rceil \leqq n
\leqq \lfloor k\log_2 10+\log_2 124\rfloor\mbox{ ……(2)}
\end{align*}
(1)の左辺を ,右辺を とおいて,(2)の左辺を ,右辺を とおきます。
と は無理数で,差は非常に小さい値です。
と の整数部分が等しいとき, の変換では小数部分の切り上げがおきて, では切り捨てがおきます。
なので(3)をみたす はありません。
と の整数部分が異なるときは になって です。
になる 678910 番目の に対する が答えです。約 36 秒かかります。
import time from math import ceil, floor, log2 def left(k): return ceil(log2(123) + k * log2(10)) def right(k): return floor(log2(124) + k * log2(10)) def solve(n): k, cnt = 0, 0 while cnt < n: if left(k) == right(k): cnt += 1 k += 1 return ceil(left(k - 1)) if __name__ == "__main__": start = time.time() n = 678910 print(solve(n)) elapsed_time = time.time() - start print("elapsed_time:{0} [sec]".format(elapsed_time))
だけで評価
なので と の整数部分が異なるのは の小数部分と の和が 1 以上のときです。
整理すると次のようになります。等号は成立しないので外しました。
この解法は約 18 秒でした。
import time from math import ceil, log2 A, B, C = log2(10), log2(123), log2(124) def left(k): return B + k * A def isok(k): return k * A - int(B + k * A) > 1 - C def solve(n): k, cnt = 0, 0 while cnt < n: if isok(k): cnt += 1 k += 1 return ceil(left(k - 1)) if __name__ == "__main__": start = time.time() n = 678910 print(solve(n)) elapsed_time = time.time() - start print("elapsed_time:{0} [sec]".format(elapsed_time))
両側からの評価をやめたことに加えて,対数を定数扱いしたことが効いた感じです。
ためしに pe686a.py を対数をすべて定数扱いするように書き直したところ,計算時間が 36 秒から 23 秒に減りました。
の候補をしぼりこむ
階差の候補は3つだけ
20000 以下の で をみたすものをすべて求めて,その階差をとってみました(mathematica)。
条件をみたす は 234 個あって,階差は 59, 87, 146 の 3 つの値しかありません。
In[]:= AbsoluteTiming[ left[k_] := Ceiling[Log2[123] + k*Log2[10]]; right[k_] := Floor[Log2[124] + k*Log2[10]]; lst = Select[Range[0, 20000], left@# == right@# &]; {Length@lst, Sort@DeleteDuplicates@Differences@lst}] Out[]= {1.12065, {234, {59, 87, 146}}}
なぜ の階差が限られた値しかとらないのか考えましょう。
ある で になったとします。
と の整数部分は異なるので次のようにおけます。 は整数で と は 0 と 1 の間の小数です。
\begin{align*}
&l(k)=m-\alpha,\, r(k)=m+\beta\\
&\qquad (\alpha+\beta=r(k)-l(k)=\log_2 124-\log_2 123\approx 0.0116818)
\end{align*}
整数 をはさんでそのちょっと下に があって,ちょっと上に があるイメージです。
この状態で を 1 増やすと , は 増えます。
この数の小数部分は約 です。
, なので と の整数部分は になって一致してしまいます。
このように「整数のちょっと下」「ちょっと上」を同時にみたすのは難しく, の階差が制約を受けるわけです。
一方, を 59, 87, 146 回足すときはうまくいきます。以下, の小数部分を であらわします。
\begin{align*}
&\langle 59\cdot \log_2 10\rangle \approx 0.993758=1-0.0062424\\
&\langle 87\cdot \log_2 10\rangle \approx 0.00774426\\
&\langle 146\cdot \log_2 10\rangle \approx 0.00150185
\end{align*}
整数から高々 しか離れていない , にこれらの値を足しても「整数のちょっと下」「ちょっと上」という状態は保たれます。
コードに落とす
「 の階差は 59, 87, 146 だけ」を示すのは難しそうですが,「58 回以下足しても無駄」は示せます。
, で を 増やしたときの小数部分の変化量の絶対値は次の式で与えられます。
に対する最小値は のときの です。mathematica で計算しました。
In[]:= AbsoluteTiming[ L123 = Log2@123; L124 = Log2@124; L10 = Log2@10; f[n_] := N[Min[FractionalPart[L10*n], 1 - FractionalPart[L10*n]]]; m = Min[f /@ Range@58]; {m, m < L124 - L123}] Out[]= {0.0011467, {0.0139867, False}}
が整数から目一杯離れたとしてその距離は で, のときの移動量はこれを超えてしまいます。
条件をみたす の階差として 1~58 はすべて不適です。
まとめると,「ある が条件をみたすとき,次の は 以降」です。
この性質を利用することで計算時間を 秒に縮めることができました。
import time from math import ceil, log2 L10, L123, L124 = log2(10), log2(123), log2(124) def left(k): return L123 + k * L10 def isok(k): return k * L10 - int(L123 + k * L10) > 1 - L124 def solve(n): k, cnt = 0, 0 while True: if not (isok(k)): k += 1 cnt += 1 if cnt == n: return ceil(left(k)) else: k += 59 if __name__ == "__main__": start = time.time() n = 678910 print(solve(n)) elapsed_time = time.time() - start print("elapsed_time:{0} [sec]".format(elapsed_time))
未証明の解法=嘘解法
「 の階差は 59, 87, 146 だけ」は証明できていませんが,これでコードを書いてみました。
ある が OK だったら,次の として , , を順次試すコードです。
これだと 秒で終わります。最初にあげたコードの約 80 倍のスピードです。
import time from math import ceil, log2 L10, L123, L124 = log2(10), log2(123), log2(124) def left(k): return L123 + k * L10 def isok(k): return k * L10 - int(L123 + k * L10) > 1 - L124 def solve(n): k = 0 while not (isok(k)): k += 1 cnt = 1 lst = [59, 87, 146] while cnt < n: flag = False for x in lst: if isok(k + x): k += x cnt += 1 flag = True break if flag is False: return -1 return ceil(left(k)) if __name__ == "__main__": start = time.time() print(solve(678910)) elapsed_time = time.time() - start print("elapsed_time:{0} [sec]".format(elapsed_time))
に注目して , だけ試すコードも書いてみたのですが,こちらはうまくいきません。
前に見たように は小数部分を減らす操作で, は増やす操作です。これらだけではうまくいかなくて,適当なタイミングで して,ほんのちょっとだけ小数部分を増やすことが必要なようでした。