【问题标题】:Generating Cartesian products in Haskell在 Haskell 中生成笛卡尔积
【发布时间】:2016-05-07 05:00:30
【问题描述】:

我正在尝试生成 n 个数字的所有可能组合。例如,如果 n = 3,我想要以下组合:

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

post 描述了如何在 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 &gt; 3,遵循相同的模式很快就会变得非常愚蠢。假设我想找到所有的组合:(a, b, c, d, e, f, g, h, i, j) 等。

任何人都可以在这里指出正确的方向吗?理想情况下,我宁愿不使用内置函数,因为我正在尝试学习 Haskell,我宁愿花时间理解一段代码,而不仅仅是使用别人编写的包。不需要元组,列表也可以。

【问题讨论】:

  • 只写一个递归函数。给定n-1 选项的组合,生成n 选项的组合。你会想要输出一个列表,而不是一个固定长度的元组。
  • 如果在你解决了这个问题之后你仍然想利用库函数,请知道replicateM n [1..100] 可以完成这项工作。不过,了解原因并非易事——所以等你更熟悉 Haskell 之后再做吧。
  • sequence $ replicate 3 [0..9] 会将叉积生成为列表,不是元组但易于泛化
  • 生成所有数字组合...例如3 位数字是 000001002...009010011...999...我错过了什么,或者不是这个计数的数学概念?
  • 为什么没有人提到这个问题与组合完全无关。

标签: haskell functional-programming tuples cartesian-product


【解决方案1】:

我的other answer 给出了一个算术算法来枚举所有数字组合。这是通过概括您的示例而产生的替代解决方案。它也适用于非数字,因为它只使用列表的结构。

首先,让我们提醒自己如何将列表推导式用于三位数组合。

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

我们想要抽象列表推导中的变量数量(相当于循环的嵌套)。让我们开始玩弄它。首先,我将把这些列表推导重写为等效的 monad 推导

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]

有趣。看起来threeDigitCombinationstwoDigitCombinations 大致相同的一元动作,但有一个额外的语句。再次重写...

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]] -&gt; [[a]] 的定义,我们可以看到它实际上并不需要使用 lists 本身:它只使用列表的 monad-y 部分。它可以在任何单子上工作,而且确实如此!它在标准库中,名为sequence :: Monad m =&gt; [m a] -&gt; m [a]。如果您对此感到困惑,请将m 替换为[],您应该会看到这些类型的含义相同。

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

最后,注意到sequence . replicate nreplicateM 的定义,我们把它归结为一个非常简洁的单行。

combinationsOfDigits n = replicateM n [0..9]

总而言之,replicateM n 给出了输入列表的 n-ary 组合。这适用于任何列表,而不仅仅是数字列表。实际上,它适用于任何 monad - 尽管“组合”解释仅在您的 monad 代表选择时才有意义。

这段代码确实非常简洁!如此之多,以至于我认为它的工作原理并不完全明显,这与我在其他答案中向您展示的算术版本不同。 list monad 一直是我觉得不太直观的 monad 之一,至少当您使用高阶 monad 组合器而不是 do-notation 时。

另一方面,它的运行速度比数字运算版本快得多。在我的(高规格)MacBook Pro 上,使用-O2 编译,这个版本计算 5 位数组合的速度比处理数字的版本快 4 倍。 (如果有人能解释我正在听的原因!)

【讨论】:

  • 这和第一个答案一样有效吗?看起来应该是这样,虽然我对 GHC 的优化能力不是特别精通。
  • 好问题,我不知道答案。我会在今天晚些时候有空的时候对其进行基准测试并更新帖子。
【解决方案2】:

三个数字的所有组合是什么?让我们手动写一些。

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

我们最终只是计数!我们枚举了 0 到 999 之间的所有数字。对于任意位数,这可以直接概括:上限是 10^n(不包括),其中 n 是位数。

数字是故意这样设计的。如果有一个可能的三位数组合不是有效数字,或者如果有一个低于 1000 的数字不能用三位数组合表示,那就太奇怪了!

这对我来说是一个简单的计划,它只涉及算术,不需要深入了解 Haskell*:

  1. 生成介于 0 和 10^n 之间的数字列表
  2. 将每个数字变成数字列表。

第 2 步是有趣的部分。要提取三位数字的数字(以 10 为基数),you do this

  1. 取您的数字除以 100 的商和余数。商是数字的第一位。
  2. 取第 1 步的余数,然后取其商和相对于 10 的余数。商是第二个数字。
  3. 第 2 步的余数是第三位数。这与取 1 的商相同。

对于 n 位数字,我们将商 n 乘以,以 10^(n-1) 开始,以 1 结束。每次,我们使用上一步的余数作为下一步的输入。这表明我们将数字转换为数字列表的函数应该实现为折叠:我们将通过操作线程化余数并构建一个列表。 (如果你不在 base 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)]]

令我吃惊的是,这段代码看起来与我对我们想要执行的算术的英文描述非常相似。我们通过从 0 向上取幂来生成一个 10 次幂表。然后我们把那张桌子折回去;在每一步,我们将商放在数字列表中,并将余数发送到下一步。我们必须在最后reverse 输出列表,因为它是从右到左构建的。

顺便说一句,在 Haskell 中,生成列表、转换列表、然后将其折叠起来的模式是惯用的做法。它甚至还有一个自以为是的数学名称,hylomorphismGHC knows about this pattern too 并且可以将它编译成一个紧密的循环,优化掉你正在使用的列表的存在。

让我们测试一下吧!

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]

它就像一个魅力! (好吧,当numberOfDigitstheNumber 来说太小时,它会出现异常行为,但不要介意这一点。)现在我们只需要生成一个使用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 的版本,请参阅 my other answer

【讨论】:

  • 这是一篇令人难以置信的帖子! Haskell 不断让我大吃一惊!你有什么教程可以推荐吗?正如您所指出的,这个答案比您的第二个答案更容易理解,虽然我认为我可以理解这个答案,但我几乎没有触及另一个答案的表面。这是非常令人生畏的!但是,很高兴看到您的解决方案如何与标准库如此完美地结合在一起。如果我能接受这两个答案,我会的!
  • 谢谢!乐意效劳。教程方面,您可能已经阅读过,但Learn You A Haskell 仍然是我读过的最好的编程书籍之一。它有趣且平易近人,并且很好地解释了更高级的主题。顺便说一句,如果您对其他答案有什么不明白的地方,我很乐意详细说明。
【解决方案3】:
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]

【讨论】:

    猜你喜欢
    • 2016-10-08
    • 1970-01-01
    • 1970-01-01
    • 2011-09-18
    • 2021-05-06
    • 2012-04-24
    • 2011-12-26
    • 2021-09-05
    • 1970-01-01
    相关资源
    最近更新 更多