Project Euler Problem 8

問題8: 提示された1000桁の数字列内で連続する5つの数字の積の最大はいくつか。[=>原文和訳]

問題8 解答

 ちょっと凝った作りになりました。

(defun problem008 (str)
  "数字列内の5個の連続する数字の積の最大値を取得"
  (let ((N 5) (ans 0) (blk nil))
    (dolist (blk (split-string str "0") ans)
      (when (<= N (length blk))
        (let* ((lis (mapcar
                     #'(lambda (c) (- c ?0)) blk))
               (tmp (apply #'* (subseq lis 0 N))))
          (if (> tmp ans)
              (setq ans tmp))
          (let ((lis-len (length lis)))
            (if (> lis-len N)
                (let ((vec (vconcat lis))
                      (i 0) (j N))
                  (while (< j lis-len)
                    (setq tmp
                          (* (/ tmp
                                (aref vec i))
                             (aref vec j)))
                    (if (> tmp ans)
                        (setq ans tmp))
                    (incf i)
                    (incf j))))))))))

 5文字の中に 0 が含まれると積も 0 になります。0 をまたいだ数列は解になりえないので、つまり全体文字列を 0 で split して、分割されたそれぞれのブロック内での積を調べればよいわけです。分割されたブロックが 5文字未満の場合そのブロックは脱落です。
 6行目の let* は2つの変数を束縛します。1つ目の lis は 分割された1ブロック(blk)をリストに変換したものです。文字列をリストにする際、各要素から文字'0'(Emacs Lisp では ?0 と表記)を引くことで、数値リストに変換しています。
 let* の2つ目の変数 tmp は、lis のサブシーケンスの積で初期化されます。最初のN(=5)要素を subseq で切り出しています。以降 tmp は 5つの数字の積を作るための一時変数として使います。また、ansは解となる値を保持する変数で、最終的には dolist の評価値となります。
 もし lis が 6要素以上であれば 15行目の while で順にチェックを行います。ループ内では 5文字の先頭の数字で tmp を割り、次に5文字に入ってくる数字を掛けることで 1文字スライドした 5文字の積を新たに取得しています。ランダムアクセスが必要になるのであらかじめ lis は vector に変換しておきます。vconcat は複数のシーケンスを結合して新たな vector を作る関数ですが、単体のリストや文字列に使うと vectorへの型変換関数として機能します。
 あとは、tmp が ans を超えたら ans を更新する処理を繰り返すだけです。 

もっとシンプルに

 今回の問題はそこまで凝った作りにしなくても解けます。

(defun problem008-2 (str)
  (let ((N 5) (ans 0) (i 0)
        (lis (mapcar
              #'(lambda (c) (- c ?0)) str)))
    (dotimes (i (- (length lis) N) ans)
      (let ((tmp (apply
                  #'*
                  (subseq lis i (+ i N)))))
        (if (< ans tmp)
            (setq ans tmp))))))

 とにかく1文字ずつずらしながら 5要素を取り出し毎回5つの積をとる。0があろうと無かろうとお構いなし。無駄はあるけどシンプルなプログラムです。これでも一瞬で答が出ます。

比べてみよう

 profile モジュールを使って、一万回の繰り返しの平均を出してみました。

(defvar data
  (concat
  "73167176531330624919225119674426574742355349194934"
  "96983520312774506326239578318016984801869478851843"
  "85861560789112949495459501737958331952853208805511"
  "12540698747158523863050715693290963295227443043557"
  "66896648950445244523161731856403098711121722383113"
  "62229893423380308135336276614282806444486645238749"
  "30358907296290491560440772390713810515859307960866"
  "70172427121883998797908792274921901699720888093776"
  "65727333001053367881220235421809751254540594752243"
  "52584907711670556013604839586446706324415722155397"
  "53697817977846174064955149290862569321978468622482"
  "83972241375657056057490261407972968652414535100474"
  "82166370484403199890008895243450658541227588666881"
  "16427171479924442928230863465674813919123162824586"
  "17866458359124566529476545682848912883142607690042"
  "24219022671055626321111109370544217506941658960408"
  "07198403850962455444362981230987879927244284909188"
  "84580156166097919133875499200524063689912560717606"
  "05886116467109405077541002256983155200055935729725"
  "71636269561882670428252483600823257530420752963450"))

(require 'profile)
(setq profile-functions-list
      '(problem008 problem008-2))

(let ((i 0))
  (dotimes (i 10000)
    (problem008 data)
    (problem008-2 data)))

 あらかじめ、profile-functions-list に測定したい関数を登録して、M-x profile-functions を実行した後、好きなだけ関数を実行します。その後 M-x profile-results を実行するとこんな風に結果が表示されます。

Function       Calls  Total time (sec)  Avg time per call
============  ======  ================  =================
problem008     10000         10.349000           0.001035
problem008-2   10000         47.261000           0.004726

 一応、いろいろ考えたのが報われてパフォーマンスの差が出たようです。よかった。
 計測をやり直す場合は、M-x profile-finish を実行し、最初から手順をやり直します。