简体   繁体   English

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

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

Peter Norvig's PAIP book contains this code as a solution to the permutation problem (some sections are removed for brevity) 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)))

The part where 2 lambdas are involved is indeed brilliant yet a bit hard to comprehend as there are many moving parts intermingled into each other.涉及 2 个 lambda 的部分确实很棒,但有点难以理解,因为有许多相互混合的移动部分。 My questions are:我的问题是:

1- How to interpret those 2 lambdas properly? 1- 如何正确解释这 2 个 lambda? An explanation in detail is welcome.欢迎详细解释。

2- How did Norvig rightly infer that the first map function should be mapcan ? 2- Norvig 如何正确推断第一个 map 函数应该是mapcan

Optional : How did he in general think of such a short yet effective solution in the first place?可选:他最初是如何想到这么短而有效的解决方案的?

Apart from some small difference which has been explained above, the important thing is that mapcan and mapcar are loop functions.除了上面已经解释的一些小区别之外,重要的是mapcanmapcar循环函数。 So the double lambda can be simply interpreted as a loop within a loop.所以双lambda可以简单地解释为循环中的循环。

You could rewrite it as你可以把它改写为

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

In this skeleton you are still missing how to accumulate the results.在这个骨架中,您仍然缺少如何累积结果。 It could be done eg as它可以做,例如

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

The same is accomplished by mapcan and mapcar , much more elegantly, in Norvig's version.在 Norvig 的版本中, mapcanmapcar更优雅地完成了同样的任务。 But I hope this explanation makes it more clear to you.但我希望这个解释能让你更清楚。

Concerning question 2 (on mapcan):关于问题 2 (在 mapcan 上):

The Hyperspec says "mapcan..(is) like mapcar..except that the results of applying function are combined into a list by the use of nconc rather than list ." 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))

In the permutations function, if you had used mapcar instead of mapcan , you would have one more nesting layer for each of (permutations (remove e bag)) , which would make the resulting list "grouped".permutations函数中,如果您使用mapcar而不是mapcan ,您将为每个(permutations (remove e bag))多一个嵌套层,这将使结果列表“分组”。 To make this more clear, if you define a function permutations2 , which is exactly the same with permutations , just using mapcar in place of mapcan :为了使这更清楚,如果你定义一个函数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)))))

Therefore, the outer map function is mapcan , so that permutations returns a list of permutations (as the docstring says), and not a list of "groups" of permutations.因此,外部 map 函数是mapcan ,因此permutations返回permutations列表(如文档字符串所述),而不是排列“组”列表。

Concerning question 1 (on the lambdas):关于问题 1 (关于 lambdas):

In this case, the lambda-expressions look intermingled because they refer to variables defined outside of them, ie from the surrounding lexical environment (the first/outer refers to bag , the second/inner refers to e ).在这种情况下,lambda 表达式看起来是混合的,因为它们指的是在它们外部定义的变量,即来自周围的词法环境(第一个/外部指的是bag ,第二个/内部指的是e )。 In other words, to both mapcan and mapcar we are actually passing closures .换句话说,对于mapcanmapcar我们实际上是在传递闭包

Since the code has the strategy described in its comments, we need:由于代码在其注释中描述了策略,我们需要:

  1. To map over the elements of bag , which is what mapcan does here.映射bag的元素,这就是mapcan在这里所做的。 So we need a function, that takes as argument an element (e) of bag and does something (the role of the outer lambda function).所以我们需要一个函数,它将bag一个元素 (e) 作为参数并做一些事情(外部 lambda 函数的作用)。

  2. To map over the permutations of the remaining elements, which is what mapcar does here.映射剩余元素的排列,这就是mapcar在这里所做的。 So we need a function, that takes as argument a permutation (p) of (permutations (remove e bag)) and does something (the role of the inner lambda function).所以我们需要一个函数,它将(permutations (remove e bag))的排列 (p) 作为参数并做一些事情(内部 lambda 函数的作用)。

Concerning the optional question , just a trail of thoughts:关于可选问题,只是一些想法:

The docstring of permutations is "Return a list of all the permutations of the input." permutations的文档字符串是“返回输入的所有排列的列表”。

If we think of counting the n-permutations of n, we start by:如果我们考虑计算 n 的 n 排列,我们开始:

(number of options for 1st place) * (num of options for 2nd place) * ... * (num of options for nth place) (第一名的选项数)*(第二名的选项数)* ... *(第n名的选项数)

Which is :这是:

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

n! = n * (n-1)!

This way, we compute the factorial recursively, and the permutations function "translates" that in a way: The mapcan-part corresponds to n , and the mapcar-part, calling permutations recursively on the remaining elements, corresponds to (n-1)!这样,我们递归地计算阶乘,并且permutations函数以某种方式“翻译”: mapcan 部分对应于n ,而 mapcar 部分,在剩余元素上递归调用permutations ,对应于(n-1)! . .

Almost certainly Norvig's thinking is reflected in the code comments.几乎可以肯定,Norvig 的想法反映在代码注释中。 One of the main reasons for writing a recursive function definition is to avoid thinking about the details of the calculation.编写递归函数定义的主要原因之一是避免考虑计算的细节。 Writing recursive definitions allows you to focus on higher-level descriptions of what you want to do:编写递归定义可以让你专注于你想要做的事情的更高层次的描述:

If you want to find all permutations of a bag of elements, remove the first element from the bag, find all permutations of the remaining elements, and add the removed element to the front of those permutations.如果你想找到一个元素包的所有排列,从包中取出第一个元素,找到剩余元素的所有排列,然后将删除的元素添加到这些排列的前面。 Then remove the second element from the bag, find all permutations of the remaining elements, and add the removed element to the front of those permutations.然后从包中取出第二个元素,找到剩余元素的所有排列,并将删除的元素添加到这些排列的前面。 Continue until you have removed each element from the bag and collect all of the permutations in a list.继续直到您从包中取出每个元素并收集列表中的所有排列。

This is a pretty straightforward description of how you can generate all permutations of a bag of elements.这是关于如何生成元素包的所有排列的非常简单的描述。 How to convert that into code?如何将其转换为代码?

We can map a function over the bag that, for each element e of the bag, returns a list containing all but e , resulting in a list of lists:我们可以在包上映射一个函数,对于包的每个元素e ,返回一个包含除e所有元素的列表,从而得到一个列表列表:

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

But, for each one of the subsets we want to generate a list of permutations, and we want to cons e onto the front of each permutation.但是,对于每个子集,我们希望生成一个排列列表,并且我们希望将 cons e放在每个排列的前面。 I haven't defined permutations yet, so I will use list as a substitute (a list of permutations is a list of lists):我还没有定义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)))

The inner mapcar takes a list of permutations (only one permutation at the moment) and adds e to the front of each permutation.内部mapcar获取一个排列列表(目前只有一个排列)并在每个排列的前面添加e The outer mapcar iterates this process for each element in the bag, consing the results into a list.外部mapcar为包中的每个元素迭代这个过程,将结果放入一个列表中。 But, since the result of the inner mapcar is a list of permutations, the consed together results of the outer mapcar is a list of lists of permutations.但是,由于内部mapcar的结果是一个排列列表,因此外部mapcar合并结果是一个排列列表列表。 Instead of mapcar , mapcan can be used here to append the results of mapping.这里可以使用mapcan代替mapcar附加映射的结果。 That is, we really want to append the lists of permutations created by the inner mapcar together:也就是说,我们真的想将内部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))

Now we have a list of permutations with each element represented in the first position, but we need to get the rest of the permutations.现在我们有一个排列列表,每个元素表示在第一个位置,但我们需要获得其余的排列。 Instead of consing the elements e from the bag to a list that is only the bag with e removed, we need to cons the elements e to each permutation of the bag after e has been removed.我们需要将元素e从包中 cons 到一个列表,而不是将e删除后的包,而是在e被删除后将元素e cons 到包的每个排列中。 To do this we need to go ahead and define permutations , and we need to implement a base case: when the bag is empty, the list of permutations contains an empty bag:为此,我们需要继续定义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))

Now we are done;现在我们完成了; each element e from the bag has been consed onto the front of every permutation of the rest of the bag.袋子中的每个元素e都被放置在袋子其余部分的每个排列的前面。 Adding a call to print might help make the sequence of events more clear:添加对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))

Good code in a nice readable language doesn't need to be brilliant.用一种很好的可读语言编写的好的代码不需要非常出色。 It's better when it's just simple and self-evident.当它只是简单和不言而喻时会更好。

So let's re-write that 'brilliant' code down in some readable pseudocode and see if it clears up a bit.因此,让我们用一些可读的伪代码重写那个“出色”的代码,看看它是否变得清晰一点。

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

I rest my case.我休息一下。

Incidentally, now that the code's meaning has cleared up, we can see that the code is not quite right: it removes elements by value, while permutations belongs to combinatorics which is purely positional.顺便说一句,现在代码的含义已经清楚了,我们可以看到代码不太正确:它按值删除元素,而排列属于纯位置的组合。 (The second and third link below do that; the second link also contains a version directly corresponding to the one here). (下面的第二个和第三个链接就是这样做的;第二个链接还包含一个与此处的版本直接对应的版本)。

see also:也可以看看:


So what is really going on here is generation (nay yielding ) of elements of the result list one by one inside two nested loops .所以这里真正发生的是在两个嵌套循环一个一个地生成(不产生)结果列表的元素。 The use of mapcan = concat ... mapcar ... is just an implementational detail. mapcan = concat ... mapcar ...的使用只是一个实现细节。

Or we could use the M word, say that the essence of Monad is flatMap is mapcan , and its meaning generalized nested loops .或者我们可以用M这个词,说Monad的本质是flatMapmapcan ,它的含义概括了嵌套循环


A permutation of bag is a sequence with the same elements as bag , though possibly in a different order. bag的排列是具有与bag相同元素的序列,但可能顺序不同。

If we write bag as (e1 ... en) , then the set of all permutations contains all permutations where e1 is in first position, all the permutations where e2 is in first positions etc. and all the permutations where en is the first element.如果我们将 bag 写成(e1 ... en) ,那么所有排列的集合包含e1处于第一位的所有排列、 e2处于第一位等的所有排列以及en是第一个元素的所有排列.

This way of partitioning the permutations is what the algorithm exploits, by recursively computing all the permutations where each element is in first position.这种划分排列的方式是算法所利用的,通过递归计算每个元素处于第一个位置的所有排列。

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

The innermost lambda is interpreted as:最里面的lambda被解释为:

Given a permutation p , which is a list of values, return a new permutation with element e in front.给定一个排列p ,它是一个值列表,返回一个前面有元素e的新排列。

It is passed to mapcar for a list of permutations.它被传递给mapcar以获得排列列表。 So the meaning is of the call to mapcar is:所以调用mapcar的意思是:

Compute all the permutations of the subset obtained by removing e from bag .计算通过从bag移除e获得的子集的所有排列。 This gives a list of permutations, each one being a list of values that do not contain e .这给出了一个排列列表,每个排列都是一个不包含e的值列表。 For all of those lists, add e in front.对于所有这些列表,在前面添加e The result of mapcar is a list of permutations where e is the element in front of each permutation. mapcar的结果是一个排列列表,其中e是每个排列前面的元素。

So if you have a bag (1 2 3) , and if e is 1 , then first you remove 1 from the bag, which is (2 3) , you compute all the permutations recursively, which is ((2 3) (3 2)) , and for all the permutations in that list you add 1 in front of the permutations;所以如果你有一个包(1 2 3) ,如果e1 ,那么首先你从包中删除1 ,即(2 3) ,你递归地计算所有排列,即((2 3) (3 2)) ,并且对于该列表中的所有排列,您在排列前添加1 you obtain ((1 2 3) (1 3 2)) .你得到((1 2 3) (1 3 2))

Notice that this does not contain all the possible permutations of (1 2 3) .请注意,这不包含(1 2 3)所有可能排列。

You want also to compute the permutations where e is 2, so you remove 2 and compute the permutations, which are ((1 3) (3 1)) , and you add 2 in front, for another list of permutations, namely ((2 1 3) (2 3 1)) .您还想计算e为 2 的排列,因此您删除2并计算排列,即((1 3) (3 1)) ,然后在前面添加2 ,用于另一个排列列表,即((2 1 3) (2 3 1))

Finally, you also want to do the same when e is 3 .最后,当e3时,您也想这样做。 Let's skip the intermediate computations, the result is ((3 1 2) (3 2 1)) .让我们跳过中间计算,结果是((3 1 2) (3 2 1))

All the intermediate results are different permutations of (1 2 3) that cover, without duplicates, all the permutations of the initial bag:所有中间结果都是(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))

When we append all the lists together, we obtain the list of all the permutations of (1 2 3) :当我们将所有列表附加在一起时,我们获得(1 2 3)的所有排列的列表:

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

That's the purpose of the call to mapcan .这就是调用mapcan的目的。

(mapcan ... bag) compute different permutations of bag for each element of bag and append them to compute the complete set of permutations. (mapcan ... bag)计算的不同排列bag的每个元素bag和追加他们计算一套完整的排列。

I don't know how Norvig thought about writing this code in particular, but the recursive algorithms for computing permutations were already documented.我不知道 Norvig 特别是如何考虑编写这段代码的,但是用于计算排列的递归算法已经被记录在案。

See for example Permutation Generation Methods (R. Sedgewick, 1977) .参见例如排列生成方法 (R. Sedgewick, 1977) This paper is mostly concerned about computing permutations over vectors, not linked lists, and one of the best algorithm in this category (that minimizes swaps) is Heap's algorithm .本文主要关注计算向量的排列,而不是链表,该类别中最好的算法之一(最小化交换)是堆算法

For linked lists, I found this paper Functional Programs for Generating Permutations.对于链表,我找到了这篇论文Functional Programs for Generating Permutations。 by Topor in 1982 (PAIP was published in 1991). Topor 于 1982 年发表(PAIP 于 1991 年出版)。

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

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