のてすきあ − 完全独習非プログラマー −

Haskell で Project Euler を解く素人遊び

Problem 74

145 → 1! + 4! + 5! = 1 + 24 + 120 = 145 である。
このように各桁の階乗の和を取る作業を繰り返すことで、一連の数列を得ることができる。例えば169なら 169 → 363601 → 1454 → 169 と長さ3のループ(4回目でもとに戻る)を得る。
完全なループにならない場合もある。例えば
69 → 363600 → 1454 → 169 → 363601 (→ 1454)
78 → 45360 → 871 → 45361 (→ 871)
540 → 145 (→ 145)

長さ60の数列を生成する数は1から1,000,000まで中にいくつあるか。


 ある範囲の数に何らかの処理を施した結果に対し、最大値や◯◯の性質を持つものが全部でいくつあるかを問うのはProject Eulerの常套である。大抵の場合は同じ計算が繰り返し出てくるため、連想リストを使って2度目以降は計算を省いて結果のみを参照する作戦も既に自分の中では常套手段になっている。

  今回出てくる繰り返しは、同じ数字の並べ替えに対する数列の長さの計算である。問題にある長さ60を生成する最小の数は1479であるが(これは探せば見つかる)、1,4,7,9を並べ替えた数(例えば4791)も当然長さ60の数列を生成する。これは数列の作り方から明らかである。

 単純に考えて、4桁の数に対して24回、5桁の数に対して120回は同じ計算が登場する見込みだ。実際は先頭がゼロになるケースとか、あるいは先の4791に対して4790のように1を0に置き換えられるケースとか(0! = 1! なので)、色々あるわけだが、とにかく同じ数字を使っている数、もっと言えば各桁の階乗の和が同じ数同士、は同じ数列を生成する。

 ということで1つ覚えの連想リストを使ってみるのである。思惑通り計算スピードは格段に早くなる。(後で比較のためにナイーブな計算と比べると20〜30倍は計算時間が違う)

 そんなわけでData.Mapを使って計算結果を記憶しながら、1,000,000までの数を走査していくことにしたが、実は今回はそれと別の目的でもData.Map.Mapを使っている。それはループの判定である。各桁の階乗の和をつないで数列を作っていくわけだが、その結果を単純なリストにすることもできる。その場合、1回和を計算する度に、出てきた答えがリストに含まれているかをelmを使って調べなければならない。その処理時間はリストの長さNに対してO(N)である。仮にリストでなく木を使えば、処理時間はO(logN)になる。リストの長さはまちまちだが、最大60ということが分かっているので、それくらいの探索はしなければならないのだろう。リストの長さが10でも4倍、60なら15倍、探索時間に差が出ることを考えると、ループの判定にも連想リスト使ってみようと思った。Haskellの連想リスト(Data.Map.Map)は二分木だからだ。

 長々書いたが、決して美しい出来ではない。もっと良い方法はあるのかも知れない。ハッキリ言ってMapでゴリ押しするのはダサいような気もする。しかし早いからやめられない。もっと美しい方法があればいいが。

普通☆