既然你踩到了 N = 5 (!),那我就试试这个。
除了 Fuchs 对递归的 [需要引用] 态度之外,我认为解释正在发生的事情的一种方法是他修改了以下递归算法。我希望这对你来说不是魔法。
def permute0(lst, n):
if n < 2:
print(lst)
else:
for j in range(n - 1, -1, -1): # j in [0, n) descending
# generate permutations with lst[n - 1] fixed
lst[j], lst[n - 1] = lst[n - 1], lst[j] # swap
permute0(lst, n - 1)
lst[j], lst[n - 1] = lst[n - 1], lst[j] # swap back
第一个优化是,在内部堆栈帧中,只需将变量j 的值存储在他调用的数组p 中。我不会打扰那个。
第二个优化是他做了一个交换切除术。我们可以通过不与自身交换元素来稍微减少交换。
def permute1(lst, n):
if n < 2:
print(lst)
else:
permute1(lst, n - 1)
for j in range(n - 2, -1, -1):
lst[j], lst[n - 1] = lst[n - 1], lst[j]
permute1(lst, n - 1)
lst[j], lst[n - 1] = lst[n - 1], lst[j]
为了进一步减少交换次数,我们必须更加聪明。 permute2 与之前的版本不同,在返回之前不会恢复lst。 permute2 不是递归的,因为它使用 permute1 来完成脏活。
def permute2(lst, n):
if n < 2:
print(lst)
else:
permute1(lst, n - 1)
for j in range(n - 2, -1, -1):
lst[j], lst[n - 1] = lst[n - 1], lst[j]
permute1(lst, n - 1)
permute2 对lst 做了什么?忽略对permute1 的调用,使lst 保持不变,它将lst 向左旋转一个元素。现在我们可以写permute2a,它调用permute2并寻找下一个元素来交换到lst[n - 1],permute2放它,但我们不能写一整座permutes。
我们需要一个关于我们尚未存在的permute 函数行为的归纳假设,我们可以用它来证明下一个函数。 这是一种创造性的行为,也是计算机科学中许多“魔法”的来源。我要写的可能不是对我的想法的真实描述。
我们正在研究的算法类别有两个自然约束。首先,这些算法使交换次数最少。其次,它们是自相似的,并且在移动最后一个元素之前耗尽了前 N - 1 个元素的所有排列。这些约束一起强制将交换的元素之一存储在与 Fuchs 的i 对应的位置。
N = 0: there's only one permutation
N = 1: there's only one permutation
N = 2: the graph looks like 0 1 - 1 0
N = 3: the graph looks like
0 1 2 1 2 0 2 0 1
| | |
-------+------- all bipartite connections
| | |
0 2 1 2 1 0 1 0 2
从0 1 2开始,不失一般性。 (lst的初始内容无关紧要。)那我们要开始了
0 1 2 # forced w.l.o.g.
1 0 2 # forced because 2 cannot be moved yet.
这里出现了唯一有趣的选择。我们可以完成
2 0 1 # chose 1
0 2 1 # forced because 1 cannot be moved yet
1 2 0 # forced because we must move element 1 and 0 1 2 is already visited
2 1 0 # forced because 0 cannot be moved yet.
另一个可能的延续是
1 2 0 # chose 0
2 1 0 # forced because 0 cannot be moved yet
2 0 1 # forced because we must move element 0 and 0 1 2 is already visited
0 2 1 # forced because 1 cannot be moved yet.
此时,我注意到 N = 2 和 N = 3 的第一种可能性都反转了排列。我将尝试构建与lst 相反的permute3。让我们看看它在 N = 4 时会做什么。
0 1 2 3
...
2 1 0 3
3 1 0 2
...
0 1 3 2
0 2 3 1
...
3 2 0 1
3 2 1 0
...
1 2 3 0 # oops
好吧,那没用。我们需要奇数次递归调用才能使子数组反转。这是第一个建议,也许我们需要 i 奇数和 i 偶数的不同行为。
与permute2 一样,对于 N = 4,此假设算法将元素向左旋转一圈。该主题的变化似乎是死胡同。时间已经很晚了,我已经做了足够多的徒劳的实验,Fuchs 的算法也开始让我觉得很神奇。
毕竟让我写permute2a。回想一下permute2 将lst 向左旋转一个元素。如果permute2a 使用位置j 进行交换,同时补偿旋转,它会重复交换相同的位置(比如0,因为不能保证可以访问其他位置)。
def permute2a(lst, n):
if n < 2:
print(lst)
else:
permute2(lst, n - 1)
for j in range(n - 2, -1, -1):
lst[0], lst[n - 1] = lst[n - 1], lst[0]
permute2(lst, n - 1)
现在,我意识到,如果 permute2 调用 permute2a 而不是 permute1,那么这对几乎等同于 Fuchs 的算法。 这对我来说仍然很神奇,但我该收工了。也许明天。
事实上,permute2a 不仅可以处理左旋转,还可以处理来自permute2 的任何固定排列,这是一个循环,即,当重复应用于其域中的单个元素时,一个排列给出了其他所有元素。鉴于permute2 以这种方式运行,permute2a 的效果是应用permute2 的 (N - 1) 循环置换 N 次,除了它在循环之间交换元素进出位置 0,与每个元素沿循环移动 N - 1 次的效果,没有效果。因此permute2a 对lst 的影响,不管permute2,是交换位置0 和N - 1。
现在我们要做的就是证明permute2 可以使用permute2a。 permute2a 的行为对 permute2 的实现细节不敏感,这给了我们很大帮助。此外,由于permute2a 仅涉及子数组的第一个和最后一个元素,因此permute2 的当前实现大部分都在那里。事实上,如果 N 是偶数,它就可以正常工作。
0123
...
2103
2130
...
3120
3021
...
2031
1032
...
3012
N 奇数的问题是同一个元素会被交换两次到最后一个位置。
01234
...
31204
31240
...
41230
41032
...
31042
32041
...
42031
12034 # oops
我们现在要做的就是显示新的permute2 循环其元素(当 N 为偶数时)。我将使用一些群论,因为我看不到简单的基本证明。在cycle notation中,排列是
(0 n-2)(0 n-1)(0 n-2)(1 n-1)(0 n-2)...(0 n-2)(n-3 n-1)(0 n-2)(n-2 n-1)(0 n-2).
不相交的循环通勤。
(0 n-2)(0 n-1)[(0 n-2)...n-2 times...(0 n-2)](1 n-1)...(n-3 n-1)(n-2 n-1)(0 n-2).
由于 n 是偶数,因此 n-2 次连续交换无效。
(0 n-2)(0 n-1)(1 n-1)...(n-3 n-1)(n-2 n-1)(0 n-2).
序列(0 n-1)(1 n-1)...(n-3 n-1)(n-2 n-1),正如我们之前观察到的,是一个循环。
(0 n-2)(0 n-1 n-2 ... 1)(0 n-2).
这是一个共轭循环,这也是一个循环。
(0 n-3 n-4 ... 1 n-2 n-1).
这是最终版本。我声称它相当于 Fuchs 的算法以显式堆栈为模。
def permute3(lst, n):
if n < 2:
print(lst)
else:
permute3(lst, n - 1)
for k in range(n - 2, -1, -1):
i = n - 1
j = 0 if i % 2 == 0 else k
lst[j], lst[i] = lst[i], lst[j]
permute3(lst, n - 1)