以下の1000桁の数字の列から隣接する13桁の積が最大の部分を探す
(答えはその積)
7316717653133062491922511967442657474235534919493496983520312774506326239578318016984801869478851843858615607891129494954595017379583319528532088055111254069874715852386305071569329096329522744304355766896648950445244523161731856403098711121722383113622298934233803081353362766142828064444866452387493035890729629049156044077239071381051585930796086670172427121883998797908792274921901699720888093776657273330010533678812202354218097512545405947522435258490771167055601360483958644670632441572215539753697817977846174064955149290862569321978468622482839722413756570560574902614079729686524145351004748216637048440319989000889524345065854122758866688116427171479924442928230863465674813919123162824586178664583591245665294765456828489128831426076900422421902267105562632111110937054421750694165896040807198403850962455444362981230987879927244284909188845801561660979191338754992005240636899125607176060588611646710940
4桁の積が最大の部分は
Project Euler ※解法の公開は問1~100のみ可
一番シンプルにHaskellらしい書き方
digits1000 :: [Int]
digits1000 = [7,3,1,6,7..]
solve :: [Int] -> Int
solve = maximum $ map (product . take 13) $ tails digits1000
tailsで1つずつ先頭を除いたリストを作っていき、そのリストごとに最初の13個の積を求めて最大値を探すだけ
遅延評価のおかげか、リストがたくさん出来ても先頭13桁しか評価されないのでそこまで重い処理ではなさそう
しかも、簡潔でHaskellらしい書き方になっている
上記のやり方は、書き方としては美しい。しかし、誰がどう見ても積を計算して比較するのは無駄
少なくとも命令型の言語でならこんなに無駄な計算をせずにアルゴリズムを工夫するはず!
と、言うわけで考えてみる
今現在、1000桁の数字の中のとある13桁の数字を最大値として持っているとする。
1,2,3,4, [3,5,6,8,2,4,5,3,7,4,5,1,9] x,2,7,8
次の数字xが今持っている13桁の先頭3よりも大きければ、間違いなく最大値になるはず
ghci> product [3,5,6,7,2,4,5,3,7,4,5,1,9]
95256000
ghci> product [5,6,7,2,4,5,3,7,4,5,1,9,4]
127008000
こういう考えで、digits1000を先頭から走査して、1つ進む毎に出てく値と入ってくる値を比較するという方法をとれれば積の計算はいらなさそう
このままではいくつかの障壁にぶち当たる
常に最大を更新し続けるのなら簡単だが、出て行く値の方が大きくなった場合にどうするか?
例えば
1,2,3,4, [3,5,6,8,2,4,5,3,7,4,5,1,9] 4,2,7,8 = 95256000
-> 1,2,3,4,3, [5,6,8,2,4,5,3,7,4,5,1,9,4] ,2,7,8 大きくなった = 127008000 ※最大
-> 1,2,3,4,3,5, [6,8,2,4,5,3,7,4,5,1,9,4,2] ,7,8 小さくなった = 58060800
-> 1,2,3,4,3,5,6, [8,2,4,5,3,7,4,5,1,9,4,2,7] ,8 大きくなった = 67737600
最大値になるたびに保存したいが、大きくなるたびに最大値になるわけではないので結局積を比較しないといけない
0をどうするか?
積を求める方法では、簡単に0が入っていることが分かるが今の方法ではどうするか
13桁のリストを保存するのはもったいない?
先頭のインデックスだけ保存しとけば良さそう
0が入ってきたらカウントを"+"、出て行ったら"-"をしてカウントが0のときは13桁の中に0は含まれていないと出来る
Rational (分数)を使おう!
Haskellには分数を簡単に扱う型Rationalがある
1,2,3,4, [3,5,6,8,2,4,5,3,7,4,5,1,9] 4,2,7,8 = 95256000
-> 1,2,3,4,3, [5,6,8,2,4,5,3,7,4,5,1,9,4] ,2,7,8 大きくなった = 127008000 = 95256000 * (4/3)
-> 1,2,3,4,3,5, [6,8,2,4,5,3,7,4,5,1,9,4,2] ,7,8 小さくなった = 58060800 = 12700800 * (2/5)
-> 1,2,3,4,3,5,6, [8,2,4,5,3,7,4,5,1,9,4,2,7] ,8 大きくなった = 67737600 = 58060800 * (7/6)
このように、積は"入ってくる値 / 出て行く値"倍になる
これを利用して分数が1より大きくなったら最大値が更新されたとし、分数を1に戻すという操作をすれば
1STEPの計算は簡単な分数のかけ算のみにできる
例
1,2,3,4, [3,5,6,8,2,4,5,3,7,4,5,1,9] 4,2,7,8 = 95256000 (Rational = 1)
-> 1,2,3,4,3, [5,6,8,2,4,5,3,7,4,5,1,9,4] ,2,7,8 大きくなった = 127008000: (Rational = 4/3) -> ここでRational1に戻す
-> 1,2,3,4,3,5, [6,8,2,4,5,3,7,4,5,1,9,4,2] ,7,8 小さくなった = 58060800: (Rational = 1 * 2/5)
-> 1,2,3,4,3,5,6, [8,2,4,5,3,7,4,5,1,9,4,2,7] ,8 大きくなった = 67737600: (Rational = 2/5 * 7/6 = 14/15) !!! 大きくなったが最大でない
今の実力では限界だけどもっと綺麗に書けそう
純粋に上位互換のアルゴリズムがあるはず
計算量, 見やすさ, 文化的な観点から見て改善点や他回答があればぜひお願いします!
実務1年程度のへっぽこプログラマーなので暖かくも厳しく見守ってください
読んだ本
型の面白さに気づき始めました