Project Euler 86

Problem 86
蜘蛛Sが直方体で大きさが6×5×3の部屋の一つの角に座っており、蝿Fが反対側の角に座っている。部屋の表面を進んでSからFの最短の「直線」距離は10(中略)。
どの与えられた直方体にも「最短」経路候補が3つまであり、その最短ルートはいつも整数であるとは限らない。
大きさが整数でM×M×Mまでの直方体の部屋すべてを考えると、M=100のとき最短距離が整数になる直方体は2060ある。そしてこれは解の個数が2000を最初に超える最小のMである。M=99なら解の個数は1975である。
解の個数が100万を最初に超える最小のMを求めよ。
http://projecteuler.net/index.php?section=problems&id=86

直方体の辺の長さをpqrとすると、最短距離は

√((p + q)2 + r2)

となります。要するに、p + qrとこれで直角三角形を成します。
いつものようにピタゴラス数の生成式を使います。

a = l(m2 - n2) b = 2lmn c = l(m2 + n2)

例えば大きさ6までとして、(6, 8, 10)という直角三角形が生成されたとき、8を分割して

(2, 6, 6), (3, 5, 6), (4, 4, 6)

という直方体が生成されます。大きさが8までなら6,8のどちらかを分割して、

(1, 5, 8), (2, 4, 8), (3, 3, 8), (2, 6, 6), (3, 5, 6), (4, 4, 6)

という直方体が距離10になります。このようにして直方体の数を数えて、Mを一つずつ大きくして直方体の数が100万を超えたところでやめます(Code1)。
しかしこれは遅いです。Mを探索するのに1ずつ増すのではなく、二分探索を使いましょう。その前に1から始めて倍々して100万を超えるMを求め、そのMと前のMの間で二分探索します。これでかなり速くなります(Code2)。
より速い方法があります。それは少なくとも一つの辺がちょうどMになる直方体を求めるものです。それには直角三角形の斜辺以外のどちらかがMになる必要があります。すなわち、ピタゴラス数の生成式のl,m,nを使ってM = l(m2 - n2)またはM = 2lmnにならなければなりません。そこで、Mを素因数分解して約数を求めて個数を数えていきます(Code3)。
素因数分解をエラトステネスのふるい的にしてもみましたが、あまり速くなりませんでした。

# Code1
from itertools import imap, ifilter, count
from fractions import gcd
from math import sqrt

def head(a):
    for e in a:
        return e

def S(M):
    def f(m):
        counter = 0
        for n in ifilter(lambda n: gcd(m, n) == 1,
                                xrange(2 if m & 1 else 1, m, 2)):
            a = m * m - n * n
            b = 2 * m * n
            if a > b:
                a, b = b, a
            for l in xrange(1, M / a + 1):
                c, d = a * l, b * l
                if d <= M:
                    counter += c / 2
                    if c * 2 > d:
                        counter += c - (d - 1) / 2
                elif c <= M and d < c * 2:
                    counter += (c * 2 - d + 2) / 2
        return counter
    
    return sum(imap(f, xrange(2, (M + 1) / 2 + 1)))

N = 10 ** 6
a = head(ifilter(lambda x: x[1] > N, ((M, S(M)) for M in count(1))))
print a[0]
# Code2
from itertools import imap, ifilter
from fractions import gcd
from math import sqrt

# SはCode1と同じ

def bin_search(begin, end):
    if end - begin == 1:
        return begin
    else:
        mid = (begin + end - 1) / 2
        n = S(mid)
        if n <= N:
            return bin_search(mid + 1, end)
        else:
            return bin_search(begin, mid + 1)

N = 10 ** 6
M = 1
while True:
    n = S(M)
    if n > N:
        break
    M *= 2
print bin_search(M / 2 + 1, M + 1)
# Code3
from itertools import imap, ifilter, count, takewhile
from fractions import gcd
from math import sqrt

def head(a):
    for e in a:
        return e

def is_prime(n):
    return all(n % p != 0 for p in takewhile(lambda p: p * p <= n, primes))

def gen_primes():
    for k in count():
        if k == len(primes):
            primes.append(head(ifilter(is_prime, count(primes[-1] + 1))))
        yield primes[k]

def div_pow(n, p):
    e = 0
    while n % p == 0:
        n /= p
        e += 1
    return n, e

def factorize(n):
    if n == 1:
        return [ ]
    
    for p in takewhile(lambda p: p * p <= n, gen_primes()):
        n, e = div_pow(n, p)
        if e > 0:
            return [(p, e)] + factorize(n)
    
    return [(n, 1)]

def gen_divisors(f, k = 0):
    if k == len(f):
        yield 1, 1
    else:
        p, e = f[k]
        n = p ** e
        for d1, d2 in gen_divisors(f, k + 1):
            if p == 2:
                if e >= 2:
                    yield d1 * n, d2
                elif e == 0:
                    yield d1, d2
            elif e > 0:
                yield d1 * n, d2
                yield d1, d2 * n
            else:
                yield d1, d2

def gen_divisors2(f, k = 0):
    if k == len(f):
        yield 1, []
    else:
        p, e0 = f[k]
        for d1, f2 in gen_divisors2(f, k + 1):
            for e in range(e0 + 1):
                yield d1 * p ** e, [(p, e0 - e)] + f2

def gen_divs(f):
    for d1, f2 in gen_divisors2(f):
        for d2, d3 in gen_divisors(f2):
            yield d1, d2, d3

def count_cuboid(M, b):
    if b < M:
        return b / 2
    elif b < M * 2:
        return M - (b - 1) / 2
    else:
        return 0

def T(M):
    counter = 0
    f = factorize(M)
    for l, d1, d2 in gen_divs(f):
        if (d1 + d2) % 2 == 0:
            if d1 <= d2:
                continue
            m = (d1 + d2) / 2
            n = m - d2
            b = 2 * l * m * n
        else:
            m = d1 / 2
            n = d2
            if m < n:
                m, n = n, m
            b = l * (m * m - n * n)
        counter += count_cuboid(M, b)
    return counter

def gen_S():
    s = 0
    for M in count(1):
        s += T(M)
        yield M, s

N = 10 ** 6
primes = [ 2 ]
a = head(ifilter(lambda x: x[1] > N, gen_S()))
print a[0]