Декартово произведение 2 списков в Haskell - PullRequest
63 голосов
/ 08 ноября 2010

Я хочу произвести декартово произведение из 2 списков в Haskell, но я не могу понять, как это сделать. Декартово произведение дает все комбинации элементов списка:

xs = [1,2,3]
ys = [4,5,6]

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs ys ==> [(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6)]

Это не вопрос домашней работы и он не связан с каким-либо подобным вопросом, но способ решения этой проблемы может помочь с вопросом, на котором я застрял.

Ответы [ 13 ]

99 голосов
/ 08 ноября 2010

Это очень легко для списочных представлений.Чтобы получить декартово произведение списков xs и ys, нам просто нужно взять кортеж (x,y) для каждого элемента x в xs и каждого элемента y в ys.

Это дает нам следующее понимание списка:

cartProd xs ys = [(x,y) | x <- xs, y <- ys]
60 голосов
/ 08 ноября 2010

Как уже отмечалось в других ответах, использование понимания списков является наиболее естественным способом сделать это в Haskell.

Если вы изучаете Haskell и хотите поработать над разработкой интуиции о классах типов, таких как Monad, это забавное упражнение, чтобы выяснить, почему это немного более короткое определение эквивалентно:

import Control.Monad (liftM2)

cartProd :: [a] -> [b] -> [(a, b)]
cartProd = liftM2 (,)

Вы, вероятно, никогда не захотите писать это в реальном коде, но основная идея - это то, что вы будете постоянно видеть в Haskell: мы используем liftM2, чтобы поднять немонадную функцию (,) в монаду - в данном случае, в частности, в монаду списка.

Если это не имеет никакого смысла или бесполезно, забудьте об этом - это просто еще один способ взглянуть на проблему.

52 голосов
/ 08 ноября 2010

Если ваши входные списки одного типа, вы можете получить декартово произведение произвольного числа списков, используя sequence (используя монаду List). Это даст вам список списков вместо списка кортежей:

> sequence [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
45 голосов
/ 08 ноября 2010

Есть очень элегантный способ сделать это с Applicative Functors:

import Control.Applicative

(,) <$> [1,2,3] <*> [4,5,6]
-- [(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6)]

Основная идея заключается в применении функции к «завернутым» аргументам, например,

(+) <$> (Just 4) <*> (Just 10)
-- Just 14

В случае списков, функция будет применена ко всем комбинациям, поэтому все, что вам нужно сделать, это "кортежировать" их с помощью (,).

Подробнее см. http://learnyouahaskell.com/functors-applicative-functors-and-monoids#applicative-functors или (более теоретически) http://www.soi.city.ac.uk/~ross/papers/Applicative.pdf.

14 голосов
/ 24 октября 2017

Другие ответы предполагают, что два списка ввода конечны. Часто идиоматический код на Haskell включает в себя бесконечные списки, поэтому стоит кратко прокомментировать, как производить бесконечное декартово произведение в случае необходимости.

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

   1  2  3  4 ...
a a1 a2 a3 a4 ...
b b1 b2 b3 b4 ...
c c1 c2 c3 c4 ...
d d1 d2 d3 d4 ...

.  .  .  .  . .
.  .  .  .  .  .
.  .  .  .  .   .

Конечно, работа через любой отдельный ряд даст нам бесконечные элементы, прежде чем он достигнет следующего ряда; аналогично, движение по столбцам будет иметь катастрофические последствия. Но мы можем идти по диагоналям, которые идут вниз и влево, начиная с немного дальше вправо каждый раз, когда мы достигаем края сетки.

a1

   a2
b1

      a3
   b2
c1

         a4
      b3
   c2
d1

... и так далее. В порядке, это даст нам:

a1 a2 b1 a3 b2 c1 a4 b3 c2 d1 ...

Чтобы закодировать это в Haskell, мы сначала можем написать версию, которая создает двумерную таблицу:

cartesian2d :: [a] -> [b] -> [[(a, b)]]
cartesian2d as bs = [[(a, b) | a <- as] | b <- bs]

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

diagonalBad :: [[a]] -> [a]
diagonalBad xs =
    [ xs !! row !! col
    | diagonal <- [0..]
    , depth <- [0..diagonal]
    , let row = depth
          col = diagonal - depth
    ]

Эта реализация немного прискорбна: повторяющаяся операция индексации списка !! становится все дороже с течением времени, обеспечивая довольно плохую асимптотическую производительность. Более эффективная реализация примет вышеупомянутую идею, но осуществит это, используя застежки-молнии Итак, мы разделим нашу бесконечную сетку на три фигуры, подобные этой:

a1 a2 / a3 a4 ...
     /
    /
b1 / b2 b3 b4 ...
  /
 /
/
c1 c2 c3 c4 ...
---------------------------------
d1 d2 d3 d4 ...

 .  .  .  . .
 .  .  .  .  .
 .  .  .  .   .

Верхний левый треугольник будет битами, которые мы уже выпустили; верхний правый четырехугольник будет строками, которые были частично испущены, но все же будут способствовать результату; и нижний прямоугольник будет строками, которые мы еще не начали излучать. Начнем с того, что верхний треугольник и верхний четырехугольник будут пустыми, а нижний прямоугольник будет всей сеткой. На каждом шаге мы можем испустить первый элемент каждой строки в верхнем четырехугольнике (по существу, переместив наклонную линию на единицу), затем добавить одну новую строку из нижнего прямоугольника в верхний четырехугольник (по существу, сдвинув горизонтальную линию вниз на единицу ).

diagonal :: [[a]] -> [a]
diagonal = go [] where
    go upper lower = [h | h:_ <- upper] ++ case lower of
        []         -> concat (transpose upper')
        row:lower' -> go (row:upper') lower'
        where upper' = [t | _:t <- upper]

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

Но вы, конечно, не должны писать весь этот код самостоятельно! Вместо этого вы должны использовать пакет universe . В Data.Universe.Helpers есть (+*+), который объединяет в себе вышеуказанные функции cartesian2d и diagonal, чтобы дать только операцию декартового произведения:

Data.Universe.Helpers> "abcd" +*+ [1..4]
[('a',1),('a',2),('b',1),('a',3),('b',2),('c',1),('a',4),('b',3),('c',2),('d',1),('b',4),('c',3),('d',2),('c',4),('d',3),('d',4)]

Вы также можете увидеть сами диагонали, если эта структура станет полезной:

Data.Universe.Helpers> mapM_ print . diagonals $ cartesian2d "abcd" [1..4]
[('a',1)]
[('a',2),('b',1)]
[('a',3),('b',2),('c',1)]
[('a',4),('b',3),('c',2),('d',1)]
[('b',4),('c',3),('d',2)]
[('c',4),('d',3)]
[('d',4)]

Если у вас есть много списков для объединения продуктов, повторение (+*+) может несправедливо сместить определенные списки; Вы можете использовать choices :: [[a]] -> [[a]] для ваших n-мерных картезианских потребностей.

11 голосов
/ 08 ноября 2010

Еще один способ сделать это - использовать аппликативы:

import Control.Applicative

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs ys = (,) <$> xs <*> ys
11 голосов
/ 08 ноября 2010

Еще один способ, используя обозначение do:

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs ys = do x <- xs
                    y <- ys
                    return (x,y)
11 голосов
/ 08 ноября 2010

Правильный путь - использование списочных представлений, как уже отмечали другие люди, но если вы хотите сделать это без использования списочных представлений по любой причине, то вы можете сделать это:

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs [] = []
cartProd [] ys = []
cartProd (x:xs) ys = map (\y -> (x,y)) ys ++ cartProd xs ys
6 голосов
/ 08 ноября 2010

Что ж, одним из самых простых способов сделать это было бы с использованием списочных представлений:

cartProd :: [a] -> [b] -> [(a, b)]
cartProd xs ys = [(x, y) | x <- xs, y <- ys]

Я полагаю, что именно так я и поступлю, хотя я не эксперт по Haskell (во что бы то ни стало).

5 голосов
/ 08 ноября 2010

что-то вроде:

cartProd x y = [(a,b) | a <- x, b <- y]
...