通常,一项任务需要真实的数组。以实现Befunge或> <>的任务为例。我试图为此使用Array
模块,但这确实很麻烦,因为感觉我的编码太冗长了。有人可以帮我解决冗长,功能更多的代码高尔夫任务吗?
通常,一项任务需要真实的数组。以实现Befunge或> <>的任务为例。我试图为此使用Array
模块,但这确实很麻烦,因为感觉我的编码太冗长了。有人可以帮我解决冗长,功能更多的代码高尔夫任务吗?
Answers:
首先,我建议您看一下Data.Vector,它在某些情况下可以更好地替代Data.Array。
Array
并且Vector
非常适合一些memoization的情况下,如证明我的回答“寻找最大的路径”。但是,有些问题很难用功能样式表达。例如,问题28在项目欧拉要求对螺旋的对角线的数目求和。当然,找到这些数字的公式应该很容易,但是构造螺旋线更具挑战性。
Data.Array.ST提供了可变的数组类型。但是,这种类型的情况很混乱:它使用类MArray重载其除runSTArray之外的每个方法。因此,除非计划从可变数组操作返回不可变数组,否则必须添加一个或多个类型签名:
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 :: Ord k => k -> a -> Map k a -> Map k a
因为Map
在内部实现为平衡的二叉树,所以insert
只占用O(log n)的时间和空间,并保留原始副本。因此,Map
不仅提供了与功能编程模型兼容的效率较高的“可变数组”,而且还可以让您“时光倒流”。
这是使用Data.Map的Euler 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的答案是:不要使用数组。不太高兴的答案是:尝试重新考虑您的问题,使其不需要数组。
通常,完全不需要任何类似数组的结构就可以解决某些问题。例如,这是我对欧拉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中的字节。这只是实践而已(也许是我这么多打高尔夫的原因!)
另一个示例是我如何解决找到最大路径的问题。在这里,通过折叠操作直接表示将部分解作为波逐行通过矩阵的方法。切记:在大多数CPU上,您无法一次处理整个阵列:随着时间的推移,程序将不得不对其进行操作。它可能不需要随时一次使用整个数组。
当然,某些问题以其固有地基于数组的方式陈述。诸如> <>,Befunge或Brainfuck之类的语言具有其核心。但是,即使在那里,也常常可以省去阵列。例如,请参阅我的解释Brainfuck的解决方案,其语义的真正核心是拉链。要开始以这种方式思考,请关注访问模式以及与问题含义更接近的结构。通常不需要将其强制为可变数组。
当所有其他方法都失败时,您确实需要使用数组-@Joey的技巧是一个好的开始。