Memoization pascals triangle

 ̄綄美尐妖づ 提交于 2019-12-02 03:46:51
Will Ness

There are several guidelines to achieving memoization (take a look here for some recent discussion).

First, use -O2 optimization flag with GHC compiler. Second, use monomorphic type signature. Name your intermediate list(s), that you want to achieve sharing of.

Then, pay attention to your nested definitions. If a nested definition depends on value of an argument in its enclosing ("outer") scope, that means each call to that outer function will have to create all its nested definitions anew, so there won't be any one list to be shared, but many separate independent ones.

Here in your function, separating out and naming the list expression that you want shared, we get

memopascals r c = xs !! (r-1) !! (c-1) 
 where
   xs = [[pascals a b | b<-[1..c]] | a<-[1..r]] 
   pascals 1 _ = 1 
   pascals _ 1 = 1 
   pascals x y = memopascals (x-1) y + memopascals x (y-1)

Your xs definition is dependent on values of r and c, but you call your "outer" function, memopascals, from within a nested one, pascals. Each invocation of memopascals has to create its own copy of xs because it depends on memopascals's arguments, r and c. No sharing possible.

If you need to have that dependent definition, you must arrange not to call "out of scope", but rather stay inside that scope, so that all the internal ("nested") definitions can be reused.

memopascals r c = f r c
 where
   f r c = xs !! (r-1) !! (c-1)
   xs = [[pascals a b | b<-[1..c]] | a<-[1..r]] 
   pascals 1 _ = 1 
   pascals _ 1 = 1 
   pascals x y = f (x-1) y + f x (y-1)

Now each invocation of memopascals creates its internal definitions (from its nested scope) which then call each other, never calling out of scope - so the list xs gets reused, achieving sharing.

But another call to memopascals will create its own new copy of its internal definition of the list xs, and will use it. We can call this a "local" sharing, not a "global" sharing (i.e. memoization). That means it runs fast, but second call with same parameters takes the same time as the first - not the 0 time as with full memoization.

There's only one way to achieve that, and that is to make your xs definition independent. Then the compiler can smash all the nested scope frames together, perform lambda lifting turning nested closures into plain lambdas, etc. etc.:

memopascals :: Int -> Int -> Integer
memopascals r c = [[pascals a b | b<-[1..]] | a<-[1..]] !! (r-1) !! (c-1) 
 where 
   pascals 1 _ = 1 
   pascals _ 1 = 1 
   pascals x y = memopascals (x-1) y + memopascals x (y-1)

With -O2 switch, GHC performs full memoization even for this version. As long as we don't forget the monomorphic type signature (or it's local sharing again).

Memoization doesn't work in your functions because a call like memopascals 5 5 builds internally a part of the triangle and returns a single value from it. A different call mempascals 6 6 doesn't have any connection to that partial triangle internal to memopascals 5 5.

For efficient memoization you want the common part (like the list of lists that represents the calculated numbers in the triangle) in a separate function, which is then used by the functions that access specific indexes. This way you use the same list of lists to lookup different indexes.

Usually the easiest way to do this is to write one function like fullpascals :: [[Int]] that produces the complete (infinite) data structure, and than another function to access that data structure, like

memopascals x y = fullpascals !! (x-1) !! (y-1)

In your case fullpascals would be the same as one of your current functions, but without the parameters for a specific index. It can even still use memopascals internally for the recursion.


Sidenote: In the memoized_fib example in the wiki, the "common part" that is memoized isn't directly the list of all fibs, but a function that accesses the list of all fibs. The effect is the same, since the compiler is smart enough to optimize this.

zmemopascals r c =  zipWith pascals [1 ..] [1 ..] !! (r-1) !! (c-1) 
 where pascals 1 _ = 1 
       pascals _ 1 = 1 
       pascals x y = memopascals (x-1) y + memopascals x (y-1)

Not that it matters for the error, but in the last line, you want to call zmemopascals rather than memopascals.

In the first line, you have two list index operators. So zipWith pascals [1 .. ] [1 .. ] must have type [[a]] for some a. The definition of pascals says

pascals :: Num t => Int -> Int -> t

thus zipWith pascals [1 .. ] [1 .. ] :: [t]. Hence the type inference finds that t = [a], but the compiler doesn't find an instance Num [a].

For memoisation, you have to give a name to the full triangle, like @sth suggested, to avoid recomputation (the Fibonacci memoisation only works because the compiler is smart enough, it's not guaranteed by its form).

Another option would be to construct the triangle using iterate,

pascal :: Int -> Int -> Integer
pascal n k = iterate nextRow [1] !! n !! k
  where
    nextRow ks = zipWith (+) (0:ks) (ks ++ repeat 0)
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!