[英]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
?
可选:他最初是如何想到这么短而有效的解决方案的?
除了上面已经解释的一些小区别之外,重要的是mapcan
和mapcar
是循环函数。 所以双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 的版本中, mapcan
和mapcar
更优雅地完成了同样的任务。 但我希望这个解释能让你更清楚。
关于问题 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
)。 换句话说,对于mapcan
和mapcar
我们实际上是在传递闭包。
由于代码在其注释中描述了策略,我们需要:
映射bag
的元素,这就是mapcan
在这里所做的。 所以我们需要一个函数,它将bag
一个元素 (e) 作为参数并做一些事情(外部 lambda 函数的作用)。
映射剩余元素的排列,这就是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的本质是flatMap
是mapcan
,它的含义概括了嵌套循环。
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)
,如果e
是1
,那么首先你从包中删除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))
。
最后,当e
为3
时,您也想这样做。 让我们跳过中间计算,结果是((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.