【问题标题】:Permutation of an array by recursion, when does the second element swap take place?通过递归排列数组,第二个元素交换何时发生?
【发布时间】:2022-01-04 08:52:09
【问题描述】:

这个排列程序中的第二个元素交换是如何工作的? 第二次交换发生在程序的哪一步?在第一次递归之后还是在所有递归完成之后?

def permutations(A: List[int]) -> List[List[int]]:
    def directed_permutations(i):
        if i == len(A) - 1:
            result.append(A.copy())
            return  

        # Try every possibility for A[i]
        for j in range(i, len(A)):
            A[i], A[j] = A[j], A[i]
            # Generate all permutations for A[i + 1:]
            directed_permutations(i + 1)
            # Second element swap, which I do not understand
            A[i], A[j] = A[j], A[i]

    result: List[List[int]] = []
    directed_permutations(0)
    return result

当我离开第二个交换并尝试 A = [2,3,5,7] 时,我得到 4!元素,但有重复:

[[2, 3, 5, 7], [2, 3, 7, 5], [2, 7, 3, 5], [2, 7, 5, 3], [2, 3, 5 , 7], [2, 3, 7, 5], [3, 2, 7, 5], [3, 2, 5, 7], [3, 5, 2, 7], [3, 5, 7 , 2], [3, 2, 7, 5], [3, 2, 5, 7], [5, 2, 3, 7], [5, 2, 7, 3], [5, 7, 2 , 3], [5, 7, 3, 2], [5, 2, 3, 7], [5, 2, 7, 3], [3, 2, 7, 5], [3, 2, 5 , 7], [3, 5, 2, 7], [3, 5, 7, 2], [3, 2, 7, 5], [3, 2, 5, 7]]

通过第二次交换,我得到了正确的 4!元素:

[[2, 3, 5, 7], [2, 3, 7, 5], [2, 5, 3, 7], [2, 5, 7, 3], [2, 7, 5 , 3], [2, 7, 3, 5], [3, 2, 5, 7], [3, 2, 7, 5], [3, 5, 2, 7], [3, 5, 7 , 2], [3, 7, 5, 2], [3, 7, 2, 5], [5, 3, 2, 7], [5, 3, 7, 2], [5, 2, 3 , 7], [5, 2, 7, 3], [5, 7, 2, 3], [5, 7, 3, 2], [7, 3, 5, 2], [7, 3, 2 , 5], [7, 5, 3, 2], [7, 5, 2, 3], [7, 2, 5, 3], [7, 2, 3, 5]]

【问题讨论】:

  • 这有点难以理解,因为递归函数正在修改可变数据结构。与不可变数据结构一起使用时,递归感觉要自然得多。在for 循环中发生的情况是,我们想尝试directed_permutations(i+1),将每个可能的元素 A[j] 移动到位置 A[i]。因此,首先我们尝试交换 A[i] 和 A[i]。然后我们撤消交换并尝试交换 A[i] 和 A[i+1]。然后我们撤消交换并尝试交换 A[i] 和 A[i+2]。等等
  • 所以,“第二次交换”是第一次交换的“撤消”。
  • @Fortinbras 我同意 Stef 的观点,递归最适合用于持久数据结构。我想你会发现 this Q&A 很有帮助。

标签: python algorithm recursion permutation


【解决方案1】:

递归的规则是(几乎总是)你不要让子问题中的状态变化改变父状态。每个调用框架不应该知道或关心在(可能是遥远的)子调用中发生了什么,从而保留递归自相似性。参数数据结构应该是有效的不可变的。

后递归调用交换通过执行调用前交换的撤消来实现不变性,将列表返回到循环迭代之前的状态。这种状态恢复对于正确性至关重要,因为没有它,循环的下一次迭代(以及之后的每一次迭代)将有一个列表,该列表已被先前迭代的任意嵌套子调用修改,从而破坏了每个循环的有条不紊的、枚举的方法递归调用帧将 i 处的元素与每个元素 j 交换,然后在列表的其余部分上递归,从 i + 1 开始,0..i 固定用于其余的子调用。

为了表明不变性是关键因素,而撤消交换只是实现这一点的一种手段,您可以编写算法,使其在作为参数传递时复制参数列表:

from typing import List

def permutations(A: List[int]) -> List[List[int]]:
    result = []

    def directed_permutations(A, i=0):
        if i == len(A) - 1:
            result.append(A)
        else:
            for j in range(i, len(A)):
                B = A[:]
                B[i], B[j] = B[j], B[i]
                directed_permutations(B, i + 1)

    directed_permutations(A)
    return result

if __name__ == "__main__":
    print(permutations([1, 2, 3]))

请注意,我们在附加到结果时不再需要复制,因为现在每个帧都有一个唯一的列表来操作,而无需担心混叠。不过,与原始方法相比,这种方法会产生更多的复制开销。

这不是重点,但我会为这样的算法返回一个生成器,就像 itertools 所做的那样。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-01-05
    • 1970-01-01
    • 1970-01-01
    • 2021-03-14
    • 1970-01-01
    • 2021-04-03
    • 1970-01-01
    相关资源
    最近更新 更多