【问题标题】:How to generate all the permutations of elements in a list one at a time in Lisp?如何在 Lisp 中一次生成一个列表中元素的所有排列?
【发布时间】:2018-09-25 16:51:17
【问题描述】:

我已经有了为元素列表生成所有排列的代码。但是,我意识到如果我想操作生成的列表,我需要遍历这个列表。此列表可能非常庞大,因此保留成本很高。我想知道是否有办法通过每次调用生成排列,以便我可以检查列表是否与我需要的匹配,如果不匹配,我将生成下一个排列。 (每次函数都会一次返回一个列表。)

我的代码:

(defun allPermutations (list) 
  (cond
     ((null list)  nil) 
     ((null (cdr list))  (list list)) 
     (t  (loop for element in list 
               append (mapcar (lambda (l) (cons element l))
                              (allPermutations (remove element list))))))) 

【问题讨论】:

    标签: list lisp common-lisp generator permutation


    【解决方案1】:

    这是一种方法(遵循 @coredumptheir answer 的代码结构;在 tio.run 上运行速度大约快 4 倍):

    (defun permutations (list callback)
      (when list
        (let* ((all (cons 'head (copy-list list)))           ; head sentinel FTW!
               (perm (make-array (length list))))
          (labels ((g (p i &aux (q (cdr p)))
                    (cond
                      ((null (cdr q))   
                         (setf (svref perm i) (car q))       ; the last item
                         (funcall callback perm))
                      (T (loop while q do 
                            (setf (svref perm i) (car q))    ; pick the item
                            (rplacd p (cdr q))               ; pluck it out
                            (g all (1+ i))                   ; recurse!
                            (rplacd p q)                     ; heal the list back
                            (pop p)  
                            (pop q))))))                     ; advance the pointers
            (g all 0))))) 
    
    ; > (permutations '(1 2 3) #'princ)
    ; #(1 2 3)#(1 3 2)#(2 1 3)#(2 3 1)#(3 1 2)#(3 2 1)
    

    这使用递归为 n 长的输入列表构建一个 n 嵌套循环 结构,在运行时使用固定的 i = 0, 1 , ..., n-1 在每个嵌套循环中作为结果保存permutation 数组中的位置,以放入选取的项目。当数组中的所有 n 个位置都被填满时,在最里面的循环中(它甚至不再是一个循环,因为它只剩下一个元素要处理),调用用户提供的回调以 perm 数组作为参数。该数组被重复用于每个新的排列。

    在此伪代码中实现“缩小域”范式:

    for item1 in list:
       domain2 = remove item1 from list by position
       for item2 in domain2:
          domain3 = remove item2 domain2 by position
          for item3 in domain3:
                 ......
                 ......
                 (callback (list item1 item2 ... item_n))
    

    但在the real code 中,我们通过手术操作列表结构,完全 取消了此伪代码使用的所有 二次 临时存储。链表的唯一优点是它们的O(1) 节点删除能力;我们不妨使用它!

    update: 对排列的最后一个 两个 元素进行特殊处理(通过将最后一个循环展开到相应的两个回调调用中)得到大约 1.5 x 额外加速。

    (以防 the TIO link 腐烂,这里是 the pastebin 的工作代码。)

    更新:这种技术被称为,通过递归创建n嵌套循环回溯计算结构

    【讨论】:

    • 不错的方法,我看到您更改了答案以避免对每个排列都进行欺骗。干得好。
    【解决方案2】:

    一般原则

    假设你有以下range函数:

    (defun range (start end &optional (step 1))
      (loop for x from start below end by step collect x))
    

    您可以接受另一个参数,一个函数,并为每个元素调用它:

    (defun range-generator (callback start end &optional (step 1))
      (loop for x from start below end by step do (funcall callback x)))
    

    这使调用者可以控制迭代过程:

    (block root
      (range-generator (lambda (v)
                         (print v)
                         (when (>= v 10)
                           (return-from root)))
                       0 300))
    
    
    0 
    1 
    2 
    3 
    4 
    5 
    6 
    7 
    8 
    9 
    10
    

    请参阅RETURNBLOCK

    排列

    如果您想避免分配过多的内存,您可以安排代码分配中间数据结构一次,并在每次调用回调时重复使用它们。这是一个带注释的示例:

    (defun permutations% (list callback)
      (when list
        (let* (;; Size of input list
               (size (length list))
    
               ;; EMPTY is a sentinel value which is guaranteed to
               ;; never be equal to any element from LIST.
               (empty (gensym "SENTINEL"))
    
               ;; Working vector containing elements from LIST, or
               ;; EMPTY. This vector is mutated to remember which
               ;; element from the input LIST was already added to the
               ;; permutation.
               (items (make-array size :initial-contents list))
    
               ;; Working vector containing the current
               ;; permutation. It contains a FILL-POINTER so that we
               ;; can easily call VECTOR-PUSH and VECTOR-POP to
               ;; add/remove elements.
               (permutation (make-array (length items) :fill-pointer 0)))
    
          ;; Define a local recursive function named POPULATE, which
          ;; accepts a COUNT argument. The count starts at SIZE and
          ;; decreases at each recursive invocation, allowing the
          ;; function to know when it should end.
          (labels ((populate (count)
                     (if (plusp count)
                         ;; Loop over ITEMS by index
                         (dotimes (item-index size)
                           (let ((item (svref items item-index)))
                             ;; We found an ITEM which is not yet
                             ;; present in PERMUTATION.
                             (unless (eq item empty)
                               ;; Push that element
                               (vector-push item permutation)
                               ;; Replace current value in ITEMS by EMPTY
                               (setf (svref items item-index) empty)
    
                               ;; POPULATE will recursively populate
                               ;; the remaining elements in
                               ;; PERMUTATION and call CALLBACK. Once
                               ;; it is done, it will return here.
                               (populate (1- count))
    
                               ;; There are other items to process in
                               ;; current loop. Reset the state to how
                               ;; it was before calling POPULATE.
    
                               ;; Replace the EMPTY value by the
                               ;; original ITEM at current index.
                               (setf (svref items item-index) item)
    
                               ;; Remove ITEM from PERMUTATION.
                               (vector-pop permutation))))
    
                         ;; We filled PERMUTATION with SIZE elements.
                         ;; Call CALLBACK with PERMUTATION. Note: the
                         ;; callback function is always given the same
                         ;; vector, but its content changes over
                         ;; time. The value passed to CALLBACK is thus
                         ;; valid only during the time we are
                         ;; executing CALLBACK. If the caller needs to
                         ;; keep a copy of the current permutation, it
                         ;; should COPY-LIST the value.
                         (funcall callback permutation))))
    
            ;; Initiate recursive function with current SIZE.
            (populate size)))))
    

    该函数接受一个列表和一个回调,这是一个接受一个参数的函数,即当前排列。请注意,此参数仅在调用的dynamic extent 期间有效,因为一旦调用返回,将修改传递给回调的相同数据结构。

    如上所述,您可以调用任何函数,特别是闭包,它引用词法环境中的其他变量。在这里,匿名 lambda 增加了 count 变量,它允许计算排列的数量,而无需将它们存储在列表中并获取列表的大小:

    (time
     (let ((count 0))
       (permutations% '(a b c d e f g h i j k) (lambda (p) (incf count)))
       count))
    => 39916800
    
    Evaluation took:
      6.455 seconds of real time
      6.438200 seconds of total run time (6.437584 user, 0.000616 system)
      99.74% CPU
      17,506,444,509 processor cycles
      0 bytes consed
    

    在上面的报告中,0 bytes consed代表了大约分配的内存数量(不包括堆栈分配)。 您还可以提供更安全的函数版本,在将每个排列发送到回调函数之前复制它。

    (defun permutations (list callback)
      (permutations% list (lambda (permutation)
                            (funcall callback (coerce permutation 'list)))))
    

    另见

    另请参阅the answer from Will Ness,它设法使用列表处理剩余元素的集合,从而避免过滤 EMPTY 元素的需要。

    【讨论】:

    • 我已根据他们的评论将 OP 的代码编辑到问题中。很难看出如何应用您的一般准则来获得答案。如果我们将return-from 插入到 mapcar 的 lambda 函数中,则仍需要完整地创建被映射的列表。一种解决方案似乎是为长度为 n 的参数列表创建 n 嵌套循环(从 deepest 级别返回第一个可接受的排列);但如何?编写宏是实现这一目标的唯一方法吗?
    • @WillNess (1) 删除附加,只是“做”。 (2) 将回调包装在一个 lambda 中,它接收一个排列并构建下一个排列,并最终调用原始回调。你会得到一堆相互引用的回调函数,它们也将分配,但它原则上是有效的。
    • 好的,是的,您确实使用递归创建了嵌套循环结构,这很好。你的EMPTY 标记技巧虽然感觉有点像作弊,太临时了。 :) 我期待看到像您一样操纵索引和从它们创建的排列,是的,但我也希望看到缩小的域,因为我们在下降的过程中一一挑选项目。再想一想,我现在看到这一切都是通过手术列表操作完成的,通过 consing 构建排列。我认为,将更新后的状态传递到递归深度 BTW 会给我们一个大致的 Prolog 实现。
    • 我认为这是一个很好的、有趣的问题。我希望它至少没有被打负分。 ---重新进行手术操作,它会遵循您的代码结构:从列表中提取一个项目,将其转换为正在构建的排列,递归,取消,将项目恢复到列表中的原始位置。将需要为此保存一些指针(单元格)。当然,从复制的列表开始递归,也可能在前面加上一个头哨兵来简化编码。有趣的东西!
    • @WillNess Re 缩小域:我可以使用预先分配的列表,并在递归时指向其中的一个 cons-cell;这将需要在该列表中旋转元素。我首先想到了向量方法。我看不出 gensym 有什么问题,我需要一个新对象,这可能是一个新的缺点或哈希表,但 gensym 工作正常。
    猜你喜欢
    • 2018-05-21
    • 1970-01-01
    • 2016-01-20
    • 2013-11-07
    • 2023-04-02
    • 1970-01-01
    • 2020-08-18
    • 2018-12-10
    相关资源
    最近更新 更多