【问题标题】:need help understanding fuchs' iterative countdown permutation algorithm需要帮助理解 fuchs 的迭代倒计时置换算法
【发布时间】:2013-04-15 19:52:12
【问题描述】:

Phillip Fuchs 教授有一个非常简洁的算法,可以使用纯迭代方法生成对象数组的排列(参见 quickperm.org)。基本上有 2 种变体,一种倒计时算法和一种计数(向上计数)算法。思路是一样的,只是初始化的问题和“里程表”的数字是递增还是递减不同。

我已经研究了一段时间的算法,并且可以理解这个想法,除了一个特定的细节。在算法中,以及Fuchs教授对其算法的解释中,如果high index是偶数,low index设置为0;而如果high index为奇数,则low index设置为high index指向的“里程表”位(原文使用j、i、p[]):

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

我只是很难理解这个逻辑。为什么 highIndex 为偶数或奇数确定 lowIndex 应为 0 或 odometerDigit 中的值? Fuchs 教授没有对此提供更详细的解释,但这就是算法的“魔力”症结所在。我真的很感激任何可以帮助我进一步理解这一点的见解。

【问题讨论】:

  • 你试过用手踩吗?
  • 事实上我有。我用一个 3 元素数组、一个 4 元素数组和一个 5 元素数组手动逐步完成(花了很长时间)。肯定有一些模式,但我无法准确指出是什么。我还尝试搜索通过交换元素进行置换的算法,希望它能帮助我进一步了解该算法中的交换模式,但没有找到。这是一个相关的帖子stackoverflow.com/questions/11915026/…,但回答者也只是给出了代码并回避了任何解释

标签: algorithm loops permutation


【解决方案1】:

既然你踩到了 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放它,但我们不能写一整座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。回想一下permute2lst 向左旋转一个元素。如果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 次的效果,没有效果。因此permute2alst 的影响,不管permute2,是交换位置0 和N - 1。

现在我们要做的就是证明permute2 可以使用permute2apermute2a 的行为对 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)

【讨论】:

  • 大卫,非常感谢您提供如此详细的解释,这一定花了您几个小时。我仍然需要花时间消化您在此处提供的信息。
  • @Jim 我自己对结果如此微妙感到惊讶。如果您不介意从消防站喝酒,您可能还会对 Knuth 在 TAoCP 中的治疗感兴趣。
  • 大卫,有趣的是你提到了 TAoCP。前几天我只是在想,在购买 TAoCP 还是混凝土数学之间争论不休。
猜你喜欢
  • 2019-05-01
  • 2018-11-01
  • 1970-01-01
  • 2016-10-22
  • 1970-01-01
  • 2020-01-20
  • 1970-01-01
  • 2021-07-29
  • 2010-11-04
相关资源
最近更新 更多