Haskell generating all combinations of n numbers

前端 未结 3 1267
深忆病人
深忆病人 2021-02-15 17:38

I am trying to generate all possible combinations of n numbers. For example if n = 3 I would want the following combinations:

(0,0,0), (0,0,1), (0,0,2)... (0,0,9         


        
相关标签:
3条回答
  • 2021-02-15 18:13
    combos 1 list = map (\x -> [x]) list
    combos n list = foldl (++) [] $ map (\x -> map (\y -> x:y) nxt) list
        where nxt = combos (n-1) list
    

    In your case

    combos 3 [0..9]
    
    0 讨论(0)
  • 2021-02-15 18:21

    My other answer gave an arithmetic algorithm to enumerate all the combinations of digits. Here's an alternative solution which arises by generalising your example. It works for non-numbers, too, because it only uses the structure of lists.

    First off, let's remind ourselves of how you might use a list comprehension for three-digit combinations.

    threeDigitCombinations = [[x, y, z] | x <- [0..9], y <- [0..9], z <- [0..9]]
    

    What's going on here? The list comprehension corresponds to nested loops. z counts from 0 to 9, then y goes up to 1 and z starts counting from 0 again. x ticks the slowest. As you note, the shape of the list comprehension changes (albeit in a uniform way) when you want a different number of digits. We're going to exploit that uniformity.

    twoDigitCombinations = [[x, y] | x <- [0..9], y <- [0..9]]
    

    We want to abstract over the number of variables in the list comprehension (equivalently, the nested-ness of the loop). Let's start playing around with it. First, I'm going to rewrite these list comprehensions as their equivalent monad comprehensions.

    threeDigitCombinations = do
        x <- [0..9]
        y <- [0..9]
        z <- [0..9]
        return [x, y, z]
    twoDigitCombinations = do
        x <- [0..9]
        y <- [0..9]
        return [x, y]
    

    Interesting. It looks like threeDigitCombinations is roughly the same monadic action as twoDigitCombinations, but with an extra statement. Rewriting again...

    zeroDigitCombinations = [[]]  -- equivalently, `return []`
    oneDigitCombinations = do
        z <- [0..9]
        empty <- zeroDigitCombinations
        return (z : empty)
    twoDigitCombinations = do
        y <- [0..9]
        z <- oneDigitCombinations
        return (y : z)
    threeDigitCombinations = do
        x <- [0..9]
        yz <- twoDigitCombinations
        return (x : yz)
    

    It should be clear now what we need to parameterise:

    combinationsOfDigits 0 = return []
    combinationsOfDigits n = do
        x <- [0..9]
        xs <- combinationsOfDigits (n - 1)
        return (x : xs)
    
    ghci> combinationsOfDigits' 2
    [[0,0],[0,1],[0,2],[0,3],[0,4],[0,5],[0,6],[0,7],[0,8],[0,9],[1,0],[1,1] ... [9,8],[9,9]]
    

    It works, but we're not done yet. I want to show you that this is an instance of a more general monadic pattern. First I'm going to change the implementation of combinationsOfDigits so that it folds up a list of constants.

    combinationsOfDigits n = foldUpList $ replicate n [0..9]
        where foldUpList [] = return []
              foldUpList (xs : xss) = do
                  x <- xs
                  ys <- foldUpList xss
                  return (x : ys)
    

    Looking at the definiton of foldUpList :: [[a]] -> [[a]], we can see that it doesn't actually require the use of lists per se: it only uses the monad-y parts of lists. It could work on any monad, and indeed it does! It's in the standard library, and it's called sequence :: Monad m => [m a] -> m [a]. If you're confused by that, replace m with [] and you should see that those types mean the same thing.

    combinationsOfDigits n = sequence $ replicate n [0..9]
    

    Finally, noting that sequence . replicate n is the definition of replicateM, we get it down to a very snappy one-liner.

    combinationsOfDigits n = replicateM n [0..9]
    

    To summarise, replicateM n gives the n-ary combinations of an input list. This works for any list, not just a list of numbers. Indeed, it works for any monad - though the "combinations" interpretation only makes sense when your monad represents choice.

    This code is very terse indeed! So much so that I think it's not entirely obvious how it works, unlike the arithmetic version I showed you in my other answer. The list monad has always been one of the monads I find less intuitive, at least when you're using higher-order monad combinators and not do-notation.

    On the other hand, it runs quite a lot faster than the number-crunching version. On my (high-spec) MacBook Pro, compiled with -O2, this version calculates the 5-digit combinations about 4 times faster than the version which crunches numbers. (If anyone can explain the reason for this I'm listening!)

    0 讨论(0)
  • 2021-02-15 18:28

    What are all the combinations of three digits? Let's write a few out manually.

    000, 001, 002 ... 009, 010, 011 ... 099, 100, 101 ... 998, 999
    

    We ended up simply counting! We enumerated all the numbers between 0 and 999. For an arbitrary number of digits this generalises straightforwardly: the upper limit is 10^n (exclusive), where n is the number of digits.

    Numbers are designed this way on purpose. It would be jolly strange if there was a possible combination of three digits which wasn't a valid number, or if there was a three-digit number which couldn't be expressed by combining three digits!

    This suggests a simple plan to me, which just involves arithmetic and doesn't require a deep understanding of Haskell*:

    1. Generate a list of numbers between 0 and 10^n
    2. Turn each number into a list of digits.

    Step 2 is the fun part. To extract the digits (in base 10) of a three-digit number, you do this:

    1. Take the quotient and remainder of your number with respect to 100. The quotient is the first digit of the number.
    2. Take the remainder from step 1 and take its quotient and remainder with respect to 10. The quotient is the second digit.
    3. The remainder from step 2 was the third digit. This is the same as taking the quotient with respect to 1.

    For an n-digit number, we take the quotient n times, starting with 10^(n-1) and ending with 1. Each time, we use the remainder from the last step as the input to the next step. This suggests that our function to turn a number into a list of digits should be implemented as a fold: we'll thread the remainder through the operation and build a list as we go. (I'll leave it to you to figure out how this algorithm changes if you're not in base 10!)


    Now let's implement that idea. We want calculate a specified number of digits, zero-padding when necessary, of a given number. What should the type of digits be?

    digits :: Int -> Int -> [Int]
    

    Hmm, it takes in a number of digits and an integer, and produces a list of integers representing the digits of the input integer. The list will contain single-digit integers, each one of which will be one digit of the input number.

    digits numberOfDigits theNumber = reverse $ fst $ foldr step ([], theNumber) powersOfTen
        where step exponent (digits, remainder) =
                  let (digit, newRemainder) = remainder `divMod` exponent
                  in (digit : digits, newRemainder)
              powersOfTen = [10^n | n <- [0..(numberOfDigits-1)]]
    

    What's striking to me is that this code looks quite similar to my English description of the arithmetic we wanted to perform. We generate a powers-of-ten table by exponentiating numbers from 0 upwards. Then we fold that table back up; at each step we put the quotient on the list of digits and send the remainder to the next step. We have to reverse the output list at the end because of the right-to-left way it got built.

    By the way, the pattern of generating a list, transforming it, and then folding it back up is an idiomatic thing to do in Haskell. It's even got its own high-falutin' mathsy name, hylomorphism. GHC knows about this pattern too and can compile it into a tight loop, optimising away the very existence of the list you're working with.

    Let's test it!

    ghci> digits 3 123
    [1, 2, 3]
    ghci> digits 5 10101
    [1, 0, 1, 0, 1]
    ghci> digits 6 99
    [0, 0, 0, 0, 9, 9]
    

    It works like a charm! (Well, it misbehaves when numberOfDigits is too small for theNumber, but never mind about that.) Now we just have to generate a counting list of numbers on which to use digits.

    combinationsOfDigits :: Int -> [[Int]]
    combinationsOfDigits numberOfDigits = map (digits numberOfDigits) [0..(10^numberOfDigits)-1]
    

    ... and we've finished!

    ghci> combinationsOfDigits 2
    [[0,0],[0,1],[0,2],[0,3],[0,4],[0,5],[0,6],[0,7],[0,8],[0,9],[1,0],[1,1] ... [9,7],[9,8],[9,9]]
    

    * For a version which does require a deep understanding of Haskell, see my other answer.

    0 讨论(0)
提交回复
热议问题