問題
ある関数を繰り返し適用して得られる数列が最も長くなる初期値n(1,000,000未満)を求める問題です。
解答
nから始まる数列の長さをlen(n)で表すと、次のように定義できます:
- len(n) = 1 (n = 1)
- len(n) = len(3n + 1) + 1 (nは奇数)
- len(n) = len(n / 2) + 1 (nは偶数)
つまり、len(n)の計算は1.の場合を基底ケースとする再帰アルゴリズムとして実装できます。
これをそのまま実装しても答えが得られますが、非常に遅いです。高速化の為に少し検討を加えましょう。
まず56から始まる数列を考えます。56は偶数なので3.の場合に該当し、len(56) = len(56 / 2) + 1 = len(28) + 1です。また、9から始まる数列の場合は2.に該当し、len(9) = len(3 * 9 + 1) + 1 = len(28) + 1となります。 つまりlen(56) = len(9) = len(28) + 1というわけで、len(56)とlen(9)の値は実は同じです。2.と3.の再帰式を見て予想できるように、このような重複は度々起こるので、それぞれの計算でlen(28)の計算をやり直すようなことをしていると大きな無駄となります。
これを解決するには過去に計算したlen(n)の値を覚えておき、次に同じnが与えられた時には覚えていた値を返すようにします。そうすることで2度目以降の関数呼び出しでは計算を省くことができ、時間の節約になります。これは同じ引数に対して常に同じ値を返す(つまり参照透過性がある)関数一般に使えるテクニックで、これをメモ化と呼びます。
そう言うわけで長々と説明しましたが、要はProblem 10で使ったルックアップ・テーブルと同じものです。今回はクロージャ生成時に確保されたベクタの長さより大きいn
が与えられる場合(十分大きい奇数が与えられた時)が有り得るので、上限(upper-limit
)より大きいn
に対してはその都度計算し直しています。
(use srfi-1)
(define (get-sequence-length next-value upper-limit)
(define lookup-table (make-vector (+ upper-limit 1) 0))
(define (sequence-length n)
(let ((len (if (> n upper-limit) 0 (ref lookup-table n))))
(if (not (zero? len)) len
(let ((len
(+ 1 (sequence-length (next-value n)))))
(unless (> n upper-limit) (set! (sequence-length n) len))
len))))
(set! (setter sequence-length)
(lambda (n len) (set! (ref lookup-table n) len)))
(set! (sequence-length 1) 1)
sequence-length)
(define (solve)
(define sequence-length
(get-sequence-length (lambda (n) (if (even? n) (/ n 2) (+ (* 3 n) 1)))
999999))
(car (fold (lambda (a b) (if (> (cdr a) (cdr b)) a b))
'(0 . 0)
(map (lambda (n) (cons n (sequence-length n))) (iota 999999 1)))))
(define (main argv)
(display (solve))
(newline))
コメント
コメントを投稿