Project Euler 122

http://projecteuler.net/index.php?section=problems&id=122


べき乗の計算はいわゆるバイナリ法を使うと速いですが、実はバイナリ法より乗算の回数が少ない方法があるという話です。
例えば5乗なら、

a -> a2 -> a4 -> a5
a -> a2 -> a3 -> a5
a -> a2 -> a3 -> a4 -> a5

という計算手順があります。これらを、

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

と表します。最初の手順から一手で

a5 -> a6
a5 -> a7
a5 -> a9
a5 -> a10

このように全ての手順を求めて、最小の乗算の回数の和を取ります。

N = 20
a = [ [], [(1,)] ] + [ [] for k in xrange(N - 1) ]
for x in (x for n in xrange(1, N) for x in a[n]):
    for n in (n for n in (m + x[-1] for m in x) if n <= N):
        a[n].append(x + (n,))

print sum(min(len(x) - 1 for x in a[n]) for n in xrange(1, N + 1))

しかしこれは遅いです。乗算の回数が多すぎる手順まで記憶してしまうからです。ただ、どこで打ち切ればいいのかがよくわかりません。とりあえず、ここでは半分から計算しています。例えば、a^7なら、(a^3)^2 * aだからa^3の回数+2を最小とします。これでは過剰のように思えますが、正しい答えが出ます。

from itertools import *

def calc_max_m(c, n):
    if n % 2 == 0:
        return c[n/2] + 1
    else:
        return c[n/2] + 2

N = 200
M = next(n for n in count() if 1 << n >= N)
a = [ [] for k in xrange(N + 1) ]
a[1] = [ (1,) ]
c = [ 0 ]
for n in xrange(1, N + 1):
    c.append(min(len(x) - 1 for x in a[n]))
    max_m = calc_max_m(c, n)
    for x in a[n]:
        for m in x:
            n = m + x[-1]
            if n <= N and len(x) <= calc_max_m(c, n):
                a[n].append(x + (n,))

print sum(c[n] for n in xrange(1, N + 1))