需要帮助理解fuchs的迭代倒计时排列算法

时间:2013-04-15 19:52:12

标签: algorithm loops permutation

Phillip Fuchs教授有一个非常巧妙的算法,使用纯迭代方法生成一组对象的排列(参见quickperm.org)。基本上有2种变化,倒计时算法和计数(递增计数)算法。这些想法是相同的,只是初始化问题的区别以及是否增加或减少“里程表”数字。

我已经研究了一段时间的算法,并且可以理解这个想法,除了一个特定的细节。在算法中,以及Fuchs教授为其算法提供的解释,它说如果高指数是偶数,则低指数设置为0;如果高索引是奇数,则低索引设置为高索引指向的“里程表”数字(原始文本使用j,i和p []):

lowIndex = (highIndex %2 == 0) ? 0 : odometerDigit[highIndex];

我正在努力理解这种逻辑。为什么highIndex是偶数还是奇数确定lowIndex应该是0还是odometerDigit中的值? Fuchs教授没有提供更详细的解释,但这是算法的“神奇”症结。我真的很感激任何有助于我进一步理解这一点的见解。

1 个答案:

答案 0 :(得分:2)

因为你走了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在返回之前不会恢复lstpermute2不是递归的,因为它使用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)

permute2lst做了什么?忽略permute1的调用,使lst保持不变,它会向左移动lst一个元素。现在我们可以编写permute2a,它会调用permute2并查找要转换到lst[n - 1]的下一个元素permute2放置它,但我们不能写一个完整的塔permute秒。

我们需要一个关于我们尚未存在的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的第一种可能性都反转了排列。我将尝试构建反转permute3的{​​{1}}。让我们看看N = 4可能会做些什么。

lst

嗯,这没用。我们需要奇数个递归调用才能使子阵列反转。这是第一个建议,也许我们需要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类似,对于N = 4,此假设算法将元素向左旋转一个。这个主题的变化似乎是死路一条。这里已经足够晚了,而且我已经完成了无用的实验,Fuchs的算法对我来说也开始变得神奇了。

毕竟让我写permute2。回想一下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,则该对几乎等同于Fuchs的算法。 这对我来说似乎仍然很神奇,但我现在应该把它称为一天。也许明天。

事实上,permute1不仅可以处理左旋转,还可以处理来自permute2a的任何固定排列,这是一个循环,即,当重复应用于其域中的单个元素时的排列,给出所有其他元素。鉴于permute2的行为方式,permute2的效果是将permute2a的(N - 1) - 循环排列应用N次,除非它将元素交换进出位置0在循环之间,每个元素沿着循环移动N-1次,这没有效果。 permute2permute2a的影响,无论lst如何,都是交换位置0和N - 1。

现在我们所要做的就是争辩permute2可以使用permute2。由于permute2a的行为对permute2a的实现细节不敏感,我们得到了很多帮助。此外,由于permute2仅涉及子阵列的第一个和最后一个元素,因此permute2a的当前实现大部分都在那里。事实上,如果N是偶数,它就可以正常工作。

permute2

N odd的问题是相同的元素将被交换两次到最后位置。

0123
...
2103
2130
...
3120
3021
...
2031
1032
...
3012

现在我们要做的就是表明新的01234 ... 31204 31240 ... 41230 41032 ... 31042 32041 ... 42031 12034 # oops 循环其元素(当N是偶数时)。我将使用一个小组理论,因为我看不到简单的基本证明。在cycle notation中,排列是

permute2

不相交的周期通勤。

(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).

由于n是偶数,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).

我们之前观察到的序列(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).

这是最终版本。我声称它相当于Fuchs的算法模块化显式堆栈。

(0 n-3 n-4 ... 1 n-2 n-1).