Haskell, генерирующий все комбинации из n чисел

Я пытаюсь создать все возможные комбинации из n чисел. Например, если n = 3, Мне нужны следующие комбинации:

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

этой в должности описывает как это сделать для N = 3:

[(a,b,c) | m <- [0..9], a <- [0..m], b <- [0..m], c <- [0..m] ]

или чтобы избежать дублирования (т. е. нескольких копий одного и того же n-uple):

let l = 9; in [(a,b,c) | m <- [0..3*l],
                         a <- [0..l], b <- [0..l], c <- [0..l],
                         a + b + c == m ]

однако после того же шаблона станет очень глупо очень быстро для n > 3. Скажем, я хотел найти все комбинации:(a, b, c, d, e, f, g, h, i, j), так далее.

может кто-нибудь мне точку в правильном направлении? В идеале я бы предпочел не использовать встроенный funtion, поскольку я пытаюсь изучить Haskell, и я бы предпочел потратить время на понимание кода, чем просто использовать пакет, написанный кем-то другим. Кортеж не требуется, список также будет работать.

3 ответов


что все комбинации из трех цифр? Давайте выпишем несколько вручную.

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

мы закончили просто инвентаризация! Мы перечислили все числа от 0 до 999. Для произвольного числа цифр это обобщает прямолинейно: верхний предел 10^n (эксклюзивный), где n - количество цифр.

номера разработаны таким образом специально. Было бы очень странно, если бы существовала возможная комбинация из трех цифры, которые не были действительным числом, или если было трехзначное число, которое не может быть выражено путем объединения трех цифр!

это предлагает мне простой план, который просто включает в себя арифметику и не требует глубокого понимания Haskell*:

  1. создать список чисел от 0 до 10^n
  2. превратите каждое число в список цифр.

Шаг 2-это забавная часть. Для извлечения цифр (в базе 10) a трехзначное число,сделай это:

  1. возьмите фактор и остаток вашего числа по отношению к 100. Фактор - это первая цифра числа.
  2. возьмите остаток от шага 1 и возьмите его фактор и остаток по отношению к 10. Фактор - это вторая цифра.
  3. остаток от шага 2 был третьей цифрой. Это же частное по отношению к 1.

для n-цифра номер, мы берем фактор n раза, начиная с 10^(n-1) и заканчивая 1. Каждый раз мы используем остаток от последнего шага в качестве входных данных для следующего шага. Это говорит о том, что наша функция превращения числа в список цифр должна быть реализована как складка: мы пропустим остаток через операцию и построим список по мере продвижения. (Я оставлю это вам, чтобы выяснить, как этот алгоритм меняется, если вы не в базе 10!)


теперь давайте реализуем эту идею. Мы хотим вычислить заданное количество цифр, при необходимости с нулевым заполнением, заданного числа. Какой должен быть тип digits быть?

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

Хм, он принимает количество цифр и целое число и создает список целых чисел, представляющих цифры входного целого числа. Список будет содержать однозначные целые числа, каждое из которых будет одной цифрой входного числа.

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)]]

что поразительно, что этот код очень похож на мое английское описание арифметики, которую мы хотели выполнить. Мы создаем полномочия-из-десяти таблице exponentiating цифры от 0 вверх. Затем мы складываем эту таблицу обратно; на каждом шаге мы помещаем фактор в список цифр и отправляем остаток на следующий шаг. Мы должны!--13--> список вывода в конце из-за того, что он был построен справа налево.

кстати, шаблон генерации списка, преобразования это, а затем сложить его обратно-идиоматическая вещь, чтобы сделать в Haskell. У него даже есть собственное высокопарное название mathsy,hylomorphism. GHC тоже знает об этом шаблоне и может скомпилировать его в плотный цикл, оптимизируя само существование списка, с которым вы работаете.

давайте его!
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]

это работает как шарм! (Ну, он плохо себя ведет, когда numberOfDigits слишком мал для theNumber, но не об этом.) Теперь мы просто нужно создать список подсчета чисел, на которых использовать digits.

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

... и мы закончили!

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]]

* для версии, которая тут требуется глубокое понимание Haskell, см. мой другой ответ.


мой другого ответа дал арифметический алгоритм для перечисления всех комбинаций цифр. Вот альтернативное решение, которое возникает, обобщая ваш пример. Он также работает для не-чисел, потому что он использует только структуру списков.

во-первых, давайте напомним себе, как вы могли бы использовать понимание списка для трехзначных комбинаций.

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

что здесь происходит? Понимание списка соответствует вложенным циклам. z отсчет от 0 до 9, тогда y идет до 1 и z снова начинает отсчет с 0. x тикает медленнее. Как вы заметили, форма понимания списка изменяется (хотя и единообразно), когда вы хотите другое количество цифр. Мы будем использовать это единообразие.

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

мы хотим абстрагироваться от количества переменных в понимании списка (эквивалентно вложенности цикла). Давай поиграем с ним. Во-первых, я переписать этот список понятий как их эквивалент выделения монады.

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]

интересные. Похоже на threeDigitCombinations это примерно то же монадическое действие, что и twoDigitCombinations, но с дополнительным заявлением. Снова переписываю...

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)

теперь должно быть ясно, что нам нужно параметризовать:

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]]

это работает, но мы еще не закончили. Я хочу показать вам, что это пример более общей монадической модели. Сначала я собираюсь изменить реализацию combinationsOfDigits так что он складывает список констант.

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

глядя на определение foldUpList :: [[a]] -> [[a]], мы видим, что это на самом деле не требуют использования списки per se: он использует только монадные части списков. Это может работать на любой монаде, и это действительно так! Он находится в стандартной библиотеке и называется sequence :: Monad m => [m a] -> m [a]. Если вы смущены этим, замените m С [] и вы должны увидеть что эти типы означают одно и то же.

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

наконец, заметив, что sequence . replicate n определение replicateM, мы сводим его к очень быстрому однострочному.

combinationsOfDigits n = replicateM n [0..9]

таким образом, replicateM n дает n-ary комбинации входного списка. Это работает для любого списка, а не только для списка чисел. Действительно, он работает для любой монады - хотя интерпретация "комбинаций" имеет смысл только тогда, когда ваша монада представляет выбор.

этот код очень лаконичен! Настолько, что я думаю, что это не совсем очевидно, как это работает, в отличие от арифметической версии, которую я показал вам в моем другом ответе. Монада списка всегда была одной из монад, которые я нахожу менее интуитивными, по крайней мере, когда вы используете комбинаторы монад более высокого порядка, а не do-нотации.

С другой стороны, он работает намного быстрее, чем версия с числом. На моем (High-spec) MacBook Pro, скомпилированном с -O2, эта версия вычисляет 5-значные комбинации примерно в 4 раза быстрее, чем версия, которая хрустит цифры. (Если кто-нибудь может объяснить причину этого, я слушаю!)

benchmark


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

в вашем случае

combos 3 [0..9]