【问题标题】:Cartesian product of 2 lists in HaskellHaskell中2个列表的笛卡尔积
【发布时间】:2011-05-06 09:43:51
【问题描述】:

我希望在 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)]

这不是一个实际的家庭作业问题,也与任何此类问题无关,但解决此问题的方式可能对我遇到的问题有所帮助。

【问题讨论】:

标签: haskell combinatorics cartesian-product


【解决方案1】:

使用列表推导很容易做到这一点。要获得列表xsys 的笛卡尔积,我们只需要为xs 中的每个元素xys 中的每个元素y 获取元组(x,y)

这给了我们以下列表理解:

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

【讨论】:

  • 谢谢,如此简单而优雅,真的帮助解决了另一个问题:)
  • 对 Erlang 也有好处,谢谢。语法变化不大: cartProd(Xs, Ys) -> [{X,Y} || X
【解决方案2】:

正如其他答案所指出的,使用列表推导是在 Haskell 中执行此操作的最自然方式。

但是,如果您正在学习 Haskell 并希望致力于开发关于类型类(如 Monad)的直觉,那么找出为什么这个稍短的定义是等效的,这是一个有趣的练习:

import Control.Monad (liftM2)

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

你可能永远不想用真正的代码来写这个,但基本的想法是你会一直在 Haskell 中看到的东西:我们使用 liftM2 来提升非单子函数 (,)进入一个单子——在这种情况下特别是列表单子。

如果这没有任何意义或没有用,那就忘记它——这只是看待问题的另一种方式。

【讨论】:

  • 先了解 monad 实际做什么可能是个好主意:P
  • 作为脚注(三年后):我现在猜想,为了清楚起见,我最初在这里使用了 monadic liftM2(听说过 monad 的人多于 applicative functor?),但是您只需要列表的应用函子实例,因此liftA2 将等效地工作。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

    使用 Applicative Functor 有一种非常优雅的方法:

    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

    【讨论】:

    • 非常酷,您可以根据需要实际扩展元组:(,,) [1..3] [4..6] [7..9 ]
    【解决方案5】:

    其他答案假设两个输入列表是有限的。通常,惯用的 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
        ]
    

    这个实现有点不幸:重复的列表索引操作!! 变得越来越昂贵,渐近性能很差。更有效的实现将采用上述想法,但使用 zippers 实现它。因此,我们将无限网格划分为如下三种形状:

    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]] -&gt; [[a]] 来满足您的 n 维笛卡尔积需求。

    【讨论】:

    • 我刚刚安装了 Universe-1.1,非常棒。非常感谢你。是的,你的名字到处都是,所以我们知道要相信它。价值是无法计算的。我在做对角线,这些初步结果看起来很准确。笛卡尔积是一种痛苦,但你是一个止痛药。再次感谢您。
    • @fp_mora 很高兴听到你喜欢它!
    • @Daniel Wagner 这是天赐之物。我不清楚。令人痛苦的是无限的列表。你可以熟练地处理它们。
    【解决方案6】:

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

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

    【讨论】:

      【解决方案7】:

      另一种方式,使用do 表示法:

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

      【讨论】:

        【解决方案8】:

        正如其他人已经指出的那样,正确的方法是使用列表推导,但是如果您出于任何原因想在不使用列表推导的情况下这样做,那么您可以这样做:

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

        【讨论】:

        • 没有列表理解的更简单的方法是cartProd xs ys = xs &gt;&gt;= \x -&gt; ys &gt;&gt;= \y -&gt; [(x,y)]
        • 你可以写成map ((,) x),而不是map (\y -&gt; (x,y))
        • @Chuck: Nice :) 对我来说,Haskell 方面已经有一段时间了,这就是为什么我偏向于简单化的解决方案......
        • @Yitz:是的,好电话。我已经忘记了(见上文“已经有一段时间了”)...
        • @Stuart List 理解可能是“正确的方法”,但这样做真的有什么缺点吗?对我来说似乎很好,它可以帮助初学者围绕笛卡尔积的简单实现来思考。 +1
        【解决方案9】:

        嗯,一种非常简单的方法是使用列表推导:

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

        虽然我不是 Haskell 专家(无论如何),但我想我会怎么做。

        【讨论】:

          【解决方案10】:

          类似:

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

          【讨论】:

            【解决方案11】:

            这是sequenceing 的工作。它的单子实现可能是:

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

            【讨论】:

              【解决方案12】:

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

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

              【讨论】:

              • crossProduct = sequence 怎么样?
              • 如果列表为空怎么办?我猜你在这里确定了错误的基本情况。
              【解决方案13】:

              只是为发烧友增加了一种方式,只使用递归模式匹配。

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

              【讨论】:

                【解决方案14】:

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

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

                【讨论】:

                • 不鼓励仅使用代码回答。请添加一些解释,说明这如何解决问题,或者这与现有答案有何不同。 From Review
                猜你喜欢
                • 2015-12-05
                • 2023-04-10
                • 2011-02-06
                • 2013-12-29
                • 2019-08-26
                • 2011-09-18
                • 2012-04-24
                相关资源
                最近更新 更多