Haskell中2个列表的笛卡尔积


74

我希望在Haskell中产生2个列表的笛卡尔积,但是我不知道该怎么做。笛卡尔积提供列表元素的所有组合:

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

这不是一个实际的家庭作业问题,并且与任何此类问题都没有关系,但是解决这个问题的方法可能会帮助我坚持下去。


Answers:


115

列表理解非常简单。要获取列表的笛卡儿积xsys,我们只需要把元组(x,y)的每个元素xxs,每个元素yys

这使我们可以理解以下列表:

cartProd xs ys = [(x,y) | x <- xs, y <- ys]

2
谢谢,如此简单却优雅,确实帮助了另一个问题:)
卡勒姆·罗杰斯

3
对Erlang也很好,谢谢。语法变化不大:cartProd(Xs,Ys)-> [{X,Y} || X <-Xs,Y <-Ys]。
2011年

72

正如其他答案所指出的那样,在Haskell中使用列表理解是最自然的方法。

如果您正在学习Haskell并想开发有关像这样的类型类的直觉Monad,那么弄清楚为什么这个略短的定义是等效的是一个有趣的练习:

import Control.Monad (liftM2)

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

您可能永远都不想用真实的代码编写此代码,但是基本的想法是您始终会在Haskell中看到的东西:我们正在liftM2将非Monadic函数提升(,)为monad,在这种情况下,特别是列出单子。

如果这没有任何意义或没有用,请忘记它-这只是解决问题的另一种方法。


4
了解单子首先要做什么的好主意:P
卡勒姆·罗杰斯

19
作为一个脚注(三年后):我想我现在是liftM2为了清楚起见在这里最初使用单子函数(更多的人听说过单子而不是应用函子?),但是您所需要的只是列表的应用函子实例,因此liftA2将等效地工作。
特拉维斯·布朗

58

如果输入列表属于同一类型,则可以使用sequence(使用Listmonad)获得任意数量列表的笛卡尔乘积。这将为您提供列表列表,而不是元组列表:

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

49

应用函子有一个非常优雅的方法:

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为细节。


4
很酷,您实际上可以根据需要扩展元组:(,,)<$> [1..3] <*> [4..6] <*> [7..9]
Landon Poch

17

其他答案假定两个输入列表是有限的。惯用的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,有(+*+),将上述内容cartesian2ddiagonal功能打包在一起,仅给出笛卡尔乘积运算:

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维笛卡尔积需求使用。


我刚刚安装了Universe-1.1,这很棒。非常感谢你。是的,您的名字无处不在,所以我们知道可以信任它。该值不可计算。我在做对角线,这些初始结果看起来很准确。笛卡尔积是止痛药,但您却是止痛药。再次感谢你。
fp_mora

@fp_mora很高兴听到您喜欢它!
丹尼尔·瓦格纳

@Daniel Wagner这是天赐的礼物。我不清楚。痛苦的是无限的清单。您会巧妙地处理它们。
fp_mora

15

实现此目的的另一种方法是使用应用程序:

import Control.Applicative

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs ys = (,) <$> xs <*> ys

12

正确的方法是使用列表推导,正如其他人已经指出的那样,但是如果您出于某种原因不想使用列表推导,则可以这样做:

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs [] = []
cartProd [] ys = []
cartProd (x:xs) ys = map (\y -> (x,y)) ys ++ cartProd xs ys

3
没有列表理解的更简单方法是cartProd xs ys = xs >>= \x -> ys >>= \y -> [(x,y)]
Chuck 2010年

5
代替map (\y -> (x,y))你可以写map ((,) x)
Yitz

1
@Chuck:很好:)对我而言,Haskell已有一段时间了,这就是为什么我
偏向

@Yitz:是的,打个好电话。我忘记了这一点(请参阅上面的“已经有一段时间了”)...
Stuart Golodetz 2010年

@Stuart List理解可能是“正确的方法”,但是这样做真的有不利之处吗?对我来说似乎很好,并且它可以帮助初学者围绕笛卡尔乘积的简单实现来集中精力。+1
Landon Poch '16

12

另一种使用do符号的方式:

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

6

嗯,一种很简单的方法就是列表理解:

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

尽管我不是Haskell专家(无论如何),但我想是如何做到的。



3

这是一份工作sequence。它的单例实现可以是:

cartesian :: [[a]] -> [[a]]
cartesian [] = return []
cartesian (x:xs) = x >>= \x' -> cartesian xs >>= \xs' -> return (x':xs')

*Main> cartesian [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]

您可能会注意到,以上内容类似于map纯函数的实现,但采用单子类型。因此,您可以将其简化为

cartesian :: [[a]] -> [[a]]
cartesian = mapM id

*Main> cartesian [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]

0

这是我的n元笛卡尔积的实现:

crossProduct :: [[a]] -> [[a]]
crossProduct (axis:[]) = [ [v] | v <- axis ]
crossProduct (axis:rest) = [ v:r | v <- axis, r <- crossProduct rest ]

1
怎么crossProduct = sequence
Daniel Wagner

如果列表为空怎么办?我猜您在这里确定了错误的基本情况。
dfeuer

0

仅使用递归模式匹配为发烧友增加另一种方法。

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

0

没有列表理解的递归模式匹配

crossProduct [] b=[]
crossProduct (x : xs) b= [(x,b)] ++ crossProduct xs b

cartProd _ []=[]
cartProd x (u:uv) = crossProduct x u ++ cartProd x uv

不鼓励仅使用代码的答案。请添加一些有关如何解决问题或与现有答案有何不同的解释。评论来自
尼克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.