繁体   English   中英

在 PAIP 中了解 Peter Norvig 的置换解决方案

[英]Understanding Peter Norvig's permutation solution in PAIP

Peter Norvig 的 PAIP 书包含此 代码作为置换问题的解决方案(为简洁起见,删除了某些部分)

(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

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

除了上面已经解释的一些小区别之外,重要的是mapcanmapcar循环函数。 所以双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))))))

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

关于问题 2 (在 mapcan 上):

Hyperspec 说“mapcan ..(is) 像 mapcar.. 不同之处在于应用函数的结果通过使用ncconc而不是list组合成一个列表。”

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

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

关于问题 1 (关于 lambdas):

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

由于代码在其注释中描述了策略,我们需要:

  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 部分对应于n ,而 mapcar 部分,在剩余元素上递归调用permutations ,对应于(n-1)! .

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

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

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

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

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

但是,对于每个子集,我们希望生成一个排列列表,并且我们希望将 cons 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从包中 cons 到一个列表,而不是将e删除后的包,而是在e被删除后将元素e cons 到包的每个排列中。 为此,我们需要继续定义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))

用一种很好的可读语言编写的好的代码不需要非常出色。 当它只是简单和不言而喻时会更好。

因此,让我们用一些可读的伪代码重写那个“出色”的代码,看看它是否变得清晰一点。

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

我休息一下。

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

也可以看看:


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

或者我们可以用M这个词,说Monad的本质是flatMapmapcan ,它的含义概括了嵌套循环


bag的排列是具有与bag相同元素的序列,但可能顺序不同。

如果我们将 bag 写成(e1 ... en) ,那么所有排列的集合包含e1处于第一位的所有排列、 e2处于第一位等的所有排列以及en是第一个元素的所有排列.

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

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

最里面的lambda被解释为:

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

它被传递给mapcar以获得排列列表。 所以调用mapcar的意思是:

计算通过从bag移除e获得的子集的所有排列。 这给出了一个排列列表,每个排列都是一个不包含e的值列表。 对于所有这些列表,在前面添加e mapcar的结果是一个排列列表,其中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 特别是如何考虑编写这段代码的,但是用于计算排列的递归算法已经被记录在案。

参见例如排列生成方法 (R. Sedgewick, 1977) 本文主要关注计算向量的排列,而不是链表,该类别中最好的算法之一(最小化交换)是堆算法

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM