CodeIQ 「カット・アンド・スクエア」問題
CodeIQ の Kawazoe さん出題の問題。
問題と他の方のコードが CodeIQ「カット・アンド・スクエア」問題 みんなのコード にまとまっている。
とする。
求める n 桁の整数の上下 n/2 桁をそれぞれ x, y とすると
が成り立つ。
変形して
として
つまり を2平方数の和で表す方法を列挙できればよい。
をガウス整数の積に分解して
だから、 のガウス素数への分解が与えられれば、u, v の組は簡単に列挙できる。
このためには、まず を有理素数に素因数分解して、各有理素数をガウス素数に分解すればよい。
例えば n=4 のとき、上の式は
で、 と素因数分解できる。73, 137 をそれぞれ とガウス素数に分解して、
右辺のガウス素数を左辺の因数に適当に割り振る。例えば
とすれば となって、 という解が得られる。
同様に
とすれば となって、 を得る(こちらは自明だけど)。
いちばん下にあるコードが提出したコードを少し修正したもの。言語は Ruby。
n=36 まで計算して ideone.com での実行時間は今走らせたら 0.65秒だった → http://ideone.com/mGfU6N (乱数を使っているので実行時間はばらつきがある)。
提出したものは浮動小数点の平方根 Math.sqrt を使っていたのだが、それだと丸め誤差のせいで大きな n でおかしくなるので、整数の平方根を自前で計算するように修正した。
有理素数への分解は Miller-Rabin 法と Pollard のρ法 (Pollard's rho algorithm)、有理素数のガウス素数への分解は Cornacchia-Smith のアルゴリズムによった。
の有理素数の素因数 q の片方のガウス素数の素因数は q と の共通の因数になる。
Cornacchia-Smith のアルゴリズムのところは(ガウス整数に対して gcd というのはたぶん書き方変だが) をユークリッドの互除方で求めるようなことをしている。
有理素数が4k+3型だと分解できないが、 の素因数は平方剰余の第1補充法則により4k+3型にはならないので、分解できない場合の処理は省略できる。
ユークリッドの互除法を呼んでいるところ
u = euclid(q, m, sqrt(q))
がちょっと怪しげで、英語版 Wikipedia の記事によるとここは
r = m % q r = q - r if 2 * r > q u = euclid(r, q, sqrt(q))
とでもなるところだけど*1、こうでなくてもテストした範囲では大丈夫そうなのでそのままにしてある。
実はここのところを書いたのがかなり前で、そのときは分かっていてこういう書き方になったのかもしれない。
(追記)
やっぱり r=q-r なんて要らないと思う。だって 2r>q なら互除法の最初の1段で r=q-r と同じ計算が行われるではないか。ということで r=m%q も要らなくて r のところにはそのまま m を突っ込んでもいいはず。
# Miller-Rabin法による素数判定 class Miller_Rabin def init_sd @s, @d = 0, @n >> 1 while @d.even? do @s += 1 @d >>= 1 end end def witness?(x) x = mod_pow(x, @d, @n) return true if x == 1 || x == @n - 1 @s.times.any? do x = x ** 2 % @n x == @n - 1 end end def probably_prime?(n, k = 20) @n = n return true if @n == 2 return false if @n < 2 || @n.even? init_sd k.times.all? {witness?(rand(@n - 1) + 1)} end end # ρ法による因数分解(Brent のアルゴリズムを使用) class Rho M = 64 def f(x) (x ** 2 + @c) % @n end def init_c begin @c = rand(@n - 1) + 1 end while @c == @n - 2 end def factor(n) @n = n init_c k = 1 x = y = rand(@n) loop do k.times {x = f(x)} m = [k, M].min (k / m).times do prod = m.times.inject(1) do |q| x = f(x) q * (x - y) % @n end g = @n.gcd(prod) return g if g > 1 end y = x k <<= 1 end end end # 素因数分解 class PF def factorize(n) return if n == 1 return @d[n] += 1 if Miller_Rabin.new.probably_prime?(n) q = Rho.new.factor(n) factorize(q) factorize(n / q) end # Miller-Rabin法とρ法により素因数分解を求める def prime_division(n) @d = Hash.new(0) factorize(n) @d.sort end end # b ** e % n を計算 def mod_pow(b, e, n) x = 1 loop do x = x * b % n if e.odd? return x if (e >>= 1) == 0 b = b ** 2 % n end end # ここまで(有理素数への)素因数分解のためのコード # (ライブラリの素因数分解が速ければ必要ない) require 'complex' # 正整数 a に対して floor(sqrt(a)) を求める def sqrt(a) x, y = 1 << a.to_s(4).size, a + 2 x, y = (x + a / x) / 2, x while x < y y end # ユークリッドの互除法 def euclid(x, y, c) x <= c ? x : euclid(y % x, x, c) end # m^2 + 1 をガウス素数の積に分解 def calc_gdiv(m) PF.new.prime_division(m ** 2 + 1).flat_map do |q, e| # Cornacchia-Smith のアルゴリズムにより有理素数 q をガウス素数に分解 # m^2+1 が 4k+1 型の素因数しか持たないことと # 各素因数 q について m が -1 の mod q での平方根であることを利用して # もとのアルゴリズムを簡略化してある u = euclid(q, m, sqrt(q)) v = sqrt(q - u ** 2) [u + v * Complex::I] * e end end # u^2 + v^2 = m^2 + 1 となる非負整数の組 (u, v) を列挙 def div2squares(gdiv, z = gdiv[0], k = 1) return [[z.real.abs, z.imag.abs]] if k == gdiv.size div2squares(gdiv, z * gdiv[k], k + 1) + div2squares(gdiv, z * gdiv[k].conj, k + 1) end # 問題の条件を満たす整数を列挙 def solve(n) m = 10 ** (n / 2) div2squares(calc_gdiv(m)).flat_map {|u, v| u, v = v, u if u.odd? [[u, v], [-u, v]] }.uniq.map {|u, v| [(u + m) / 2, (v + 1) / 2]}. reject {|x, y| x < m / 10 || y <= 1}. map {|x, y| m * x + y}.sort end # n=36 まで、問題の条件を満たす整数、それらの整数の個数と和を出力 2.step(36, 2) do |n| puts "n = #{n}" puts a = solve(n) puts "n_solutions = #{a.size}\nsum = #{a.inject(0, :+)}\n\n" end
*1:別のところでの説明ではここの不等号の向きが逆になっていて余計に話がややこしい