【问题标题】:Functional, tail-recursive way to generate all possible combinations from a dictionary and a dimension从字典和维度生成所有可能组合的函数式尾递归方式
【发布时间】:2020-10-27 01:28:59
【问题描述】:

我想找出实现以下指定函数的简洁、函数式和尾递归(如果可能)的方式:

(define (make-domain digits dimension)
    ;; Implementation)
;; Usage
(make-domain '(0 1) 0) => (())
(make-domain '(0 1) 1) => ((0) (1))
(make-domain '(0 1) 2) => ((0 0) (0 1) (1 0) (1 1))
(make-domain '(0 1) 3) => ((0 0 0) (0 0 1) (0 1 0) (0 1 1) (1 0 0) (1 0 1) (1 1 0) (1 1 1))

我更喜欢使用尽可能少的辅助函数或库函数的 Scheme 实现,但 SML 或 Haskell 也可以。我正在尝试找到可能使用相互或嵌套递归的尾递归解决方案,但目前没有运气。

非常感谢!

【问题讨论】:

  • 这是我的爱好,也是我自己的调查。对我来说重要的是函数式尾递归算法(想法),而不是实现语言
  • 是否是作业问题并不重要。如果问得好,它就是这里的主题,并且由提问者确保他们遵守学术诚实约束。如果它没有得到很好的要求,它应该被否决、关闭或编辑。在这种情况下,我认为这不是很好的问题,因为没有做出任何努力或尝试。这个功能已经实现了数百万次——“总的来说”还能说什么呢?只有与您对现有实现的哪些部分不了解的解释配对时,答案才会有用。
  • 另外,这个问题不适合尾递归。您可以通过在堆上构建自己的堆栈来伪造它,但最终您需要|m|^n 内存来保存结果,因此避免n 堆栈帧并不会真正产生任何重大影响。
  • Haskell 中的尾递归通常是一个坏主意,会导致代码变慢,尤其是在生成列表时。无论如何,即使它可能不是您想要的,您也可以在导入 Control.Monad 之后在 GHCi 中尝试 replicateM 5 "abc"
  • @chi 可能是尾递归 foldl 和惰性组合函数 is the right tool after all,严格、完全地构建 n “嵌套循环”,然后得到被懒惰地探索。 strict loop 来构建 lazy 嵌套循环...(Common Lisp 以使用宏为荣——首先构建代码,然后让它运行)

标签: haskell recursion functional-programming scheme sml


【解决方案1】:

那个,在 Haskell 中,至少是“实用的”和简洁的(我认为):

makeDomain :: [α] -> Int -> [[α]]
makeDomain xs 0  =  [[]]
makeDomain xs n  =  let  mdn1 = makeDomain xs (n-1)
                         fn x = map (x:) mdn1
                    in   concat (map fn xs)

试试看:

 λ> 
 λ> makeDomain [0,1] 2
[[0,0],[0,1],[1,0],[1,1]]
 λ> 
 λ> makeDomain [0,1] 3
[[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]]
 λ> 

正如 cmets 中所述,使用尾递归可能不是一个好主意,至少在 Haskell 中是这样。

补充:内存效率:

您没有在要求中列出性能问题(是因为您认为尾递归函数往往性能更好吗?)。

makeDomain 的上述版本,正如amalloy 在 cmets 中所暗示的那样,存在指数级内存消耗,至少对于某些编译器版本/优化级别而言。这是因为编译器可以将makeDomain xs (n-1) 视为要保留的循环不变值。

因此,这是您必须在优雅和效率之间进行权衡的情况之一。这个问题最近在这个related SO question 中在非常相似的replicateM 库函数的上下文中进行了讨论;根据 K. A. Buhr 的答案,可以利用 Haskell list comprehension 构造提出一种在恒定内存中运行的 makeDomain 版本。

makeDomain1 :: [α] -> Int -> [[α]]
makeDomain1 xs n =
    map reverse (helper xs n)
        where
            helper xs 0 = [[]]
            helper xs n = [ x:ys  |  ys <- helper xs (n-1),  x <- xs ]

测试:以操作系统强制的 1200 MB 内存硬限制运行。

 λ> 
 λ> import Control.Monad (replicateM)
 λ> replicateM 3 [0,1]
[[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]]
 λ> 
 λ> makeDomain1 [0,1] 3
[[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]]
 λ> 
 λ> length $ replicateM 30 [0,1]
<interactive>: internal error: Unable to commit 1048576 bytes of memory
...
 λ> 
 λ> length $ makeDomain [0,1] 30
<interactive>: internal error: Unable to commit 1048576 bytes of memory
...
 λ> 
 λ> length $ makeDomain1 [0,1] 30
1073741824
 λ> 

使用带有 -O2 选项的 GHC v8.6.5,最后一个版本占用的内存永远不会超过 150 MB,并且在普通 Intel x86-64 PC 上以每个输出列表大约 30 纳秒的速度运行。这是完全合理的。

【讨论】:

  • 在 Haskell 中,语言的惰性求值特性意味着尾递归与非尾递归相比没有真正的优势。
  • @VolodymyrProkopyuk 如果您的问题包含那些失败的尝试,它很可能不会收到那么多反对票,或者根本不会。你仍然可以编辑它们。:)
  • @WillNess 感谢您的支持!我想如果我寻求帮助是因为我需要它并且我自己已经做了很多尝试,而不是因为我很懒并且希望有人解决我的考试问题:)。无论如何,经过一些更深入的研究,我得出了我在下面提供的解决方案。
  • 参见。 thisthisthisthis。它的“缩小域”方面不适用,但“n 嵌套循环”适用。 (另外,更直接相关的是this)。这就是 Haskell 的 replicateM n digits 正在做的事情:它创建 n 个嵌套循环 for d1 in digits: for d2 in digits: ... for dn in digits: yield [d1,d2,...,dn]
  • 您能否也测试一下makeDomain2 xs n = map reverse . foldl (\xs x -&gt; liftA2 (flip (:)) xs x) (pure []) . replicate n $ xs 是否在恒定空间中运行?...(或者,就此而言,makeDomain1b xs n = map reverse . foldr (\x xs -&gt; liftA2 (flip (:)) xs x) (pure []) . replicate n $ xs 我怀疑它等同于列表理解...)。在 GHCi 中尝试它们,两者似乎都在恒定空间中运行,_2 比 _1b 快 2 倍。
【解决方案2】:

这是我对解决上述问题的建设性看法。

解决方案是在 Scheme 中功能性、简洁、递归(但不是尾递归)实现。

这个想法是域有一个归纳(递归)定义:域中的每个组合(第一个映射)是一对从初始数字字典中一对一提取的数字,所有组合都是较小的一维(第二张地图)

(define (make-domain digits dimension)
  "Builds all combinations of digits for a dimension"
  ;; There is an empty combination for a dimension 0
  (if [zero? dimension] '(())
      ;; Combine all combinations
      (apply append
             ;; For each digit from digits
             (map (lambda (d)
                    ;; Prepend the digit to each combination
                    ;; for a smaller by one dimension
                    (map (lambda (sd) (cons d sd))
                         (make-domain digits (1- dimension))))
                  digits))))

【讨论】:

    【解决方案3】:

    Your answer 可以通过使用累加器的常用技巧进行尾递归。以下是 Racket 而不是 Scheme,但可能只是因为它使用了 append*,我认为它可以定义为

    (define (append* . args)
      (apply append args))
    

    这是一个尾递归版本,因此:

    (define (make-domain digits dimension)
      (let mdl ([d dimension] [r '(())])
        (if (zero? d)
            r
            (mdl (- d 1)
                 (append* (map (λ (d)
                                 (map (λ (sd)
                                        (cons d sd))
                                      r))
                               digits))))))
    

    【讨论】:

    • 完美的尾递归解决方案!这是我对这个问题的最大期望!非常感谢!我学到了很多东西!
    【解决方案4】:

    为了完整起见,这里是翻译成标准 ML 的 Haskell 解决方案:

    fun curry f x y = f (x, y)
    fun concatMap f xs = List.concat (List.map f xs)
    
    fun makeDomain _ 0 = [[]]
      | makeDomain ys n =
        let val res = makeDomain ys (n-1)
        in concatMap (fn x => map (curry op:: x) res) ys
        end
    

    可以应用累加器的常用技巧来避免tfb 演示的n 堆栈帧。但正如amalloy 指出的那样,这几乎不是这个函数的瓶颈,它的内存使用指数因子为n。在标准 ML 变体中,过多的列表连接会花费更多。

    因此,根据您打算如何处理此列表,您可能需要考虑在标准 ML 中生成其元素并一次处理一个(就像惰性流允许您这样做);例如,您可以生成过滤后的列表,而不是生成一个长列表并对其进行过滤。这是一个示例:Translation of Pythagorean Triplets from Haskell to Standard ML

    【讨论】:

    • 惰性流、生成器或内部带有回调的嵌套循环,例如 Norvig 在其 PAIP 中的 Prolog 实现。
    猜你喜欢
    • 1970-01-01
    • 2012-03-22
    • 1970-01-01
    • 2022-01-24
    • 2014-02-28
    • 1970-01-01
    • 1970-01-01
    • 2022-01-18
    • 2019-07-24
    相关资源
    最近更新 更多