Dynamic Programming in Haskell and why DP is useful

I’m continuing to learn Haskell in my free time and as I like to learn by doing, I’ve been implementing common algorithms in the language. Last week I tackled depth first search and found it to be surprisingly challenging and consequently really educational. Today, I decided to try my hand at dynamic programming. I chose dynamic programming because these algorithms hinge on memoization, and I wanted to figure out how to do that in a purely functional way. Also, I thought that I could do some performance analysis, as implementing recursive, un-memoized algorithms to solve these problems seemed to be quite easy.

The first algorithm I tried to implement was the standard edit distance algorithm. It’s a classic DP algorithm and although no longer used much in practice, it is the basis for several modern techniques used in sequence alignment, so I figured it would be an interesting exercise. I spent an hour or so thinking about how to approach this and even started programming a solution, but I quickly got bogged down in maintaining a matrix of subproblem solutions. I found implementing all of these matrix operations to be quite mundane and decided to start with something a bit less tedious.

So I chose another classic DP problem, Longest Increasing Subsequence. Briefly, the problem formulation is: Given a list of (lets say) positive, unique integers, find the length of the longest subsequence such that the subsequence is strictly increasing. For example, given [1, 4, 2, 3, 9], the longest increasing subsequence would be [1, 2, 3, 9]. Read the wikipedia article for more details. I chose this problem because you don’t have to maintain a matrix of subproblem solutions, but rather a list, and the problem is quite a bit simpler, allowing me to focus on the algorithmic elements.

So, I started out by implementing a naive un-memoized algorithm, then went on to implement the dynamic programming algorithm that’s based on the one on the wikipedia page, but in Haskell it doesn’t really look the same at all. I implemented both so that I could do run some performance metrics which I’ll discuss about after presenting my code. First some auxiliary stuff:

[code lang="lisp"]
-- return the item in the list indexed by i --
listGet :: [Int] -> Int -> Int
listGet lst i =
if i == 0
then (head lst)
else
listGet (tail lst) (i-1)

-- return the index of item i in the list --
getIndex :: [Int] -> Int -> Int
getIndex lst i =
if (head lst) == i
then 0
else
1 + (getIndex (tail lst) i)

-- given a list of integers, return the largest value in the list --
listMax :: [Int] -> Int
listMax [] = 0
listMax lst =
if (length lst)==1
then (head lst)
else max (head lst) (listMax (tail lst))
[/code]

I admit I should have used arrays for this but I kind of started using lists before having to implement these and was too lazy to change it. I would change it now but I’m worried about breaking things right before posting and since I’m probably (read definitely) never going to look at this again it really doesn’t matter to me.

Anyway, so we have three functions that are all pretty straightforward. One essentially treats a list as an array and gets the i-th element from the array. Another does the reverse, it gets the index of an item in the array. Finally the third computes the max over a list.

My first algorithm is a naive approach that is just recursive with no memoization:

[code lang="lisp"]
-- compute the longest increasing subsequence starting from each index in the list --
longestIncreasingSubsequence :: [Int] -> Int -> [Int]
longestIncreasingSubsequence [] _ = []
longestIncreasingSubsequence lst i =
if (length lst) == i+1
then [1]
else
[(1+(listMax
(map (\x -> head (longestIncreasingSubsequence lst (getIndex lst x)))
(filter (\x -> (listGet lst i) < x)
(filter (\x -> (getIndex lst x) > i) lst)))))]
++ (longestIncreasingSubsequence lst (i+1))

-- compute the longest increasing subsequence of the list --
lis2 :: [Int] -> Int
lis2 lst =
listMax (longestIncreasingSubsequence lst 0)
[/code]

Ok so longestIncreasingSubsequence is a bit confusing and the comment is kind of inaccurate. If you call it with i == 0, then it’ll return a list where each j-th element is the value of the longest increasing subsequence that starts from and contains the jth item of lst. It basically computes the solution to all of the subproblems, but it doesn’t compute them by memoization. It’s similar if you start with i != 0 but the list only has a suffix of the subproblems. It computes these results by following the standard recursive formula: look at each j>i such that l[i]

This was a good exercise but not really what I wanted to do. So I tested that a bit and made sure it was correct, and proceeded to think about the dynamic programming algorithm. I found it a bit difficult to actually memoize the solutions, but here's what I came up with:

[code lang="lisp"]
-- given an array of subproblem, find the largest increasing subsequence starting at i --
dplis :: [Int] -> Int -> Array Int Int -> Int
dplis lst int arr =
listMax
((map (\x -> 1 + arr ! (getIndex lst x))
(filter (\x -> (listGet lst int) < x)
(filter (\x -> (getIndex lst x) > int) lst))) ++ [1])

– compute largest increasing subsequence starting at int –
getDPScores :: [Int] -> Int -> Array Int Int
getDPScores lst int =
if (length lst) == int+1
then array (0, ((length lst)-1)) ([(i, 0) | i <- [0..((length lst)-2)]]++[(((length lst)-1), 1)])
else
let arr = getDPScores lst (int+1) in
arr // [(int, dplis lst int arr)]

– compute largest increasing subsequence of list –
lis :: [Int] -> Int
lis lst =
listMax (elems (getDPScores lst 0))
[/code]

So I hope the functions make sense. dplis just does lookups in the array parameter to compute the longest increasing subsequence starting at and including i. getDPScores builds the array of subproblem solutions, starting from the smallest subproblems and relying on dplis. Finally lis is just a wrapper for calling getDPScores with the right parameters.

One noteworthy thing is the flow of the program. It's tail recursive. So the first thing we do is get to the bottom of the recursion of getDPScores, then we really do our computation as we work our way out of that recursion stack. Each call to getDPScores after returning from recursion still has to call dplis. Of course there are a lot of tail recursive algorithm, but I didn't really expect dynamic programming to naturally flow like this. Typically dynamic programming is done iteratively, but I suppose we can't do that here.

So those are my two implementation, now on to performance stuff. I decided to experiment with this just to see how amazing dynamic programming really is. If you notice the first algorithm I implemented is definitely exponential (in the worst case, imagine a ascending list of length n, then each call has to spawn O(n) smaller calls). I think it's O(n^n) but I haven't thought too much about it so please correct me if I'm wrong. Anyway, the second algorithm is O(n^2), you just recurse straight down and pop all the way back up. At each recursive call you may have to look at O(n) neighbors and there are only n recursive calls.

Given the asymptotic running times, I expect the second algorithm to be far superior to the first in actual running time. Just for fun I decided to run this experiment and plot my results. I generated random lists of sizes ranging from 5 to 70 in increments of 5 and ran each algorithm on the same lists. Here are the results:

exponential vs quadratic times

exponential vs quadratic times

They are pretty much what I expected. Notice that the y axis is log-scaled, so that for input length 70, the exponential algorithm took around 10 minutes (600 seconds) where the dynamic programming one takes well under a second. Really amazing how dynamic programming helps that much. I had originally intended to run on larger input sizes but I simply couldn't as the exponential algorithm wouldn't terminate.

As I did in my last Haskell post, I did a quick search for other people who had looked at dynamic programming in Haskell. I found this article which walks through an implementation of the knapsack algorithm and it appears to be much more elegant than mine. Of course you should remember that this I'm still a Haskell newbie so I don't have mastery over most of the features and I really don't expect myself to be able to implement elegant solutions. However, he does mention that dynamic programming is quite natural in functional languages and that you can take advantage of lazy evaluation, which I can understand. Also the knapsack algorithm uses a matrix of subproblems, so it's slightly different and more interesting.

I also found this article that implements the Needleman/Wunsch algorithm for global sequence alignment, another classic dynamic programming algorithm. This was also an interesting read, and he used some language features that I haven't used and barely knew existed so it was definitely educational. Also, this algorithm also uses a matrix of subproblem solutions so I'm thinking that my next experiment will be with more one of these dynamic programming algorithms.

So, In conclusion, I'm feeling more confident in my functional thinking and my Haskell programming. In my previous post, Jedai commented that I shouldn't consider arrays as cheating, and so I have adopted them into my repertoire here. Further, as proof of the importance of algorithm design and asymptotic efficiency, I generated some performance numbers to show how essential it is to have efficient algorithms.

Anyway... there's a leak in my ceiling and water is dripping all over the place so, until next time!

Tags:

8 Responses to “Dynamic Programming in Haskell and why DP is useful”

  1. adrian Says:

    import Data.Array

    type Matrix = Array (Int,Int) Int

    leMatrix :: String -> String -> Matrix
    leMatrix s1 s2= let (ls1,ls2)=(length s1, length s2) in array ((0,0), (ls1, ls2)) $
    [((x,0),x)|x<-[0.. ls1]] ++ [((0,x),x)|x<-[1.. ls2]] ++ — Initialisierung
    [((i,j), minimum [(leMatrix s1 s2)!(i-1,j-1)+ d i j s1 s2, -- Rekursion
    (leMatrix s1 s2)!(i-1,j)+1,
    (leMatrix s1 s2)!(i,j-1)+1]) | i<-[1.. ls1], j Int -> String -> String -> Int
    d i j s1 s2
    | s1!!(i-1) == s2!!(j-1) = 0
    | otherwise = 1

  2. Phil Says:

    I recently solved the longest common subsequence problem at http://programmingpraxis.com/2009/06/09/longest-common-subsequence/. One of the comments provided a Haskell solution.

  3. akrish » Blog Archive » Dynamic Programming in Haskell and why DP … Says:

    [...] original here:  akrish » Blog Archive » Dynamic Programming in Haskell and why DP … SHARETHIS.addEntry({ title: “akrish » Blog Archive » Dynamic Programming in Haskell and why DP [...]

  4. Some Internet Guy Says:

    You can make your code more concise by using pattern matching and guards. Consider this alternative version of your getIndex function (formatting might be eaten by the blog):

    getIndex (x:xs) i
    | x == i = 0
    | otherwise = 1 + getIndex xs i

    You can avoid calling head and tail by simply breaking your list apart in the pattern. The ‘|’ things are guards, which simply evaluate some boolean expression to decide which function body will be used.

    For even more concision, you can call Data.List.elemIndex instead of using your getIndex function.

  5. Some Internet Guy Says:

    I neglected to provide an empty list case for the alternative getIndex I sketched. Your original version contains the same flaw as mine.

    getIndex [] i = error “probably this function should return Maybe Int instead of crashing”

    If an empty list were passed to your getIndex, then the call to head would crash. If your version doesn’t find the desired item, then it will recurse until the empty list case happens, so you still get a crash.

    Functions which are not defined for all inputs are called partial functions and they can ruin your day. head is a partial function because it just crashes (prints an error and aborts the program) if you pass it an empty list. You can make a partial function a total function by returning a Maybe type, and using Maybe’s Nothing value to signal failure.

    For example, Data.list.elemIndex returns Maybe Int, such that you get Just 5 if your item is found at index 5, or Nothing if it is not found at all.

  6. Jedaï Says:

    lis :: (Ord a) => [a] -> [a]
    lis = snd . maximum . foldr go []
      where
        go x xxs = foldl’ (best x) (1,[x]) xxs : xxs
        best x cur@(len,_) (len’, yys@(y:ys)) = if x < y && len <= len’ then (len’+1, x:yys) else cur

    This seems to do the trick.

    A remark on your style : you should probably take some time to explore the prelude and Data.List (listGet = (!!), getIndex = elemIndex, listMax = maximum). You also use length too much : length is O(n) and you shouldn’t need it very much (for instance using length in your listMax was completely useless). Instead you should try to use pattern matching more, and guards are good too, though an if…then…else here and there isn’t too bad (as long you don’t start to nest them).

  7. akshayk Says:

    @adrian – I’m not entirely sure what your program is doing but it looks like a 2-d dynamic programming. However, I think it quite inefficient (i.e. exponential) because of all the calls to leMatrix
    @phil – it’s a different problem but great. i liked your post and the haskell solution looked good to me.
    @internetguy – yeah I know about pattern matching, just haven’t gotten used to using it. I didn’t know about guards but thanks for the tip. I will try to use both of them in the future. Yeah I knew about the empty list problem but the way that I’m calling it will never trigger that so it’s a bad practice but ok in this situation. You’re right about using Maybe though.
    @Jedai – yes I do need to explore the prelude. It’s one of the things that I’m lazy to do whenever i learn a new language but it’s quite important. Yeah I will try and incorporate pattern matching and guards into my programming.

    thanks for the comments everyone

  8. Jedaï Says:

    > yes I do need to explore the prelude. It’s one of the things that
    > I’m lazy to do whenever i learn a new language but it’s quite important.

    In the case of Haskell, exploring the Prelude is both important and interesting because most of this Prelude is written in Haskell (in fact all of it but for some primitives for additions, equalities and so on). It isn’t as dry and boring than reading a list of functions with their type and description, you also have some quite nice code. Reading the Prelude is as much a didactic experience that it is a necessity to write code without reinventing the wheel.

    http://www.haskell.org/onlinereport/standard-prelude.html

    (not all bits are as interesting, the numeric instances part bore me to tears myself)
    The current Prelude code in GHC contains some subtle difference and Data.List and others bring some other functions but the base of Haskell is still the Prelude.

Leave a Reply