用Haskell处理需要数组的任务的好方法是什么?


11

通常,一项任务需要真实的数组。以实现Befunge或> <>的任务为例。我试图为此使用Array模块,但这确实很麻烦,因为感觉我的编码太冗长了。有人可以帮我解决冗长,功能更多的代码高尔夫任务吗?


AFAIK,此网站仅适用于代码高尔夫本身,不适用于相关问题。我猜想这属于Stackoverflow。
Jesse Millikan

@Jesse Millikan:也请参阅问题并阅读常见问题解答。它没有说明,您不允许问有关打高尔夫球的任何问题。在本网站的定义阶段,此类问题也是“主题”问题的重要组成部分。如果您理解我在这里提出这个问题的原因,请三思而后行将其删除。
FUZxxl 2011年

嗯,我猜不好。
Jesse Millikan

@杰西·米利坎(Jesse Millikan):人文的评价
FUZxxl 2011年

常见问题解答不是很清楚。
Jesse Millikan

Answers:


5

首先,我建议您看一下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作为“可变数组”

如果要实现可变数组算法,则另一个选择是使用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

爆炸模式可防止直到最后才使用累加器项(光标,数字和映射)而导致堆栈溢出。对于大多数标准高尔夫,输入箱的大小应不足以需要此规定。


9

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的技巧是一个好的开始。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.