-
-
Save tnoda/4394295 to your computer and use it in GitHub Desktop.
| (ns tnoda.projecteuler.problem-11 | |
| [:require [clojure.string :as str]]) | |
| (def ^:private input "08 02 22 97 38 15 00 40 00 75 04 05 07 78 52 12 50 77 91 08 | |
| 49 49 99 40 17 81 18 57 60 87 17 40 98 43 69 48 04 56 62 00 | |
| 81 49 31 73 55 79 14 29 93 71 40 67 53 88 30 03 49 13 36 65 | |
| 52 70 95 23 04 60 11 42 69 24 68 56 01 32 56 71 37 02 36 91 | |
| 22 31 16 71 51 67 63 89 41 92 36 54 22 40 40 28 66 33 13 80 | |
| 24 47 32 60 99 03 45 02 44 75 33 53 78 36 84 20 35 17 12 50 | |
| 32 98 81 28 64 23 67 10 26 38 40 67 59 54 70 66 18 38 64 70 | |
| 67 26 20 68 02 62 12 20 95 63 94 39 63 08 40 91 66 49 94 21 | |
| 24 55 58 05 66 73 99 26 97 17 78 78 96 83 14 88 34 89 63 72 | |
| 21 36 23 09 75 00 76 44 20 45 35 14 00 61 33 97 34 31 33 95 | |
| 78 17 53 28 22 75 31 67 15 94 03 80 04 62 16 14 09 53 56 92 | |
| 16 39 05 42 96 35 31 47 55 58 88 24 00 17 54 24 36 29 85 57 | |
| 86 56 00 48 35 71 89 07 05 44 44 37 44 60 21 58 51 54 17 58 | |
| 19 80 81 68 05 94 47 69 28 73 92 13 86 52 17 77 04 89 55 40 | |
| 04 52 08 83 97 35 99 16 07 97 57 32 16 26 26 79 33 27 98 66 | |
| 88 36 68 87 57 62 20 72 03 46 33 67 46 55 12 32 63 93 53 69 | |
| 04 42 16 73 38 25 39 11 24 94 72 18 08 46 29 32 40 62 76 36 | |
| 20 69 36 41 72 30 23 88 34 62 99 69 82 67 59 85 74 04 36 16 | |
| 20 73 35 29 78 31 90 01 74 31 49 71 48 86 81 16 23 57 05 54 | |
| 01 70 54 71 83 51 54 69 16 92 33 48 61 43 52 01 89 19 67 48") | |
| (def ^:private grid {:width 20, | |
| :numbers (->> (str/split input #"\s+") | |
| (map #(Long/parseLong %)))}) | |
| (defn- lines | |
| [w] | |
| (fn | |
| [s] | |
| (map #(take-nth % s) [1 (dec w) w (inc w)]))) | |
| (defn- fence | |
| [w nums] | |
| (->> (partition w nums) | |
| (interpose [0]) | |
| (apply concat))) | |
| (defn- muln | |
| [n] | |
| (fn | |
| [s] | |
| (if-let [nums (first (partition n s))] | |
| (apply * nums)))) | |
| (defn- solver* | |
| [{w :width nums :numbers}] | |
| (->> (fence w nums) | |
| (iterate next) | |
| (take-while seq) | |
| (mapcat (lines (inc w))) | |
| (keep (muln 4)) | |
| (apply max))) | |
| (def solver (partial solver* grid)) |
lines はそもそも
(is (= ((lines 3) '(a b c
d e f
g h i))
'((a b c , d e f g h i) ; right?
(a , c e g i) ; left-down?
(a d g) ; down
(a e i)))) ; right-down
(is (= ((lines 3) '( b c
d e f
g h i))
'((b c , d e f g h i ) ; right?
(b d , f h) ; left-down?
(b e h) ; down
(b f )))) ; right-downのような使われ方を想定して書き , で示した箇所で枠を飛び出して反対側から出て来てしまうので lines は変えずに入力の方を fence で変形して,
(is (= ((lines 4) '(a b c
0 d e f
0 g h i))
'((a b c 0 d e f 0 g h i) ; right
(a 0 f h) ; left-down
(a d g) ; down
(a e i)))) ; right-down
(is (= ((lines 4) '( b c
0 d e f
0 g h i))
'((b c 0 d e f 0 g h i ) ; right
(b d 0 i) ; left-down
(b e h) ; down
(b f )))) ; right-downのようにして, 枠を越えて積を取ると 0 になるようにして, 使うことにしたってことですよね.
最初読んだときは, 誤解していて, 自分の解法に似てると思ったんですが, ちゃんと読んだら全然違いました.
ユニークな解法でとても参考になります.
コードだけからだと lines と fence が何のために何をするのか分かりにくいので, 上記のようなテストを付けるといいかなと思いました.
lines と muln が計算をしてしまわず, 計算する関数を返すのが, 定義を読んでるときは不思議だったのですが ->> で呼び出されているところを見て納得. すっきりしますね.
コードだけからだと lines と fence が何のために何をするのか分かりにくいので, 上記のようなテストを付けるといいかなと思いました.
おっしゃるように,例が無いと一目で何をしている関数なのか分かりにくいです > lines, fence
@kohyama の指摘どおり,きちんとテストを書くべきなのですが,Project Euler だとついつい手抜きしてしまいます.ごめんなさい.
本来ならテストと言わず docstring に利用例を書いておくほうが利用者に優しいライブラリになるのですが,この例を自動で抽出してテストできれば,テストを書く手間が省けて一石二鳥になります.どなたか,Python にある doctest の Clojure 版をご存知の方いらっしゃいませんか? あれば使ってみたいです.
どなたか,Python にある doctest の Clojure 版をご存知の方いらっしゃいませんか? あれば使ってみたいです.
と思って探していたら with-test を見つけました.
考え方
take-nthを使います.grid入力のサイズが変わっても対応できるように,入力のグリッドを一行の幅
:widthと,グリッドに含まれる数列:numbersとで表現します.:numbersを:widthで折り返してラスタスキャンするイメージです.lines数列
:numbersの n 番目の数について,一行の幅を w とすると,の数ということになります.この形は
take-nthで表現できます.linesは一行の幅wを受け取り,数列が与えられたときに,数列の先頭から縦横斜めのラインを作る関数を返します.fencelinesの方法ではグリッドに端があることを考慮していません.ちょうどグリッドを右端と左端をくっつけて円筒状にした形を考えてラインを引いています.これでは実際には引けない線も含まれてしまうので,右端と左端との間に 0 を入れて,端をまたぐ線の積が 0 になるようにします.fence関数は幅wのグリッドの数列numsを受けとり,グリッドの左端に0だけの列を,グリッドの下端に0だけの行を,それぞれくっつけた幅w+1のグリッドを返します.配列にマージンをつけ加えてコードをシンプルにする技法は何か名前がついていたような気がするのですが思いだせませなかったので,fenceという名前にしました.solver*関数中ではこのfenceを使って入力の幅を 1 大きくしているので,lines関数を呼ぶときには幅を(inc w)としています.mulnシーケンスを受けとり先頭から
n個の要素の積を返す関数を返します.シーケンスの長さがn未満であればnilを返します.この問題では n=4 固定なのですが気持ち汎用性を考えています.solver*で解き方なのですが,
(fence w nums)(iterate next) (take-while seq)(mapcat (lines (inc w))(keep (muln 4))(apply max)という関数を作っています.
速度
問題の入力 (20 x 20) で 10ms と,やはり全部遅延シーケンスだけあって遅いです.200 x 200 程度で 1 秒越えそうです.
おわりに
この
solver*は今後も拡張しやすい形にすることも目指しました.この問題では「4 つの数の積」ですが,mulnにこの長さを渡せるようにしているので,4を可変にしたければ,solver*の引数にlenを追加して,(muln 4)のところを(muln len)とすればできます.また積以外に和にも対応させたい場合には,mulnの代わりに(calc n op)のように演算子opを渡せるような関数を用意すればできます.その際にはfenceは0でガードするのではなくnilでガードすることになると思います.あまり慣れないスタイルで書いているので,いろいろとおかしいところがあるかもしれません.コーディングスタイルやら値の命名やらで気になるところがありましたらご指摘よろしくお願いします.
Gist 移転時の更新
mulnに渡す数列の長さがn未満のときにnilを返すようにしました.これにより,fenceでグリッドの底に0を敷く必要がなくなったので,fence自体もconsを使ったものからinterposeを使ったスッキリとしたものに変更しました.