【问题标题】:Understanding Peter Norvig's permutation solution in PAIP了解 Peter Norvig 在 PAIP 中的置换解决方案
【发布时间】:2020-04-07 22:43:17
【问题描述】:

Peter Norvig 的 PAIP 书籍包含此 code 作为排列问题的解决方案(为简洁起见,删除了某些部分)

(defun permutations (bag)
  ;; If the input is nil, there is only one permutation:
  ;; nil itself
  (if (null bag)
      '(())
      ;; Otherwise, take an element, e, out of the bag.
      ;; Generate all permutations of the remaining elements,
      ;; And add e to the front of each of these.
      ;; Do this for all possible e to generate all permutations.
      (mapcan #'(lambda (e)
                  (mapcar #'(lambda (p) (cons e p))
                          (permutations (remove e bag))))
              bag)))

涉及 2 个 lambda 的部分确实很棒,但有点难以理解,因为有许多活动部分相互混合。我的问题是:

1- 如何正确解释这 2 个 lambda?欢迎详细解释。

2- Norvig 如何正确推断出第一个 map 函数应该是 mapcan

可选:他最初是如何想到这样一个简短而有效的解决方案的?

【问题讨论】:

  • 我很确定,我无法告诉您 Peter Norvig 的想法 ;-) 但是如果您查看 mapcan 的文档并在代码中将其替换为 @987654326 @你会很清楚地看到差异。此外,如果您 trace permutations 您会看到 lambdas 按照评论中的说明工作。
  • 感谢您的评论。老实说,mapcan 的文档并没有多大帮助,因为它没有显示它的任何实际用例。 trace 也没有多大帮助,因为它只显示了permutations 的两次调用,一次是输入,一次是最终输出,即它没有显示mapcanmapcar 的各个进程。唯一有用的是将mapcan 替换为mapcar,因为它显示了进展,但它再次没有清楚地显示这两个lambdas 如何完美和谐地工作以产生正确的输出,也没有解释何时使用mapcan
  • 你的输入数据是什么?如果您从(a b) 之类的简单测试用例开始,然后增加到(a b c),它应该会在跟踪中显示差异。
  • 对于'(a b)的输入,跟踪输出为:1. Trace: (PERMUTATION '(A B))1. Trace: PERMUTATION ==> ((A B) (B A)),即只有输入和输出重复。
  • 它是clisp,它只为 2 个元素输入 '(a b) 提供了 2 个跟踪输出,显然它有问题,所以我在 sbcl 上尝试了 3 个元素输入 '(a b c)它提供了 31 行跟踪输出,信息量很大。 Here it is谢谢你的评论,很有帮助。

标签: recursion functional-programming lisp common-lisp permutation


【解决方案1】:

除了上面已经解释的一些小区别之外,重要的是mapcanmapcar循环 函数。所以 double lambda 可以简单地解释为循环中的循环。

你可以改写成

(dolist (e bag)
  (dolist (p (permutations (remove e bag)))
    (cons e p) ))

在这个骨架中,您仍然缺少如何累积结果。可以这样做,例如作为

(defun permutations (bag) 
  (if (null bag)  (list bag) 
    (let*  ((res (list 1))  (end res))
       (dolist  (e  bag  (cdr res))
           (dolist  (p  (permutations (remove e bag)))
               (rplacd  end  (list (cons e p)))
               (pop end))))))

mapcanmapcar 在 Norvig 的版本中更优雅地完成了同样的任务。但我希望这个解释能让你更清楚。

【讨论】:

    【解决方案2】:

    关于问题2(在mapcan上):

    Hyperspec 说“mapcan..(is) like mapcar..除了应用 function 的结果通过使用 nconc 而不是组合成一个列表列表。”

    (mapcan #'identity '((1 2 3) (4 5 6))) ;=> (1 2 3 4 5 6)
    (mapcar #'identity '((1 2 3) (4 5 6))) ;=> ((1 2 3) (4 5 6))
    

    permutations 函数中,如果您使用mapcar 而不是mapcan,则每个(permutations (remove e bag)) 都会有一个嵌套层,这将使结果列表“分组”。为了更清楚一点,如果你定义一个函数permutations2,它和permutations完全一样,只是用mapcar代替mapcan

    (permutations '(1 2 3))  ;=> ((1 2 3) (1 3 2) (2 1 3) (2 3 1) (3 1 2) (3 2 1))
    (permutations2 '(1 2 3)) 
    ;=> (((1 (2 (3))) (1 (3 (2)))) ((2 (1 (3))) (2 (3 (1)))) ((3 (1 (2))) (3 (2 (1)))))
    

    因此,外部映射函数是mapcan,因此permutations 返回排列列表(如文档字符串所述),而不是排列“组”列表。

    关于问题 1(关于 lambda):

    在这种情况下,lambda 表达式看起来是混合的,因为它们引用定义在它们之外的变量,即来自周围的词法环境(第一个/外部指的是bag,第二个/内部指的是e) .换句话说,对于mapcanmapcar,我们实际上是在传递闭包

    由于代码有其cmets中描述的策略,我们需要:

    1. 映射bag 的元素,这就是mapcan 在这里所做的。所以我们需要一个函数,它将bag 的元素 (e) 作为参数并做一些事情(外部 lambda 函数的作用)。

    2. 映射剩余元素的排列,这就是mapcar 在这里所做的。所以我们需要一个函数,它以(permutations (remove e bag)) 的排列 (p) 作为参数并做一些事情(内部 lambda 函数的作用)。

    关于可选问题,只是一些想法:

    permutations 的文档字符串是“返回输入的所有排列的列表。”

    如果我们想计算 n 的 n 次排列,我们可以这样开始:

    (第一名的选项数量)*(第二名的选项数量)* ... *(第 n 名的选项数量)

    这是:

    n * (n-1) * ...* 2 * 1 = n!

    n! = n * (n-1)!

    这样,我们递归地计算阶乘,permutations 函数以某种方式“翻译”它:mapcan-part 对应于n,而 mapcar-part 在 剩余个元素,对应(n-1)!

    【讨论】:

    • 感谢您的回答。它写得很好,而且信息量很大,但它似乎基本上回顾了代码已经很明显的含义,并没有充分展示这两个 lambdas 的内部工作原理并解释背后的“基本原理”。在使用mapcar 代替mapcan 的示例中似乎存在错误,在我看来,它给出了(((1 (2 (3))) (1 (3 (2)))) ((2 (1 (3)))...,即带有更多括号。
    • 感谢您指出代码结果中的错误(编辑了我的答案)。关于 lambda 的内部工作原理和基本原理,您是指一般情况(例如为什么要使用 lambda 表达式、替代方法等),还是更多 trace 解释?
    【解决方案3】:

    几乎可以肯定,Norvig 的思想反映在代码 cmets 中。编写递归函数定义的主要原因之一是避免考虑计算的细节。编写递归定义可以让您专注于对您想要做的事情的更高层次的描述:

    如果你想找到一个元素包的所有排列,从包中移除第一个元素,找到剩余元素的所有排列,然后将移除的元素添加到这些排列的前面。然后从包中取出第二个元素,找到剩余元素的所有排列,并将取出的元素添加到这些排列的前面。继续,直到您从袋子中删除每个元素并将所有排列收集到一个列表中。

    这是对如何生成一袋元素的所有排列的非常简单的描述。如何将其转换为代码?

    我们可以在包上映射一个函数,对于包的每个元素e,返回一个包含除e 之外的所有元素的列表,从而生成一个列表列表:

    CL-USER> (let ((bag '(a b c)))
               (mapcar #'(lambda (e) (remove e bag)) bag))
    ((B C) (A C) (A B))
    

    但是,对于每个子集,我们想要生成一个排列列表,并且我们想要在每个排列的前面加上 e。我还没有定义permutations,所以我将使用list 作为替代(排列列表是列表的列表):

    CL-USER> (let ((bag '(a b c)))
               (mapcar #'(lambda (e)
                           (mapcar #'(lambda (p) (cons e p))
                                   (list (remove e bag))))
                       bag))
    (((A B C)) ((B A C)) ((C A B)))
    

    内部mapcar 获取一个排列列表(目前只有一个排列)并将e 添加到每个排列的前面。外部mapcar 为包中的每个元素迭代此过程,将结果组合到一个列表中。但是,由于内部mapcar 的结果是一个排列列表,所以外部mapcar 的结果是一个排列列表的列表。此处可以使用mapcan 代替mapcar追加映射结果。也就是说,我们真的想将内部 mapcar 创建的排列列表附加在一起:

    CL-USER> (let ((bag '(a b c)))
               (mapcan #'(lambda (e)
                           (mapcar #'(lambda (p) (cons e p))
                                   (list (remove e bag))))
                       bag))
    ((A B C) (B A C) (C A B))
    

    现在我们有一个排列列表,其中每个元素都表示在第一个位置,但我们需要获取其余的排列。与其将包中的元素 e 转换为仅删除了 e 的包的列表,不如将元素 e 转换为在删除 e 后包的每个排列。为此,我们需要继续定义permutations,并且我们需要实现一个基本情况:当包为空时,排列列表包含一个空包:

    CL-USER> (defun permutations (bag)
               (if (null bag)
                   '(())
                   (mapcan #'(lambda (e)
                               (mapcar #'(lambda (p) (cons e p))
                                       (permutations (remove e bag))))
                           bag)))
    PERMUTATIONS
    
    CL-USER> (permutations '(a b c))
    ((A B C) (A C B) (B A C) (B C A) (C A B) (C B A))
    

    现在我们完成了;袋子中的每个元素e 都被放在袋子其余部分的每个排列的前面。添加对print 的调用可能有助于使事件顺序更加清晰:

    CL-USER> (defun permutations (bag)
               (if (null bag)
                   '(())
                   (mapcan #'(lambda (e)
                               (let ((perms (mapcar #'(lambda (p) (cons e p))
                                                    (permutations (remove e bag)))))
                                 (print perms)
                                 perms))
                           bag)))
    PERMUTATIONS
    
    CL-USER> (permutations '(a b c))
    ((C)) 
    ((B C)) 
    ((B)) 
    ((C B)) 
    ((A B C) (A C B)) 
    ((C)) 
    ((A C)) 
    ((A)) 
    ((C A)) 
    ((B A C) (B C A)) 
    ((B)) 
    ((A B)) 
    ((A)) 
    ((B A)) 
    ((C A B) (C B A)) 
    ((A B C) (A C B) (B A C) (B C A) (C A B) (C B A))
    

    【讨论】:

      【解决方案4】:

      bag 的排列是一个与bag 具有相同元素的序列,尽管顺序可能不同。

      如果我们把bag写成(e1 ... en),那么所有排列的集合包含e1在第一位的所有排列,e2在第一位的所有排列等等,以及@987654330的所有排列@ 是第一个元素。

      算法利用这种划分排列的方式,通过递归计算每个元素位于第一个位置的所有排列。

      (defun permutations (bag)
        (if (null bag)
            '(())
            (mapcan #'(lambda (e)
                        (mapcar #'(lambda (p) (cons e p))
                                (permutations (remove e bag))))
                    bag)))
      

      最里面的lambda

      #'(lambda (p) (cons e p)
      

      被解释为:

      给定一个排列p,它是一个值列表,返回一个新排列,前面有元素e

      它被传递给mapcar 以获得排列列表:

      (mapcar #'(lambda (p) (cons e p))
                (permutations (remove e bag)))
      

      调用mapcar的含义如下:

      计算从bag 中删除e 获得的子集的所有排列。这给出了一个排列列表,每个排列都是一个不包含e 的值列表。对于所有这些列表,请在前面添加 emapcar 的结果是一个排列列表,其中e 是每个排列前面的元素。

      更具体地说,如果你有一个包(1 2 3),并且如果e1,那么首先你从包中删除1,即(2 3),你递归地计算所有排列,这是((2 3) (3 2)),对于该列表中的所有排列,您在排列前面添加1;你得到((1 2 3) (1 3 2))

      现在,请注意,这并不包含 所有(1 2 3) 的可能排列。

      您还想计算 e 为 2 的排列,因此删除 2 并计算排列,即 ((1 3) (3 1)),然后在前面添加 2,以获得另一个排列列表,即((2 1 3) (2 3 1)).

      最后,当e3 时,您也想这样做。让我们跳过中间计算,结果是((3 1 2) (3 2 1))

      所有中间结果都是(1 2 3) 的不同排列,没有重复地覆盖了初始包的所有排列:

      e = 1 : ((1 2 3) (1 3 2))
      e = 2 : ((2 1 3) (2 3 1))
      e = 3 : ((3 1 2) (3 2 1))
      

      当我们将所有列表附加在一起时,我们得到(1 2 3)的所有排列的列表:

      ((1 2 3) (1 3 2)
       (2 1 3) (2 3 1)
       (3 1 2) (3 2 1))
      

      这就是调用mapcan 的目的:(mapcan ... bag)bag 的每个元素计算bag 的排列列表,并将它们附加以计算完整的排列集。

      我不知道 Norvig 是如何特别考虑编写这段代码的,但是用于计算排列的递归算法已经记录在案了。

      参见例如Permutation Generation Methods (R. Sedgewick, 1977)。这篇论文主要关注的是计算向量的排列,而不是链表,这个类别中最好的算法之一(最小化交换)是Heap's algorithm

      对于链表,我在 1982 年找到了 Topor 的这篇论文 Functional Programs for Generating Permutations.(PAIP 发表于 1991 年)。

      【讨论】:

      • 还有this。 :)
      • 啊,我记得
      【解决方案5】:

      用可读性好的语言编写的好代码不需要很出色。简单明了就更好了。

      所以让我们用一些可读的伪代码重写那个“出色”的代码,看看它是否清楚一点。

      (permutations [])  =  [ [] ]
      (permutations bag) = 
      
        = (mapcan #'(lambda (e)
                        (mapcar #'(lambda (p) (cons e p))
                                (permutations (remove e bag))))
                    bag)
      
        = (concat    ;; (concat list) = (reduce #'nconc list)
           (mapcar #'(lambda (e)
                        (mapcar #'(lambda (p) (cons e p))
                                (permutations (remove e bag))))
                    bag))
      
        = concat  { FOR e IN bag :
                      YIELD { FOR p IN (permutations (remove e bag)) :
                                YIELD [e, ...p] } }
      
        =         { FOR e IN bag :
                            { FOR p IN (permutations (remove e bag)) :
                                YIELD [e, ...p] } }
      
        = [[e,...p] FOR e IN bag,
                              FOR p IN (permutations (remove e bag)) ]
      
        = (loop  for e in bag   nconc               ;; appending
              (loop  for p in (permutations (remove e bag))
                   nconc   (list (cons e p)) ))
              ;;   collect       (cons e p)
      

      我休息一下。

      顺便说一句,现在代码的含义已经弄清楚了,我们可以看到代码不太正确:它按值删除元素,而 permutations 属于纯粹位置的组合。 (下面的第二个和第三个链接是这样做的;第二个链接也包含与此处的版本直接对应的版本)。

      另见:


      所以真正在这里发生的是结果列表中的元素的生成(不,yielding一个一个在两个嵌套循环mapcan = concat ... mapcar ... 的使用只是一个实现细节。

      或者我们可以用M这个词,说Monad的本质是flatMapmapcan,及其含义广义嵌套循环


      【讨论】:

        猜你喜欢
        • 2018-04-13
        • 1970-01-01
        • 2022-10-31
        • 2023-04-09
        • 1970-01-01
        • 1970-01-01
        • 2021-01-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多