종종 작업에는 실제 배열이 필요합니다. Befunge 또는> <>을 구현하는 작업을 예로 들어 보겠습니다. 나는 Array
이것을 위해 모듈 을 사용하려고 시도했지만 너무 장황하게 코딩하는 것처럼 느껴지므로 실제로 성가시다. 아무도 그러한 코드 골프 작업을 덜 장황하고 더 기능적으로 해결하는 방법을 도와 줄 수 있습니까?
종종 작업에는 실제 배열이 필요합니다. Befunge 또는> <>을 구현하는 작업을 예로 들어 보겠습니다. 나는 Array
이것을 위해 모듈 을 사용하려고 시도했지만 너무 장황하게 코딩하는 것처럼 느껴지므로 실제로 성가시다. 아무도 그러한 코드 골프 작업을 덜 장황하고 더 기능적으로 해결하는 방법을 도와 줄 수 있습니까?
답변:
우선, 경우에 따라 Data.Array 의 더 좋은 대안 인 Data.Vector를 보는 것이 좋습니다 .
Array
과 Vector
에서 입증 된 바와 같이, 일부 메모이 제이션의 경우에 적합하다 "찾기 최대 경로"에 대한 내 대답 . 그러나 일부 문제는 기능적 스타일로 표현하기가 쉽지 않습니다. 예를 들어, 문제 (28) 의 프로젝트 오일러는 나선형의 대각선의 수를 합산 요구한다. 물론,이 숫자에 대한 공식을 찾기는 쉽지만 나선을 구성하는 것이 더 어렵습니다.
Data.Array.ST 는 가변 배열 유형을 제공합니다. 그러나 유형 상황은 엉망입니다 .runSTArray 를 제외하고 MArray 클래스를 사용하여 메소드 중 하나를 모두 과부하 시킵니다 . 따라서 변경 가능한 배열 작업에서 변경 불가능한 배열을 반환하려는 경우가 아니라면 하나 이상의 유형 서명을 추가해야합니다.
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과 같은 언어는 그 중심에 배열이 있습니다. 그러나 거기에서도 어레이를 생략 할 수 있습니다. 예를 들어, 내 솔루션을 참조 브레인 퍽 해석은 , 그 의미의 진정한 핵심은입니다 지퍼 . 이런 식으로 생각하기 시작하면 접근 패턴과 문제의 의미에 더 가까운 구조에 중점을 둡니다. 이것은 종종 가변 배열로 강제 할 필요가 없습니다.
다른 모든 것이 실패하고 배열을 사용해야하는 경우 @Joey의 팁이 좋습니다.