多くの場合、タスクには実際の配列が必要です。たとえば、Befungeまたは> <>を実装するタスクを取ります。私はArray
このためにモジュールを使用しようとしましたが、非常に冗長でコーディングしているように感じるので、本当に面倒です。そのようなコードゴルフのタスクをより冗長で機能的にする方法を誰かが私に助けてもらえますか?
多くの場合、タスクには実際の配列が必要です。たとえば、Befungeまたは> <>を実装するタスクを取ります。私はArray
このためにモジュールを使用しようとしましたが、非常に冗長でコーディングしているように感じるので、本当に面倒です。そのようなコードゴルフのタスクをより冗長で機能的にする方法を誰かが私に助けてもらえますか?
回答:
まず第一に、場合によってはData.Arrayのより良い代替手段であるData.Vectorを調べることをお勧めします。
Array
とVector
に示されているように、いくつかのメモ化の例のための理想的な「最大のパスの検索」に私の答え。ただし、いくつかの問題は機能的なスタイルで表現するのは簡単ではありません。たとえば、プロジェクトオイラーの問題28では、スパイラルの対角線上の数値を合計する必要があります。確かに、これらの数値の式を見つけるのは非常に簡単なはずですが、スパイラルの構築はより困難です。
Data.Array.STは可変配列タイプを提供します。ただし、型の状況は混乱です。runSTArrayを除くメソッドのすべてをオーバーロードするためにクラスMArrayを使用します。したがって、可変配列アクションから不変配列を返す予定がない限り、1つ以上の型シグネチャを追加する必要があります。
import Control.Monad.ST
import Data.Array.ST
foo :: Int -> [Int]
foo n = runST $ do
a <- newArray (1,n) 123 :: ST s (STArray s Int Int) -- this type signature is required
sequence [readArray a i | i <- [1..n]]
main = print $ foo 5
それでも、Euler 28に対する私の解決策はかなりうまくいき、私はを使用しrunSTArray
たため、そのタイプシグネチャを必要としませんでした。
可変配列アルゴリズムの実装を検討している場合、別のオプションはData.Mapを使用することです。配列を使用する場合、配列の単一の要素を変更する次のような関数があればいいのに、と思っています。
writeArray :: Ix i => i -> e -> Array i e -> Array i e
残念ながら、実装でコピーオンライト戦略を使用して可能な限り回避しない限り、アレイ全体をコピーする必要があります。
良いニュースは、次のData.Map
ような機能があり、insertです:
insert :: Ord k => k -> a -> Map k a -> Map k a
Map
バランスの取れたバイナリツリーとして内部的に実装されているため、insert
O(log n)の時間とスペースのみを取り、元のコピーを保持します。したがって、Map
関数型プログラミングモデルと互換性のあるある程度効率的な「可変配列」を提供するだけでなく、必要に応じて「過去に戻る」こともできます。
Data.Mapを使用したオイラー28のソリューションを次に示します。
{-# LANGUAGE BangPatterns #-}
import Data.Map hiding (map)
import Data.List (intercalate, foldl')
data Spiral = Spiral Int (Map (Int,Int) Int)
build :: Int -> [(Int,Int)] -> Map (Int,Int) Int
build size = snd . foldl' move ((start,start,1), empty) where
start = (size-1) `div` 2
move ((!x,!y,!n), !m) (dx,dy) = ((x+dx,y+dy,n+1), insert (x,y) n m)
spiral :: Int -> Spiral
spiral size
| size < 1 = error "spiral: size < 1"
| otherwise = Spiral size (build size moves) where
right = (1,0)
down = (0,1)
left = (-1,0)
up = (0,-1)
over n = replicate n up ++ replicate (n+1) right
under n = replicate n down ++ replicate (n+1) left
moves = concat $ take size $ zipWith ($) (cycle [over, under]) [0..]
spiralSize :: Spiral -> Int
spiralSize (Spiral s m) = s
printSpiral :: Spiral -> IO ()
printSpiral (Spiral s m) = do
let items = [[m ! (i,j) | j <- [0..s-1]] | i <- [0..s-1]]
mapM_ (putStrLn . intercalate "\t" . map show) items
sumDiagonals :: Spiral -> Int
sumDiagonals (Spiral s m) =
let total = sum [m ! (i,i) + m ! (s-i-1, i) | i <- [0..s-1]]
in total-1 -- subtract 1 to undo counting the middle twice
main = print $ sumDiagonals $ spiral 1001
バングパターンは、アキュムレータアイテム(カーソル、数値、およびマップ)が最後まで使用されないことによるスタックオーバーフローを防ぎます。ほとんどのコードゴルフでは、入力ケースはこの規定を必要とするほど大きくないはずです。
glibの答えは次のとおりです。配列を使用しないでください。not-so-glibの答えは、配列を必要としないように問題を再考してみてください。
多くの場合、構造のような配列がまったくなくても、何らかの問題が発生する可能性があります。たとえば、オイラー28に対する私の答えは次のとおりです。
-- | What is the sum of both diagonals in a 1001 by 1001 spiral?
euler28 = spiralDiagonalSum 1001
spiralDiagonalSum n
| n < 0 || even n = error "spiralDiagonalSum needs a positive, odd number"
| otherwise = sum $ scanl (+) 1 $ concatMap (replicate 4) [2,4..n]
ここでコードで表現されるのは、数字が長方形のらせんの周りに成長するときの一連の数字のパターンです。数値のマトリックス自体を実際に表す必要はありませんでした。
配列を超えて考える鍵は、RAMのバイトとして表現する方法ではなく、問題が実際に何を意味するのかを考えることです。これには練習が必要です(おそらく、私がコードゴルフをたくさんする理由でしょう!)
もう1つの例は、最大パスのコードゴルフの発見をどのように解決したかです。そこでは、行列を行ごとに部分解を波として渡す方法は、フォールド演算によって直接表現されます。要確認:ほとんどのCPUでは、アレイ全体を一度に処理することはできません。プログラムは、時間の経過とともにアレイ全体を操作する必要があります。配列全体を一度に必要としない場合があります。
もちろん、本質的に配列ベースであるような方法でいくつかの問題が述べられています。> <>、Befunge、Brainfuckなどの言語には、配列が中心にあります。ただし、そこであっても、アレイは多くの場合不要です。たとえば、Brainfuckを解釈するための私のソリューションを参照してください、そのセマンティクスの本当の核心はzipperです。このように考え始めるには、アクセスのパターンと、問題の意味により近い構造に焦点を合わせます。多くの場合、これを可変配列に強制する必要はありません。
他のすべてが失敗し、配列を使用する必要がある場合-@Joeyのヒントは良い出発点です。