Stack overflow in my haskell code

618 views Asked by At

I wanna find all combinations for coin change. 1, 2, 5, 10, 20, 50, 100 und 200. ( 1 cent , 2cent ..) if coin is over 500 (5 euro) , it should give -1.My code works perfectly with those testcases: numOfSplits 10 (11) numOfSplits 20 (41) numOfSplits 100 (4563) . When i try with test cases numOfSplits 200 or 500 , it will gives C stack overflow error. How can i make better my code ?

numOfSplits :: Integer -> Integer  
numOfSplits a  
    | (abs a) > 500 = -1  
    | (abs a) == 0 = 0  
    | otherwise = intzahler (makeChange [200,100,50,20,10,5,2,1] (abs a) 200)  

intzahler :: [[Integer]] -> Integer  
intzahler array  
    | array == [] = 0  
    | otherwise = 1 + intzahler (tail array)  

makeChange :: [Integer] -> Integer -> Integer -> [[Integer]]  
makeChange coins amount maxCoins  
    | amount < 0  = []  
    | amount == 0 = [[]]  
    | null coins  = []  
    | amount `div` maximum coins > maxCoins = [] -- optimisation  
    | amount > 0  =  
        do x <- coins  
           xs <- makeChange (filter (<= x) coins)  
                            (amount - x)  
                            (maxCoins - 1)  
           guard (genericLength (x:xs) <= maxCoins)  
           return (x:xs)

I changed my code to this code and i got no longer stack overflow error but now my code works so slowly . Example : for numOfSplits 500 , it makes longer than 30 minutes how can i make that quicklier?

     numOfSplits :: Integer -> Integer  
numOfSplits a
    | (abs a) > 500 = -1
    | (abs a) == 0 = 0
    | otherwise = fromIntegral . length $ makeChange [200,100,50,20,10,5,2,1] (abs a) 

makeChange :: [Integer] -> Integer ->  [[Integer]]
makeChange coins amount 
   | amount < 0  = []
   | amount == 0 = [[]]
   | null coins  = []
   | amount > 0  =
        do x <- coins
          xs <- makeChange (filter (<= x) coins) (amount - x)
           return (x:xs)
2

There are 2 answers

0
Cirdec On BEST ANSWER

Solving this problem quickly, and thus avoiding exhausting the resources of the computer, such as the stack, requires that you reuse the partial answers that were previously computed.

Let's pretend we wanted to solve a similar problem where we seek to find out how many ways we can make 15 cents using only 1, 2, or 5 cent coins. We have two concerns we will tackle - the first is solving the problem correctly. The second is solving the problem quickly.

Solving the problem correctly

To solve the problem correctly, we need to avoid re-counting combinations of coins that we have already counted. For example, we can make 15 cents by:

  • 2 five cent coins, 5 one cent coin
  • 1 five cent coin, 5 one cent coins, 1 five cent coin
  • 2 one cent coins, 1 five cent coin, 2 one cent coins, 1 five cent coins, 1 one cent coin

All of the above examples use the same combination of coins. They all use 2 five cent coins and 5 one cent coins, counted out in different orders.

We can avoid the above problem by always issuing our coins in the same order. This suggests a simple algorithm for how many ways we can make a certain amount of change from a list of coins. We can either use one of the first type of coin, or we can commit to never using that type of coin again.

waysToMake 0 _             = 1
waysToMake x _     | x < 0 = 0 
waysToMake x []            = 0
waysToMake x (c:cs)        = waysToMake (x-c) (c:cs) + waysToMake x cs

The preceding cases cover the boundary conditions. Assuming there are no problematic zero or negatively valued coins, there is 1 way to make 0. There are 0 ways to make a negative (< 0) amount of change. There are no ways to make change if you have no types of coins to make change with.

Let's see what happens if we try to evaluate waysToMake 15 [1,2,5]. We'll evaluate each waysToMake each step to keep things short.

waysToMake 15 [1,2,5]

waysToMake 14 [1,2,5] + waysToMake 15 [2,5]

waysToMake 13 [1,2,5] + waysToMake 14 [2,5] + waysToMake 13 [2,5] + waysToMake 15 [5]

waysToMake 12 [1,2,5] + waysToMake 13 [2,5] + waysToMake 12 [2,5] + waysToMake 14 [5]
+ waysToMake 11 [2,5] + waysToMake 13 [5] + waysToMake 10 [5] + waysToMake 15 []

waysToMake 11 [1,2,5] + waysToMake 12 [2,5] + waysToMake 11 [2,5] + waysToMake 13 [5]
+ waysToMake 10 [2,5] + waysToMake 12 [5] + waysToMake 9 [5] + waysToMake 14 []
+ waysToMake 9 [2,5] + waysToMake 11 [5] + waysToMake 8 [5] + waysToMake 13 []
+ waysToMake 5 [5] + waysToMake 10 [] + 0

The first three steps don't seem too suspicious, but we have already encountered waysToMake 13 [2,5] twice. In the fourth step we see waysToMake 12 [2, 5], waysToMake 11 [2, 5], waysToMake 13 [5] all of which we have seen before. We can see that we are going to be repeating most of the other expressions we generate, which will themselves generate expressions that repeat expressions. Ugh; my limited mental computer is already complaining that there's too much work to do. We could look for a better order to use the coins in (there is one) but it would still repeat subproblems, which would themselves repeat subproblems, etc.

Solving the problem quickly

There's a faster way to do this. Each step, we add together the numbers from using fewer of a coin and not using that coin. Let's make a table, and record each result as we calculate it. Each step, we will need the number from further left in the table (use one of the first coin) and one down the table (don't use any of the first coin). We'll eventually explore the whole table. We can start by filling in the numbers on the left and bottom edges covered by the boundary conditions.

Coins\Change  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5]       1  
  [2,5]       1 
    [5]       1 
     []       1 0 0 0 0 0 0 0 0 0  0  0  0  0  0  0

Now we'll add all the numbers that can be computed from the numbers already in the table. Using a 5 cent coin requires the number 5 spots to the left, as well as the number one spot down.

Coins\Change  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5]       1  
  [2,5]       1 
    [5]       1 0 0 0 0 1
     []       1 0 0 0 0 0 0 0 0 0  0  0  0  0  0  0

Using a 2 cent coin requires the number 2 sports to the left, as well as the number one spot down.

Coins\Change  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5]       1  
  [2,5]       1 0 1
    [5]       1 0 0 0 0 1 0 0 0 0  1
     []       1 0 0 0 0 0 0 0 0 0  0  0  0  0  0  0

Using a 1 cent coin requires the number 1 spot to the left, as well as the number one spot down.

Coins\Change  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5]       1 1 
  [2,5]       1 0 1 0 1
    [5]       1 0 0 0 0 1 0 0 0 0  1  0  0  0  0  1
     []       1 0 0 0 0 0 0 0 0 0  0  0  0  0  0  0

We'll go one more step. We can see that in just another 13 simple steps we'll compute the number for 15 in the top row, and we'll be done.

Coins\Change  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5]       1 1 2 
  [2,5]       1 0 1 0 1 1 1
    [5]       1 0 0 0 0 1 0 0 0 0  1  0  0  0  0  1
     []       1 0 0 0 0 0 0 0 0 0  0  0  0  0  0  0

Here's the table after all the steps. My mental computer had no difficulty calculating that waysToMake 15 [1,2,5] = 18.

Coins\Change  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5]       1 1 2 2 3 4 5 6 7 8 10 11 13 14 16 18
  [2,5]       1 0 1 0 1 1 1 1 1 1  2  1  2  1  2  2
    [5]       1 0 0 0 0 1 0 0 0 0  1  0  0  0  0  1
     []       1 0 0 0 0 0 0 0 0 0  0  0  0  0  0  0

If we found a better order to use the coins in (there is one) we wouldn't need to fill out all of the table, but it'd be approximately the same amount of work.

We can make a table like this in Haskell using an Array from Data.Array in the array package.

The general plan for using a table is going to be a - make a table defined in terms of our function waysToMake. Wherever waysToMake would recurse into itself, look the result up in the table instead. We have two wrinkles to deal with on the way.

The first wrinkle is that an Array requires that the index into the array be an instance of Ix. Our lists of coins don't make good array indexes. Instead, we can replace the list of coins with the number of coins we have skipped. 0 for the first row, 1 for the second, and the length of the list of coins for the last row.

The second wrinkle is that we want to look beyond the edge of the table. We could define a special lookup procedure to fill the part outside the table with 0, we can change the code so it never looks outside the table, or we could make an extra-large table that's twice as big. I'm going to skip all of those routes, and make checking that the value falls in the table part of the table's responsibility.

waysToMake x coins = waysToMake' (x,0)
    where
        tabled = tableRange ((0,0),(x,length coins)) waysToMake'
        waysToMake' (n, s) = waysToMake'' n (drop s coins)
            where                    
                waysToMake'' 0  _              = 1
                waysToMake'' n  _     | n <  0 = 0
                waysToMake'' n []              = 0
                waysToMake'' n (c:cs)          = tabled (n-c, s) + tabled (n, s+1)

tableRange makes a function that memorizes its results within some bounds. It builds an Array holding the lazily-evaluated results of a function within those bounds. The function it returns checks to see if the argument falls within the range of the bounds, and looks up the result from the table if it does, otherwise it asks the original function directly.

tableRange :: Ix a => (a, a) -> (a -> b) -> (a -> b)
tableRange bounds f = lookup
    where
        lookup x = if inRange bounds x then table ! x else f x
        table = makeArray bounds f

makeArray is a convenience function that makes an array containing the supplied function f applied to each index in the supplied bounds.

makeArray :: Ix i => (i, i) -> (i -> e) -> Array i e
makeArray bounds f = listArray bounds . map f $ range bounds

Our code now runs almost instantly, even for much larger problems like waysToMake 10000 [200,100,50,20,10,5,2,1].

We could go on to discuss how to make this "tabling" or "memoing" or "memoizing" or "dynamic programming" code for recursive functions generic, but I think that discussion would belong to a different question. It's a good topic if you want to learn about a very practical use of fixed points of functions.

3
ErikR On

Are you compiling your program or just running it in it ghci? Also what platform are you on?

numOfSplits 200 should only take around 6 secs when compiled.

Here is your code on ideaone.com:

https://ideone.com/sBWsvP

and for an input of 180 it runs in less than 5 seconds (which is the maximum running time allowed by that site.)

As Andrew Lorente pointed out, your intzahler function is the same as genericLength or even length, although in this case it doesn't seem to make that much of a difference.