ふと、有理数 n/d を循環小数に変換するメソッドを書いてみた
これを行うには、x_{i+1} = (10 * x_i) % d という漸化式で 定義される数列が、どこで cycle するかを検出しなければならない
x_i は d 未満の非負整数なので、状態数が有限であるため、かならずどこかで cycle が起きる
なお、数列の最初のあたりは cycle にならないことがある
x_0 から x_{h-1} は cycle に含まれず、 x_h から x_{h+l-1} が最初の cycle とすると、 h と l を求める、というのが cycle を検出する、という話である
これを行うにはいくつかのアルゴリズムがあるが、 wikipedia の Cycle detection にのっている、 兎と亀のアルゴリズム、 Brent のアルゴリズム、 Gosper のアルゴリズムというのを使って実装してみた
なお、Hare というのが野うさぎらしい (亀は Tortoise)
兎と亀のアルゴリズムおよび Brent のアルゴリズムは実行中に数列の要素を 2つしか保持しないのに対し、 Gosper のアルゴリズムは log くらいの要素を保持する。 たくさん保持するぶん、cycle を速やかに検出することが期待されるので速いのではないかと思ったのだが... 後述
# recurring-decimal.rb class Rational # uses Floyd's Tortoise and Hare def to_dec_th sign = self < 0 ? "-" : "" n = self.numerator.abs d = self.denominator int, mod = n.divmod(d) s = "#{sign}#{int}" return s if mod == 0 # There are fractional digits. # # If the fractional digits is recurring, # it begins with non-recurring part of length h and # recurring part of length l. # foobarbazbazbaz... = foobar(baz) : h = 6, l = 3 # # We have a sequence # x0 = mod # x1 = (x0 * 10) % d # ... # x{i+1} = (xi * 10) % d # ... # # Since this sequence repeats from xh, # xj = x{j+k*l} for all j >= h and k >= 0. # At first, we find some i such that xi = x{2i}. # However, we don't use i itself. # i = 0 m1 = m2 = mod begin m1 = (m1 * 10) % d # i += 1 m2 = (m2 * 10) % d m2 = (m2 * 10) % d end while m1 != m2 # We found i such that xi = x{2i} # The difference of i and 2i is a multiple of the cycle length, l. # 2i - i = i = k * l for some k. # Then, we detect h. # We search h from 0 to detect xh = x{h+k*l} = x{h+i}. f = "" h = 0 m1 = mod while m1 != m2 d1, m1 = (m1 * 10).divmod(d) f << d1.to_s m2 = (m2 * 10) % d h += 1 end # If m2 is zero, the number is not recurring. return "#{s}.#{f}" if m2 == 0 # At last, we find the recurring part. l = 0 f << "(" m2 = m1 begin d1, m1 = (m1 * 10).divmod(d) f << d1.to_s l += 1 end while m1 != m2 f << ")" "#{s}.#{f}" end # uses Gosper's loop detector def to_dec_gosper sign = self < 0 ? "-" : "" n = self.numerator.abs d = self.denominator int, mod = n.divmod(d) s = "#{sign}#{int}" return s if mod == 0 f = "" # Gosper's loop detector # https://web.archive.org/web/20160414011322/http://hackersdelight.org/hdcodetxt/loopdet.c.txt x0 = mod ms = [x0] x = x0 n = 1 while true digit, x = (x * 10).divmod(d) f << digit.to_s return "#{s}.#{f}" if x == 0 break if k = ms.index(x) n +=1 j = (n & ~(n - 1)).bit_length - 1 ms[j] = x end m = ((((n >> k) - 1) | 1) << k) - 1; l = n - m; lgl = (l - 1).bit_length - 1 h_lo = m - [1, 1 << lgl].max + 1; # Now, we have the length of the recurring part, l, and # a lower bound of the length of the non-recurring part, h_lo. # find the exact length of the non-recurring part, h. # At first, find an element before the recurring part. # Any element works well but we find maximum one already computed. m0 = nil k0 = nil ms.each_index {|k2| m2 = ((((n >> k2) - 1) | 1) << k2) - 1; next if h_lo < m2 if m0 == nil || m0 < m2 then k0 = k2 m0 = m2 end } if m0 then h = m0 x1 = ms[k0] else h = 0 x1 = x0 end # advance l times x2 = x1 l.times { x2 = (x2 * 10) % d } # find h. while x1 != x2 h += 1 x1 = (x1 * 10) % d x2 = (x2 * 10) % d end "#{s}.#{f[0, h]}(#{f[h,l]})" end end # References: # * Cycle detection # https://en.wikipedia.org/wiki/Cycle_detection # * HAKMEM ITEM 132 (Gosper): LOOP DETECTOR # http://www.inwap.com/pdp10/hbaker/hakmem/flows.html # * Gosper's loop detector implementation # https://web.archive.org/web/20160414011322/http://hackersdelight.org/hdcodetxt/loopdet.c.txt
これを使うと、以下のように、有理数を十進の循環小数に 変換して、循環部分を括弧で括った文字列を得られる
(1/3r).to_dec_th #=> "0.(3)" (1/7r).to_dec_th #=> "0.(142857)" (22/7r).to_dec_th #=> "3.(142857)" (27/88r).to_dec_th #=> "0.306(81)"
循環しない小数で表現可能なら、括弧の部分はつかないようにしてある (0 が循環するとして "(0)" をつけてもいいのだが)
(1/5r).to_dec_th #=> "0.2"
とりあえず、以下のようにして 3個のメソッドが同じ結果になることを確認した (なんとなく、各分母に対して分子は乱数にしてみた)
% ruby -r./recurring-decimal.rb -e ' 1.upto(20000) {|i| r = rand(i).to_r/i s1 = r.to_dec_th s2 = r.to_dec_brent s3 = r.to_dec_gosper raise if s1 != s2 || s1 != s3 } '
せっかく同じ動作をするメソッドを 3個も実装したので、 速度を測ってみよう
まずは平均から
% ruby -r./recurring-decimal.rb -e ' acc_a, acc_b, acc_c = 0.0, 0.0, 0.0 n = 2000 1.upto(n) {|i| r = rand(i).to_r/i t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) s1 = r.to_dec_th t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) s2 = r.to_dec_brent t3 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) s3 = r.to_dec_gosper t4 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) acc_a += t2 - t1 acc_b += t3 - t2 acc_c += t4 - t3 } printf("to_dec_th: %f\n", acc_a / n) printf("to_dec_brent: %f\n", acc_b / n) printf("to_dec_gosper: %f\n", acc_c / n) ' to_dec_th: 0.000224 to_dec_brent: 0.000281 to_dec_gosper: 0.000567
うぅむ、いちばん素朴な兎と亀のアルゴリズムを使ったもの (to_dec_th) がいちばん速くて、 いちばん凝った Gosper のアルゴリズムを使ったもの (to_dec_gosper) がいちばん遅い
生データをとってプロットしてみよう
% ruby -r./recurring-decimal.rb -e ' acc_a, acc_b, acc_c = 0.0, 0.0, 0.0 n = 2000 puts "numerator,t,alg\n" 1.upto(n) {|i| r = rand(i).to_r/i t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) s1 = r.to_dec_th t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) s2 = r.to_dec_brent t3 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) s3 = r.to_dec_gosper t4 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) a = t2 - t1 b = t3 - t2 c = t4 - t3 printf "%d,%g,th\n", i, a printf "%d,%g,brent\n", i, b printf "%d,%g,gosper\n", i, c } ' > recurring-decimal.csv
recurring-decimal.R:
library(ggplot2) d <- read.csv("2020-07/recurring-decimal.csv") p <- ggplot(d, aes(numerator, t)) + geom_point(aes(colour = alg)) + ylab("t[s]") print(p)
三角形になっているのは、 分母が大きくなるほど小数点以下の桁数が長くなる可能性がある (分子によっては短くなることもある) のでそれはそうなるか、という感じ
時間がかかっているのは、to_dec_gosper が多くて、 次いで to_dec_brent が多い、 というのは平均と整合する
ということで、循環小数については難しいことはせず、素朴に兎と亀のアルゴリズムを使うのがよさそう
Gosper の loop detector は、 数列を最初から順に調べていき、 いくつかの (調べた範囲の log くらいの) 要素を残しておいて 新しい要素を得るたびにそれと同じものが残しておいた要素の中にあるかどうかを調べる
ここで残しておくのは、新しいほうは密に残しておいて、古いほうはまばらに残す
そのために、i 番目の要素は、i を 2進数で表現したときに下位に 0 がいくつ並んでいるかで 上書きする要素を決めていく
これを読んで思い出したのだが、これは昔自分でバックアップのために考えた仕掛けと同じだな
なお、下位に並んでいる 0 の数を返す関数は RULER 関数と呼ぶひとがいたらしい
素朴なほうがいいというなら 兎と亀のアルゴリズムよりも単純に、 途中結果をぜんぶ記録しておくのはどうか
class Rational def to_dec_naive sign = self < 0 ? "-" : "" n = self.numerator.abs d = self.denominator int, mod = n.divmod(d) s = "#{sign}#{int}" return s if mod == 0 f = "" visited = {} begin visited[mod] = visited.size digit, mod = (mod * 10).divmod(d) f << digit.to_s return "#{s}.#{f}" if mod == 0 end until visited[mod] i = visited[mod] "#{s}.#{f[0,i]}(#{f[i..-1]})" end end
これは、途中結果をぜんぶ記録するので、循環がなかなか見つからないと、 メモリをたくさん消費してしまう
とりあえずランダムな入力に対して to_dec_th と結果を比較して結果が一致することは確認した
で、to_dec_th と実行時間を比較
% ruby -r./recurring-decimal.rb -e ' n = 1000 puts "numerator,t,alg\n" 1.upto(n) {|i| n = (1.01 ** i).to_i d = rand(n) + 1 r = rand(d).to_r/d t0 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) s0 = r.to_dec_naive t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) s1 = r.to_dec_th t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) raise if s0 != s1 a = t1 - t0 b = t2 - t1 printf "%d,%g,naive\n", n, a printf "%d,%g,th\n", n, b } ' > recurring-decimal2.csv
今回は、分母も乱数にして、あと、その乱数の範囲を指数関数的に広げて、 大きな値も測定するようにしてみた
recurring-decimal2.R:
library(ggplot2) d <- read.csv("2020-07/recurring-decimal2.csv") p <- ggplot(d, aes(numerator, t)) + geom_point(aes(colour = alg)) + scale_x_log10() + scale_y_log10("t[s]") print(p)
まぁ、to_dec_naive と to_dec_th は速度は同じくらいかな
任意の数列に使える循環検出じゃなくて、 循環小数固有の性質を使えばもっと高速にできるのではないかと思って wikipedia の説明を読んでみた
すると、有理数 n/d に対して、d の約数の 2 と 5 を括り出して d = 2^a * 5^b * d' とすると、 繰り返されない部分の長さは max(a,b) なのだそうだ
そして繰り返し部分の長さは 10 modulo d' の multiplicative order というものになるらしい
これを使って、循環小数に変換するメソッドを書いてみた
class Rational def to_dec_factor sign = self < 0 ? "-" : "" n = self.numerator.abs d = self.denominator int, mod = n.divmod(d) s = "#{sign}#{int}" return s if mod == 0 d2 = d # find the number of zero bits at lower bits in d. num_factor_2 = (d & ~(d - 1)).bit_length - 1 d2 = d >> num_factor_2 num_factor_5 = 0 while d2 % 5 == 0 num_factor_5 += 1 d2 /= 5 end h = [num_factor_2, num_factor_5].max if h == 0 f = "" else digits, mod = (mod * 10**h).divmod(d) f = digits.to_s if f.length < h f = "0" * (h - f.length) + f end end return "#{s}.#{f}" if mod == 0 # find the multiplicative order of 10 modulo d2 l = 0 m = 1 begin m = (m * 10) % d2 l += 1 end until m == 1 f << "(" digits = (mod * 10**l) / d digits = digits.to_s if digits.length < l f << "0" * (l - digits.length) << digits else f << digits end f << ")" "#{s}.#{f}" end end
例によって to_dec_th と比較して結果が一致することをテストした
データをとってプロットしてみる
% ruby -r./recurring-decimal.rb -e ' n = 1000 puts "numerator,t,alg\n" 1.upto(n) {|i| n = (1.01 ** i).to_i d = rand(n) + 1 r = rand(d).to_r/d STDERR.puts [i, r].inspect t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) s1 = r.to_dec_th t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) s2 = r.to_dec_factor t3 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) raise if s1 != s2 a = t2 - t1 b = t3 - t2 printf "%d,%g,th\n", n, a printf "%d,%g,factor\n", n, b STDOUT.flush } ' > recurring-decimal3.csv
recurring-decimal3.R:
library(ggplot2) d <- read.csv("2020-07/recurring-decimal3.csv") p <- ggplot(d, aes(numerator, t)) + geom_point(aes(colour = alg)) + scale_x_log10() + scale_y_log10("t[s]") print(p)
お、なんか速くなった感じ
R の中で平均とかをとってみた
> summary(d$t[d$alg == "th"]) Min. 1st Qu. Median Mean 3rd Qu. Max. 2.966e-06 8.672e-06 1.800e-05 2.632e-04 7.591e-05 1.666e-02 > summary(d$t[d$alg == "factor"]) Min. 1st Qu. Median Mean 3rd Qu. Max. 2.332e-06 7.106e-06 1.073e-05 5.056e-05 2.424e-05 2.721e-03
平均が 2.632e-04 から 5.056e-05 に下がっているので、やはり速くなっている
なお、今の実装では multiplicative order は以下のように素朴に計算している
# find the multiplicative order of 10 modulo d2 l = 0 m = 1 begin m = (m * 10) % d2 l += 1 end until m == 1
ただ、調べてみると、Baby-step giant-step という方法があって、もっと速くできるらしい
multiplicative order of 10 modulo d を求めるのに Baby-step giant-step で探索するよりも速いらしい方法として、 カーマイケルの定理を使うという方法があるようだ
ただ、d を素因数分解しなければならないようでギョッとする
でも、multiplicative order of 10 modulo d を素朴に計算すると、 最悪の場合、ほぼ d 回の除算が必要で、 Baby-step giant-step でも sqrt(d) 回くらいのようだ
sqrt(d) 回除算していいなら素因数分解できると思うので、 もともと素朴にやったり Baby-step giant-step でやるのが 素因数分解以上に時間がかかるということだろうな
[latest]