Project Euler 73

プロジェクトオイラー
http://projecteuler.net/index.php?section=problems&id=73


前にこの問題を解いた頃は、解ければいいや、ということでずいぶん雑にコードを書いていた。この日は9問解いたようだ。
とにかく何の工夫もなくしらみつぶしに調べていた。


from itertools import imap
from fractions import gcd

def count_n(d):
return sum(imap(lambda n: gcd(n, d) == 1,
xrange(d / 3 + 1, (d + 1) / 2)))

def solve():
return sum(imap(count_n, xrange(2, N + 1)))

N = 12000
print solve()

分母を固定し、分子を変えて、互いに素だったらカウントする。これで40秒近くかかる。
しかし、次のように考えると速くなる。
例えば、分母を60とすると、まず次のような分子が考えられる。

21 22 23 24 25 26 27 28 29

(60 - 1) / 2 - 60 / 3 = 9個ある。
しかし、分子が2の倍数だと約分できるので、これらは取り除く。

22 24 26 28

(60 - 1) / (2 * 2) - 60 / (3 * 2) = 4個ある。
同様に3の倍数、5の倍数も取り除く。

しかし、6の倍数は重複しているので、この分の個数はまた追加する。
このような手順を組み込むと速くなる。


from itertools import imap, ifilter

def make_primes(max_n):
def is_prime(n):
for p in primes:
if p * p > n:
return True
elif n % p == 0:
return False
return True

primes = [ 2 ]
for n in ifilter(is_prime, xrange(3, max_n + 1, 2)):
primes.append(n)
return primes

def factorize(max_n):
b = range(max_n + 1)
a = [ () for n in b ]
for p in primes:
for n in xrange(p, max_n + 1, p):
b[n] /= p
a[n] += (p, )
while b[n] % p == 0:
b[n] /= p

for n in xrange(max_n + 1):
p = b[n]
if p != 1:
a[n] += (p, )

return a

def gen_divisors(a, k0 = 0):
yield 1

for k in xrange(k0, len(a)):
d = a[k]
for d2 in gen_divisors(a, k + 1):
yield -d * d2

def counter_div(n, d):
if d > 0:
return (n - 1) / (2 * d) - n / (3 * d)
else:
d = -d
return -( (n - 1) / (2 * d) - n / (3 * d))

def count_n(d):
counter = 0
return sum(imap(lambda d1: counter_div(d, d1), gen_divisors(f[d])))

def solve():
return sum(imap(count_n, xrange(2, N + 1)))

N = 12000
primes = make_primes(int(N ** 0.5 + 0.5))
f = factorize(N)
print solve()

こうしたら、0.4秒ほどになった。


このように何も考えないでも解けるけれど、工夫すれば速くなる問題はたくさんあるのではないかと思う。
Forumの最初のほうを見たら、何も考えていないバージョンばかりだった。