Project Euler 140

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


Problem 137と同じように見えますが、より難しくなっています。
同じように計算していくと、

(n + 3)x2 + (n + 1)x - n = 0

x有理数になる必要十分条件は判別式が平方数になることだから、

5n2 + 14n + 1 = m2
(5n + 7)2 - 5m2 = 44

ペル方程式に似た方程式、

x2 - 5y2 = 44

が解ければよいです。一般的に解法がどうなっているのか知りませんが、(x1, y1)をx2 - 5y2 = ±11の解、(x2, y2)をx2 - 5y2 = ±4の解(符号同順)とすれば、x + √5y = (x1 + √5y1)(x2 + √5y2)で与えられる(x, y)が元の解になっていることが簡単に確かめられます。後者はペル方程式なので、前者を求めます。

x2 - 5y2 = 11

最小解は(4, 1)なので、これにx2 - 5y2 = 1の最小解(9, 4)を「掛けて」いけばよい、と思いきやこれでは解が足りないんですね。別にx2 - 5y2 = -11の最小解(3, 2)の系統の解があります。これに、x2 - 5y2 = -1の最小解(2, 1)を「掛けた」(16, 7)から始まる解の列があります。同様に負のほうも求めて、±4の解と「掛けます」。昇順に出すのが難しいので、それは範囲を区切って適当に。あと重複があるので、それを排除します。

from itertools import *
from math import sqrt

def unique(g):
    prev = next(g)
    for e in g:
        if e != prev:
            yield prev
        prev = e
    yield prev

def unfold(f, s):
    while True:
        x, s = f(s)
        yield x

def gen_Pells(a, r, n):
    def mul(p, q):
        x1, y1 = p
        x2, y2 = q
        return (x1 * x2 + n * y1 * y2, x1 * y2 + y1 * x2)
    
    return unfold(lambda s: (s, mul(s, r)), a)

def gen_Pells4(a, r, n):
    def mul(p, q):
        x1, y1 = p
        x2, y2 = q
        return ((x1 * x2 + n * y1 * y2) / 2, (x1 * y2 + y1 * x2) / 2)
    
    return unfold(lambda s: (s, mul(s, r)), a)

def combine(g1, g2):
    v1 = g1.next()
    v2 = g2.next()
    while True:
        if v1 == v2:
            yield v1
            v1 = g1.next()
            v2 = g2.next()
        elif v1 < v2:
            yield v1
            v1 = g1.next()
        else:
            yield v2
            v2 = g2.next()

def gen_Pells_11():
    return combine(gen_Pells(( 4, 1), (9, 4), 5),
                   gen_Pells((16, 7), (9, 4), 5))


def gen_Pells__11():
    return combine(gen_Pells(( 3, 2), (9, 4), 5),
                   gen_Pells((13, 6), (9, 4), 5))

def gen_Pells_44():
    def mul(p, q):
        x1, y1 = p
        x2, y2 = q
        return (x1 * x2 + 5 * y1 * y2, x1 * y2 + y1 * x2)
    
    for y_limit in unfold(lambda s: (s, s * 10), 1):
        a = [ ]
        for p in takewhile(lambda p: p[1] < y_limit * 10, gen_Pells_4()):
            for r in takewhile(lambda r: r[1] <= y_limit * 10,
                     dropwhile(lambda r: r[1] <  y_limit,
                            (mul(p, q) for q in gen_Pells_11()))):
                a.append(r)
        
        for p in takewhile(lambda p: p[1] < y_limit * 10, gen_Pells__4()):
            for r in takewhile(lambda r: r[1] <= y_limit * 10,
                     dropwhile(lambda r: r[1] <  y_limit,
                            (mul(p, q) for q in gen_Pells__11()))):
                a.append(r)
        
        for r in unique(r for r in sorted(a)):
            yield r

N = 30
def gen_Pells__4(): return gen_Pells4((1, 1), (3, 1), 5)
def gen_Pells_4():  return gen_Pells4((3, 1), (3, 1), 5)
g = ((n - 7) / 5 for n, m in gen_Pells_44() if n % 5 == 2)
print(sum(islice(g, N)))