問題の名前 : ぐるぐる数
問題 : http://nabetani.sakura.ne.jp/hena/orde31rotnum/
実装リンク集 : https://qiita.com/Nabetani/items/d139d5ef70aa23c2d038
次回のイベントは 4/6 最終回にするかも。
see https://yhpg.doorkeeper.jp/events/88379
で。
やや遅い実装
まずは、やや遅い実装。
# frozen_string_literal: true
require "json"
def next_vals(b,n)
[n, (n+1)%b]
end
def build_number(b, head, size, states)
(size-1).times.with_object([head]) do |ix,o|
candidates = next_vals(b, o.last).sort
o.push candidates[:low==states[ix] ? 0 : 1]
end
end
def undigits(b,d)
d.reverse_each.inject(0) do |acc,n|
acc*b+n
end
end
def build_states(b, digits)
r=digits.each_cons(2).with_object([]){ |(p,q),o|
candidates = next_vals( b, p )
o << %i(low high above)[candidates.count{ |e| e<q }]
break o unless candidates.include?(q)
}
r + [:low]*(digits.size-1-r.size)
end
def guru(b,digits)
states = build_states(b, digits)
above = states.index(:above)
return build_number(b, digits.first, digits.size, states) unless above
# 繰り上がり処理がある
n = undigits(b,digits[0,above+1].reverse)
d = (n+1).digits(b).reverse
head = guru(b,d)
head + [next_vals( b, head.last).min] * (digits.size-above-1)
end
def sup(b,x)
undigits(b, guru(b, x.digits(b).reverse).reverse)
end
def solve_impl( b, x, y )
x = sup(b,x)
count = 0
while x<=y
count+=1
x = sup(b,x+1)
end
count
end
def solve( src )
sb, *sxy = src.split(",")
b = sb.to_i
xy = sxy.map{ |e| e.to_i(b) }
if b==2
(xy[1] - xy[0]+1).to_s
else
solve_impl( b, *xy ).to_s
end
end
if __FILE__==$PROGRAM_NAME
json = File.open( ARGV[0], &:read)
data = JSON.parse( json, symbolize_names:true )
data[:test_data].map{ |number:, src:, expected:|
actual = solve( src )
ok = expected==actual
p [ (ok ? "ok" : "**NG**"), number, src, expected, actual ]
ok
}.tap{ |r| puts( r.all? ? "everything is okay" : "something wrong" ) }
end
ruby 1.rb data.json
のように実行する。
「次に大きいぐるぐる数」という計算ができればよいというアイディアだけど、MikuriyaHiroshi さんのアイディアの方が平易なコードになっていいかもね。
ただ。
b
が 2 のときはすべての数がぐるぐる数なので、この計算をするとえらく時間がかかる。
そこで b
が 2 のときは単に引き算するというショートカットをしている。
手元のマシンで3秒弱。
速い実装
つづいてちゃんとした実装。
# frozen_string_literal: true
require "json"
def state(b, x)
x.each_cons(2).map{ |prev,cur|
lambda{ |lo,hi|
return :below if cur<lo
return :lo if cur==lo
return :mid if cur<hi
return :hi if cur==hi
return :above
}[*[ prev, (prev+1) % b ].sort]
}
end
def count_state( s )
return 1 if s.empty?
case s[0]
when :below then 0
when :lo then count_state(s.drop(1))
when :mid then 2**(s.size-1)
when :hi then count_state(s.drop(1)) + 2**(s.size-1)
when :above then 2**s.size
end
end
# x 以下の ぐるぐる数の個数
def guru_count( b, x )
# x.size 未満の桁数
s0 = (1...x.size).sum{ |keta| (b-1)*(2**(keta-1)) }
# x.size 桁で、先頭が 1..(x.first-1)
s1 = (x.first-1)*(2**(x.size-1))
# 先頭が x.first
s2 = count_state( state(b, x) )
[s0, s1, s2].sum
end
def solve( src )
sb,sx,sy = src.split(",")
b = sb.to_i
x = (sx.to_i(b) - 1).digits(b).reverse
y = sy.to_i(b).digits(b).reverse
ycount = guru_count(b,y)
xcount = guru_count(b,x)
( ycount - xcount ).to_s
end
if __FILE__==$PROGRAM_NAME
json = File.open( ARGV[0], &:read)
data = JSON.parse( json, symbolize_names:true )
data[:test_data].map{ |number:, src:, expected:|
actual = solve( src )
ok = expected==actual
p [ (ok ? "ok" : "**NG**"), number, src, expected, actual ]
ok
}.tap{ |r| puts( r.all? ? "everything is okay" : "something wrong" ) }
end
まずはこの手の問題の定石。
「0<=x<=y で、x<=i<=y である i について C(i)が真になる i の数は?」みたいな問題は、しばしば「0<=x で、0<=i<x である i について C(i)が真になる i の数」を求める関数を書くと楽になる。
で。
問題はその『「0<=x で、0<=i<x である i について C(i)が真になる i の数」を求める関数』の実装。
例えば。
455734 ( 10進数、6桁 )
よりも小さいぐるぐる数の数を数えたい。そうすると、これは
a) 1桁〜5桁のぐるぐる数 の数
b) 1〜3で始まる6桁のぐるぐる数 の数
c) 4で始まる6桁のぐるぐるで455734より小さいものの数
の三種類の合計になる。a と b は簡単に求まる。
c は
c1) 44 で始まる 6桁のぐるぐる数 の数
c2) 45 で始まる 455734 より小さい 6桁のぐるぐる数 の数
の2つに分けられる。
c1 は簡単に求まる。
c2 は「5で始まる 5桁のぐるぐる数」の数と同じになるので、再帰呼出しで解決できる
というわけ。
これを実行すると ruby自体の起動時間に隠れるぐらいの時間で計算が終わる。
まとめ
会場の方の反応を見ると、やや難しかった模様。
わりときれいな、それでいてオリジナリティのある問題だと思ったんだけど、どうだろう。
次回 4月6日(土曜)は、最終回になるかもしれない(ならないかもしれない)です。
ご興味がお有りなら是非。