[英]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.