簡體   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