하스켈에서의 암기?


136

Haskell에서 다음 함수를 효율적으로 해결하는 방법에 대한 모든 포인터 (n > 108)

f(n) = max(n, f(n/2) + f(n/3) + f(n/4))

하스켈에서 피보나치 수를 풀기위한 메모리 화의 예를 보았습니다. 피보나치 수는 필요한 n까지 모든 피보나치 수를 계산하는 것이 었습니다. 그러나이 경우 주어진 n에 대해 중간 결과를 거의 계산하지 않아도됩니다.

감사


110
그것이 내가 집에서하고있는 일이라는 점에서만 의미가있다 :-)
Angel de Vicente

답변:


256

하위 선형 시간으로 색인을 생성 할 수있는 구조를 만들어서 매우 효율적으로 수행 할 수 있습니다.

하지만 먼저

{-# LANGUAGE BangPatterns #-}

import Data.Function (fix)

을 정의 f하되 직접 호출하는 대신 '오픈 재귀'를 사용하도록 합시다 .

f :: (Int -> Int) -> Int -> Int
f mf 0 = 0
f mf n = max n $ mf (n `div` 2) +
                 mf (n `div` 3) +
                 mf (n `div` 4)

당신은 f사용하여 메모리 가없는 얻을 수 있습니다fix f

이를 통해 다음 f과 같이 작은 값을 f호출하여 의미하는 바를 테스트 할 수 있습니다 .fix f 123 = 144

우리는 이것을 정의함으로써 이것을 기억할 수 있습니다 :

f_list :: [Int]
f_list = map (f faster_f) [0..]

faster_f :: Int -> Int
faster_f n = f_list !! n

그것은 잘 수행되며 O (n ^ 3) 시간이 걸리던 것을 중간 결과를 기억하는 것으로 대체합니다.

그러나에 대한 메모 된 답변을 찾으려면 색인을 작성하는 데 여전히 선형 시간이 걸립니다 mf. 이는 다음과 같은 결과를 의미합니다.

*Main Data.List> faster_f 123801
248604

견딜 만하지 만 결과는 그보다 훨씬 뛰어나지 않습니다. 우리는 더 잘할 수 있습니다!

먼저 무한 트리를 정의 해 봅시다 :

data Tree a = Tree (Tree a) a (Tree a)
instance Functor Tree where
    fmap f (Tree l m r) = Tree (fmap f l) (f m) (fmap f r)

우리가 인덱스 노드를 찾을 수 있도록 그리고 우리는 그것으로 인덱스 방법을 정의 할 수 있습니다 nO (로그 n)이 대신 시간 :

index :: Tree a -> Int -> a
index (Tree _ m _) 0 = m
index (Tree l _ r) n = case (n - 1) `divMod` 2 of
    (q,0) -> index l q
    (q,1) -> index r q

... 그리고 우리는 편리한 자연수로 가득 찬 나무를 찾을 수 있습니다. 그래서 우리는 그 지수로 주위를 둘러 볼 필요가 없습니다.

nats :: Tree Int
nats = go 0 1
    where
        go !n !s = Tree (go l s') n (go r s')
            where
                l = n + s
                r = l + s
                s' = s * 2

색인을 생성 할 수 있으므로 트리를 목록으로 변환하면됩니다.

toList :: Tree a -> [a]
toList as = map (index as) [0..]

당신이 toList nats제공 하는 것을 확인하여 지금까지 작업을 확인할 수 있습니다[0..]

지금,

f_tree :: Tree Int
f_tree = fmap (f fastest_f) nats

fastest_f :: Int -> Int
fastest_f = index f_tree

위의 목록과 동일하게 작동하지만 각 노드를 찾기 위해 선형 시간을 소비하는 대신 로그 시간으로 추적 할 수 있습니다.

결과는 훨씬 빠릅니다.

*Main> fastest_f 12380192300
67652175206

*Main> fastest_f 12793129379123
120695231674999

실제로 너무 빠르기 때문에 위의 내용을 대체 Int하고 Integer거의 즉각적으로 엄청나게 큰 답변을 얻을 수 있습니다.

*Main> fastest_f' 1230891823091823018203123
93721573993600178112200489

*Main> fastest_f' 12308918230918230182031231231293810923
11097012733777002208302545289166620866358

3
나는이 코드를 시도했지만 흥미롭게도 f_faster는 f보다 느리게 보였다. 그 목록 참조가 실제로 속도를 늦추는 것 같습니다. nats와 index의 정의는 나에게 매우 신비한 것처럼 보였으므로 일을 더 명확하게 할 수있는 내 자신의 대답을 추가했습니다.
Pitarou

5
무한리스트 케이스는 링크 된리스트 111111111 항목을 처리해야합니다. 트리 케이스는 log n * 도달 한 노드 수를 처리합니다.
Edward KMETT

2
즉, 목록 버전은 목록의 모든 노드에 대해 썽크를 작성해야하지만 트리 버전은 많은 노드를 작성하지 않습니다.
Tom Ellis

7
나는 이것이 오래된 게시물이라는 것을 알고 있지만 호출을 통해 트리에서 불필요한 경로를 저장하지 않도록 절 f_tree에서 정의 해서는 안 where됩니까?
dfeuer

17
CAF에 넣는 이유는 전화를 통해 메모를받을 수 있기 때문입니다. 내가 비싼 전화를 받았다면 내가 기억하고 있었을 것입니다. 아마도 CAF에 남겨 두었을 것입니다. 실제 응용 프로그램에서는 물론 영구 메모 작성의 이점과 비용 사이에 균형이 있습니다. 메모를 달성하는 방법에 대한 질문이 있었지만 전화를 통해 메모를 의도적으로 피하는 기술로 대답하는 것은 오해의 소지가 있다고 생각합니다. ;)
Edward KMETT

17

에드워드의 대답 은 내가 그것을 복제하고 개방형 재귀 형태로 함수를 기억하는 조합 memoList및 구현 memoTree자를 제공 한 훌륭한 보석입니다 .

{-# LANGUAGE BangPatterns #-}

import Data.Function (fix)

f :: (Integer -> Integer) -> Integer -> Integer
f mf 0 = 0
f mf n = max n $ mf (div n 2) +
                 mf (div n 3) +
                 mf (div n 4)


-- Memoizing using a list

-- The memoizing functionality depends on this being in eta reduced form!
memoList :: ((Integer -> Integer) -> Integer -> Integer) -> Integer -> Integer
memoList f = memoList_f
  where memoList_f = (memo !!) . fromInteger
        memo = map (f memoList_f) [0..]

faster_f :: Integer -> Integer
faster_f = memoList f


-- Memoizing using a tree

data Tree a = Tree (Tree a) a (Tree a)
instance Functor Tree where
    fmap f (Tree l m r) = Tree (fmap f l) (f m) (fmap f r)

index :: Tree a -> Integer -> a
index (Tree _ m _) 0 = m
index (Tree l _ r) n = case (n - 1) `divMod` 2 of
    (q,0) -> index l q
    (q,1) -> index r q

nats :: Tree Integer
nats = go 0 1
    where
        go !n !s = Tree (go l s') n (go r s')
            where
                l = n + s
                r = l + s
                s' = s * 2

toList :: Tree a -> [a]
toList as = map (index as) [0..]

-- The memoizing functionality depends on this being in eta reduced form!
memoTree :: ((Integer -> Integer) -> Integer -> Integer) -> Integer -> Integer
memoTree f = memoTree_f
  where memoTree_f = index memo
        memo = fmap (f memoTree_f) nats

fastest_f :: Integer -> Integer
fastest_f = memoTree f

12

가장 효율적인 방법은 아니지만 기억합니다.

f = 0 : [ g n | n <- [1..] ]
    where g n = max n $ f!!(n `div` 2) + f!!(n `div` 3) + f!!(n `div` 4)

를 요청할 때 존재 f !! 144하는지 확인 f !! 143하지만 정확한 값은 계산되지 않습니다. 여전히 계산 결과를 알 수없는 결과로 설정되어 있습니다. 정확한 값만 계산하면됩니다.

따라서 처음에는 계산 된 금액만큼 아무것도 알 수 없습니다.

f = .... 

요청하면 f !! 12패턴 일치를 시작합니다.

f = 0 : g 1 : g 2 : g 3 : g 4 : g 5 : g 6 : g 7 : g 8 : g 9 : g 10 : g 11 : g 12 : ...

이제 계산을 시작합니다

f !! 12 = g 12 = max 12 $ f!!6 + f!!4 + f!!3

이것은 재귀 적으로 f에 대한 또 다른 요구를하므로 우리는

f !! 6 = g 6 = max 6 $ f !! 3 + f !! 2 + f !! 1
f !! 3 = g 3 = max 3 $ f !! 1 + f !! 1 + f !! 0
f !! 1 = g 1 = max 1 $ f !! 0 + f !! 0 + f !! 0
f !! 0 = 0

이제 우리는 몇 가지를 흘릴 수 있습니다

f !! 1 = g 1 = max 1 $ 0 + 0 + 0 = 1

이는 프로그램이 이제 다음을 알고 있음을 의미합니다.

f = 0 : 1 : g 2 : g 3 : g 4 : g 5 : g 6 : g 7 : g 8 : g 9 : g 10 : g 11 : g 12 : ...

계속해서 간질 :

f !! 3 = g 3 = max 3 $ 1 + 1 + 0 = 3

이는 프로그램이 이제 다음을 알고 있음을 의미합니다.

f = 0 : 1 : g 2 : 3 : g 4 : g 5 : g 6 : g 7 : g 8 : g 9 : g 10 : g 11 : g 12 : ...

이제 다음 계산을 계속합니다 f!!6.

f !! 6 = g 6 = max 6 $ 3 + f !! 2 + 1
f !! 2 = g 2 = max 2 $ f !! 1 + f !! 0 + f !! 0 = max 2 $ 1 + 0 + 0 = 2
f !! 6 = g 6 = max 6 $ 3 + 2 + 1 = 6

이는 프로그램이 이제 다음을 알고 있음을 의미합니다.

f = 0 : 1 : 2 : 3 : g 4 : g 5 : 6 : g 7 : g 8 : g 9 : g 10 : g 11 : g 12 : ...

이제 다음 계산을 계속합니다 f!!12.

f !! 12 = g 12 = max 12 $ 6 + f!!4 + 3
f !! 4 = g 4 = max 4 $ f !! 2 + f !! 1 + f !! 1 = max 4 $ 2 + 1 + 1 = 4
f !! 12 = g 12 = max 12 $ 6 + 4 + 3 = 13

이는 프로그램이 이제 다음을 알고 있음을 의미합니다.

f = 0 : 1 : 2 : 3 : 4 : g 5 : 6 : g 7 : g 8 : g 9 : g 10 : g 11 : 13 : ...

따라서 계산은 상당히 게으 릅니다. 이 프로그램은의 가치가 f !! 8존재 한다는 것과 그 가치 가 같다는 것을 알고 g 8있지만, 그것이 무엇인지 전혀 모릅니다 g 8.


이것에 감사합니다. 2 차원 솔루션 공간을 어떻게 만들고 사용 하시겠습니까? 그것은 목록의 목록일까요? 와g n m = (something with) f!!a!!b
vikingsteve

1
물론 이죠 진정한 솔루션을하지만, 난 아마 메모이 제이션의 같은 라이브러리를 사용하십시오 memocombinators
rampion

불행히도 O (n ^ 2)입니다.
Qumeric

8

이것은 Edward Kmett의 훌륭한 답변에 대한 부록입니다.

나는 그의 코드를 시도 할 때의 정의 nats와는 index내가 이해하기 쉽게 발견 다른 버전을 쓰기 때문에, 꽤 신비 보였다.

나는 정의 indexnats측면에서 index'nats'.

index' t n범위에 걸쳐 정의됩니다 [1..]. ( index t범위에 대해 정의되어 있음을 기억 하십시오 [0..].) n비트 열로 취급 하고 비트를 역순으로 읽어 트리를 검색합니다 . 비트가 1인 경우 오른쪽 분기를 사용합니다. 비트가 0인 경우 왼쪽 분기를 사용합니다. 마지막 비트 (이어야 함 1)에 도달하면 중지됩니다 .

index' (Tree l m r) 1 = m
index' (Tree l m r) n = case n `divMod` 2 of
                          (n', 0) -> index' l n'
                          (n', 1) -> index' r n'

마찬가지로 nats정의되어 index그 때문에 index nats n == n항상 사실, nats'정의됩니다 index'.

nats' = Tree l 1 r
  where
    l = fmap (\n -> n*2)     nats'
    r = fmap (\n -> n*2 + 1) nats'
    nats' = Tree l 1 r

자, nats그리고 index단순히 nats'하고 index'있지만, 값을 1로 이동 :

index t n = index' t (n+1)
nats = fmap (\n -> n-1) nats'

감사. 나는 다변량 함수를 기억하고 있는데, 이것은 실제로 인덱스와 nats가 실제로하고있는 것을 해결하는 데 도움이되었습니다.
Kittsil

8

Edward Kmett의 답변에서 알 수 있듯이 작업 속도를 높이려면 값 비싼 계산을 캐시하고 빠르게 액세스 할 수 있어야합니다.

함수를 비 모노 딕 방식으로 유지하기 위해 무한 게으른 트리를 작성하는 방법은 (이전 게시물에서 볼 수 있듯이) 적절한 색인 방법으로 해당 목표를 달성합니다. 비 모나 딕 함수의 기능을 포기하면 Haskell에서 사용 가능한 표준 연관 컨테이너를 "상태 유사"모나드 (상태 또는 ST와 같은)와 함께 사용할 수 있습니다.

주요 단점은 비 모노 함수를 얻는다는 것인데, 더 이상 구조를 직접 색인화 할 필요가 없으며 표준 연관 컨테이너 구현을 사용할 수 있습니다.

그렇게하려면 먼저 모든 종류의 모나드를 허용하는 함수를 다시 작성해야합니다.

fm :: (Integral a, Monad m) => (a -> m a) -> a -> m a
fm _    0 = return 0
fm recf n = do
   recs <- mapM recf $ div n <$> [2, 3, 4]
   return $ max n (sum recs)

테스트를 위해 Data.Function.fix를 사용하여 메모를 작성하지 않는 함수를 정의 할 수 있지만 조금 더 장황합니다.

noMemoF :: (Integral n) => n -> n
noMemoF = runIdentity . fix fm

그런 다음 State monad를 Data.Map과 함께 사용하여 작업 속도를 높일 수 있습니다.

import qualified Data.Map.Strict as MS

withMemoStMap :: (Integral n) => n -> n
withMemoStMap n = evalState (fm recF n) MS.empty
   where
      recF i = do
         v <- MS.lookup i <$> get
         case v of
            Just v' -> return v' 
            Nothing -> do
               v' <- fm recF i
               modify $ MS.insert i v'
               return v'

약간만 변경하면 코드를 Data.HashMap과 함께 작동하도록 조정할 수 있습니다.

import qualified Data.HashMap.Strict as HMS

withMemoStHMap :: (Integral n, Hashable n) => n -> n
withMemoStHMap n = evalState (fm recF n) HMS.empty
   where
      recF i = do
         v <- HMS.lookup i <$> get
         case v of
            Just v' -> return v' 
            Nothing -> do
               v' <- fm recF i
               modify $ HMS.insert i v'
               return v'

영구 데이터 구조 대신 ST 모나드와 함께 가변 데이터 구조 (예 : Data.HashTable)를 시도 할 수도 있습니다.

import qualified Data.HashTable.ST.Linear as MHM

withMemoMutMap :: (Integral n, Hashable n) => n -> n
withMemoMutMap n = runST $
   do ht <- MHM.new
      recF ht n
   where
      recF ht i = do
         k <- MHM.lookup ht i
         case k of
            Just k' -> return k'
            Nothing -> do 
               k' <- fm (recF ht) i
               MHM.insert ht i k'
               return k'

메모를 사용하지 않는 구현과 비교할 때 이러한 구현을 사용하면 큰 입력의 경우 몇 초 동안 기다리지 않고 마이크로 초 단위로 결과를 얻을 수 있습니다.

Criterion을 벤치 마크로 사용하면 Data.HashMap을 사용한 구현이 실제로 타이밍이 매우 유사한 Data.Map 및 Data.HashTable보다 약간 더 나은 성능 (약 20 %)을 관찰 할 수 있습니다.

벤치 마크 결과가 약간 놀랍습니다. 필자의 초기 느낌은 HashTable이 변경 가능하기 때문에 HashMap 구현을 능가한다는 것입니다. 이 마지막 구현에서 일부 성능 결함이 숨겨져있을 수 있습니다.


2
GHC는 불변 구조를 최적화하는 데 매우 효과적입니다. C의 직감이 항상 펼쳐지는 것은 아닙니다.
John Tyree

3

몇 년 후, 나는 이것을보고 선형 zipWith함수와 도우미 함수를 사용하여 이것을 선형으로 기억하는 간단한 방법이 있음을 깨달았습니다 .

dilate :: Int -> [x] -> [x]
dilate n xs = replicate n =<< xs

dilate편리한 속성을 가지고 dilate n xs !! i == xs !! div i n있습니다.

따라서 f (0)이 주어 졌다고 가정하면 계산이 간단 해집니다.

fs = f0 : zipWith max [1..] (tail $ fs#/2 .+. fs#/3 .+. fs#/4)
  where (.+.) = zipWith (+)
        infixl 6 .+.
        (#/) = flip dilate
        infixl 7 #/

원래의 문제 설명과 비슷하게 보이며 선형 솔루션을 제공합니다 ( sum $ take n fsO (n)이 필요함).


2
따라서 생성 (코어 커런트?) 또는 동적 프로그래밍 솔루션입니다. 일반적인 피보나치처럼 각 생성 된 값마다 O (1) 시간이 걸립니다. 큰! EKMETT의 솔루션은 대수 피보나치처럼 대수를 훨씬 빠르게 건너 뛰어 많은 수의 정보를 건너 뜁니다. 이게 맞습니까?
Will Ness

또는 3 개의 백 포인터가 생성되는 순서에 따라 해밍 번호에 대한 수치에 더 가깝거나 각각의 속도가 진행됩니다. 너무 예뻐요.
Will Ness

2

Edward Kmett의 답변에 대한 또 다른 부록 : 자체 포함 된 예 :

data NatTrie v = NatTrie (NatTrie v) v (NatTrie v)

memo1 arg_to_index index_to_arg f = (\n -> index nats (arg_to_index n))
  where nats = go 0 1
        go i s = NatTrie (go (i+s) s') (f (index_to_arg i)) (go (i+s') s')
          where s' = 2*s
        index (NatTrie l v r) i
          | i <  0    = f (index_to_arg i)
          | i == 0    = v
          | otherwise = case (i-1) `divMod` 2 of
             (i',0) -> index l i'
             (i',1) -> index r i'

memoNat = memo1 id id 

단일 정수 arg (예 : 피보나치)로 함수를 메모하려면 다음과 같이 사용하십시오.

fib = memoNat f
  where f 0 = 0
        f 1 = 1
        f n = fib (n-1) + fib (n-2)

음이 아닌 인수의 값만 캐시됩니다.

음수 인수의 값을 캐시하려면 memoInt다음과 같이 정의 된을 사용하십시오 .

memoInt = memo1 arg_to_index index_to_arg
  where arg_to_index n
         | n < 0     = -2*n
         | otherwise =  2*n + 1
        index_to_arg i = case i `divMod` 2 of
           (n,0) -> -n
           (n,1) ->  n

두 개의 정수 인수가있는 함수의 값을 캐시하려면 memoIntInt다음과 같이 정의 된를 사용하십시오 .

memoIntInt f = memoInt (\n -> memoInt (f n))

2

색인이없고 Edward KMETT를 기반으로하지 않는 솔루션입니다.

공통의 부모에 공통 하위 트리 밖으로 I 계수 ( f(n/4)간에 공유 f(n/2)하고 f(n/4), 그리고 f(n/6)사이에 공유 f(2)하고 f(3)). 부모에서 단일 변수로 저장하면 하위 트리 계산이 한 번 수행됩니다.

data Tree a =
  Node {datum :: a, child2 :: Tree a, child3 :: Tree a}

f :: Int -> Int
f n = datum root
  where root = f' n Nothing Nothing


-- Pass in the arg
  -- and this node's lifted children (if any).
f' :: Integral a => a -> Maybe (Tree a) -> Maybe (Tree a)-> a
f' 0 _ _ = leaf
    where leaf = Node 0 leaf leaf
f' n m2 m3 = Node d c2 c3
  where
    d = if n < 12 then n
            else max n (d2 + d3 + d4)
    [n2,n3,n4,n6] = map (n `div`) [2,3,4,6]
    [d2,d3,d4,d6] = map datum [c2,c3,c4,c6]
    c2 = case m2 of    -- Check for a passed-in subtree before recursing.
      Just c2' -> c2'
      Nothing -> f' n2 Nothing (Just c6)
    c3 = case m3 of
      Just c3' -> c3'
      Nothing -> f' n3 (Just c6) Nothing
    c4 = child2 c2
    c6 = f' n6 Nothing Nothing

    main =
      print (f 123801)
      -- Should print 248604.

코드는 일반적인 메모 기능으로 쉽게 확장되지 않으며 (적어도 어떻게 해야할지 모르겠습니다) 하위 문제가 어떻게 겹치는 지 실제로 생각해야하지만 전략 해야 은 정수가 아닌 일반적인 여러 정수 매개 변수에 대해 작동해야합니다 . (두 개의 문자열 매개 변수에 대해 생각했습니다.)

메모는 각 계산 후에 삭제됩니다. (다시 말해서 두 개의 문자열 매개 변수에 대해 생각하고있었습니다.)

이것이 다른 답변보다 더 효율적인지 모르겠습니다. 각 조회는 기술적으로 한두 단계 ( "자녀 또는 자녀의 아이"를 봅니다)이지만 많은 추가 메모리 사용이있을 수 있습니다.

편집 :이 솔루션은 아직 올바르지 않습니다. 공유가 불완전합니다.

편집 : 그것은 제대로 지금 subchildren를 공유해야하지만, 나는이 문제가 사소 공유 많이 가지고 있음을 깨달았다 n/2/2/2n/3/3같은 수 있습니다. 문제는 내 전략에 적합하지 않습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.