python itertools.permutations 的算法

Posted

技术标签:

【中文标题】python itertools.permutations 的算法【英文标题】:algorithm for python itertools.permutations 【发布时间】:2011-02-03 16:04:25 【问题描述】:

有人能解释一下 Python 标准库 2.6 中 itertools.permutations 例程的算法吗?我不明白它为什么会起作用。

代码是:

def permutations(iterable, r=None):
    # permutations('ABCD', 2) --> AB AC AD BA BC BD CA CB CD DA DB DC
    # permutations(range(3)) --> 012 021 102 120 201 210
    pool = tuple(iterable)
    n = len(pool)
    r = n if r is None else r
    if r > n:
        return
    indices = range(n)
    cycles = range(n, n-r, -1)
    yield tuple(pool[i] for i in indices[:r])
    while n:
        for i in reversed(range(r)):
            cycles[i] -= 1
            if cycles[i] == 0:
                indices[i:] = indices[i+1:] + indices[i:i+1]
                cycles[i] = n - i
            else:
                j = cycles[i]
                indices[i], indices[-j] = indices[-j], indices[i]
                yield tuple(pool[i] for i in indices[:r])
                break
        else:
            return

【问题讨论】:

【参考方案1】:

结果中的模式比文字更容易回答(除非你想知道理论的数学部分), 所以打印出来是最好的解释方式。 最微妙的是, 循环到最后,它会自动重置到上一轮的第一圈,然后开始下一轮循环,或者不断地重置到最后一轮的第一圈,甚至更大的一轮,就像一个时钟。

执行重置工作的代码部分:

         if cycles[i] == 0:
             indices[i:] = indices[i+1:] + indices[i:i+1]
             cycles[i] = n - i

整体:

In [54]: def permutations(iterable, r=None):
    ...:     # permutations('ABCD', 2) --> AB AC AD BA BC BD CA CB CD DA DB DC
    ...:     # permutations(range(3)) --> 012 021 102 120 201 210
    ...:     pool = tuple(iterable)
    ...:     n = len(pool)
    ...:     r = n if r is None else r
    ...:     if r > n:
    ...:         return
    ...:     indices = range(n)
    ...:     cycles = range(n, n-r, -1)
    ...:     yield tuple(pool[i] for i in indices[:r])
    ...:     print(indices, cycles)
    ...:     while n:
    ...:         for i in reversed(range(r)):
    ...:             cycles[i] -= 1
    ...:             if cycles[i] == 0:
    ...:                 indices[i:] = indices[i+1:] + indices[i:i+1]
    ...:                 cycles[i] = n - i
    ...:                 print("reset------------------")
    ...:                 print(indices, cycles)
    ...:                 print("------------------")
    ...:             else:
    ...:                 j = cycles[i]
    ...:                 indices[i], indices[-j] = indices[-j], indices[i]
    ...:                 print(indices, cycles, i, n-j)
    ...:                 yield tuple(pool[i] for i in indices[:r])
    ...:                 break
    ...:         else:
    ...:             return

部分结果:

In [54]: list(','.join(i) for i in permutations('ABCDE', 3))
([0, 1, 2, 3, 4], [5, 4, 3])
([0, 1, 3, 2, 4], [5, 4, 2], 2, 3)
([0, 1, 4, 2, 3], [5, 4, 1], 2, 4)
reset------------------
([0, 1, 2, 3, 4], [5, 4, 3])
------------------
([0, 2, 1, 3, 4], [5, 3, 3], 1, 2)
([0, 2, 3, 1, 4], [5, 3, 2], 2, 3)
([0, 2, 4, 1, 3], [5, 3, 1], 2, 4)
reset------------------
([0, 2, 1, 3, 4], [5, 3, 3])
------------------
([0, 3, 1, 2, 4], [5, 2, 3], 1, 3)
([0, 3, 2, 1, 4], [5, 2, 2], 2, 3)
([0, 3, 4, 1, 2], [5, 2, 1], 2, 4)
reset------------------
([0, 3, 1, 2, 4], [5, 2, 3])
------------------
([0, 4, 1, 2, 3], [5, 1, 3], 1, 4)
([0, 4, 2, 1, 3], [5, 1, 2], 2, 3)
([0, 4, 3, 1, 2], [5, 1, 1], 2, 4)
reset------------------
([0, 4, 1, 2, 3], [5, 1, 3])
------------------
reset------------------(bigger reset)
([0, 1, 2, 3, 4], [5, 4, 3])
------------------
([1, 0, 2, 3, 4], [4, 4, 3], 0, 1)
([1, 0, 3, 2, 4], [4, 4, 2], 2, 3)
([1, 0, 4, 2, 3], [4, 4, 1], 2, 4)
reset------------------
([1, 0, 2, 3, 4], [4, 4, 3])
------------------
([1, 2, 0, 3, 4], [4, 3, 3], 1, 2)
([1, 2, 3, 0, 4], [4, 3, 2], 2, 3)
([1, 2, 4, 0, 3], [4, 3, 1], 2, 4)

【讨论】:

【参考方案2】:

您需要了解permutation cycles 的数学理论,也称为“轨道”(了解这两个“艺术术语”很重要,因为combinatorics 的核心数学主题非常先进,并且您可能需要查找research papers 可以使用其中一个或两个术语)。

对于排列理论的简单介绍,wikipedia 可以提供帮助。如果您对组合学足够着迷并想要进一步探索并获得真正的理解,我提到的每个 URL 都提供了合理的参考书目(我个人是这样做的——这对我来说已经成为一种爱好;-)。

一旦你理解了数学理论,代码对于“逆向工程”来说仍然是微妙而有趣的。显然,indices 只是池中索引方面的当前排列,因为产生的项目总是由下式给出

yield tuple(pool[i] for i in indices[:r])

所以这个迷人的机器的核心是cycles,它代表排列的轨道并导致indices被更新,主要是通过语句

j = cycles[i]
indices[i], indices[-j] = indices[-j], indices[i]

即,如果cycles[i]j,这意味着索引的下一次更新是将第 i 个(从左侧)与第 j 个从右侧交换(例如,如果j 为1,则indiceslast 元素正在被交换——indices[-1])。然后当 cycles 的项目在其递减期间达到 0 时,“批量更新”的频率就会降低:

indices[i:] = indices[i+1:] + indices[i:i+1]
cycles[i] = n - i

这将indices 的第i 项放在最后,将索引的所有后续项向左移动一个,并表示下次我们来到cycles 的该项时,我们将将indices 的新ith 项(左起)与n - ith 项(右起)交换——这将是ith 再次,当然除了有将是一个

cycles[i] -= 1

在我们下一次检查之前;-)。

困难的部分当然是证明这是可行的 - 即,所有排列都是详尽生成的,没有重叠和正确的“定时”退出。我认为,在简单情况下完全暴露时,可能更容易查看机器如何工作——注释掉yield 语句并添加print 语句(Python 2.*),而不是证明,我们有

def permutations(iterable, r=None):
    # permutations('ABCD', 2) --> AB AC AD BA BC BD CA CB CD DA DB DC
    # permutations(range(3)) --> 012 021 102 120 201 210
    pool = tuple(iterable)
    n = len(pool)
    r = n if r is None else r
    if r > n:
        return
    indices = range(n)
    cycles = range(n, n-r, -1)
    print 'I', 0, cycles, indices
    # yield tuple(pool[i] for i in indices[:r])
    print indices[:r]
    while n:
        for i in reversed(range(r)):
            cycles[i] -= 1
            if cycles[i] == 0:
        print 'B', i, cycles, indices
                indices[i:] = indices[i+1:] + indices[i:i+1]
                cycles[i] = n - i
        print 'A', i, cycles, indices
            else:
        print 'b', i, cycles, indices
                j = cycles[i]
                indices[i], indices[-j] = indices[-j], indices[i]
        print 'a', i, cycles, indices
                # yield tuple(pool[i] for i in indices[:r])
            print indices[:r]
                break
        else:
            return

permutations('ABC', 2)

运行这个显示:

I 0 [3, 2] [0, 1, 2]
[0, 1]
b 1 [3, 1] [0, 1, 2]
a 1 [3, 1] [0, 2, 1]
[0, 2]
B 1 [3, 0] [0, 2, 1]
A 1 [3, 2] [0, 1, 2]
b 0 [2, 2] [0, 1, 2]
a 0 [2, 2] [1, 0, 2]
[1, 0]
b 1 [2, 1] [1, 0, 2]
a 1 [2, 1] [1, 2, 0]
[1, 2]
B 1 [2, 0] [1, 2, 0]
A 1 [2, 2] [1, 0, 2]
b 0 [1, 2] [1, 0, 2]
a 0 [1, 2] [2, 0, 1]
[2, 0]
b 1 [1, 1] [2, 0, 1]
a 1 [1, 1] [2, 1, 0]
[2, 1]
B 1 [1, 0] [2, 1, 0]
A 1 [1, 2] [2, 0, 1]
B 0 [0, 2] [2, 0, 1]
A 0 [3, 2] [0, 1, 2]

关注cycles:它们从 3、2 开始——然后最后一个递减,所以 3、1——最后一个还不是零,所以我们有一个“小”事件(一个交换索引)并打破内循环。然后我们再次输入它,这次最后的减量给出 3, 0——最后一个现在为零,所以这是一个“大”事件——索引中的“质量交换”(这里没有太多的质量,但是,可能有;-) 并且循环回到 3、2。但是现在我们还没有中断 for 循环,所以我们继续减少 next-to-last (in这种情况下,第一个)——它给出了一个次要事件,一个交换索引,我们再次打破内部循环。回到循环,最后一个再次递减,这次给出 2、1 - 次要事件等。最终,整个 for 循环发生,只有主要事件,没有次要事件 - 这就是循环开始为所有事件的时候,因此减量使每个都为零(主要事件),在最后一个周期不会发生yield

由于没有在该循环中执行过break,我们采用forelse 分支,该分支返回。请注意,while n 可能有点误导:它实际上充当while True -- n 永远不会改变,while 循环仅从该 return 语句中退出;它同样可以表示为if not n: return,然后是while True:,因为当然当n0(空的“池”)之后,在第一个微不足道的空yield 之后就没有更多的东西了。作者只是决定通过将if not n:while 合并;-) 来节省几行。

我建议您继续研究一些更具体的案例——最终您应该会感觉到“发条”在运行。一开始只关注cycles(可能相应地编辑print语句,从它们中删除indices),因为它们在轨道上的发条式进展是这个微妙而深入的算法的关键;一旦你了解那个indices 正确更新以响应cycles 的顺序几乎是一种反高潮!-)

【讨论】:

只是我失去了希望,但总能指望亚历克斯!我还没有完全grok,但是你给出的线索非常好,我会读到的。多谢。你也知道是谁在 python 库中实现了这个吗? Raymond Hettinger:参见svn.python.org/view/python/trunk/Modules/… 的第 2495 行及以下。 循环列表代表什么?作为一个学习了 6 个学期的抽象代数并且对对称群和循环/轨道非常了解的人,这个符号(和代码)对我来说几乎没有任何意义。我不知道实际的总体策略是什么。 上面的链接坏了。请参阅here。

以上是关于python itertools.permutations 的算法的主要内容,如果未能解决你的问题,请参考以下文章

Python代写,Python作业代写,代写Python,代做Python

Python开发

Python,python,python

Python 介绍

Python学习之认识python

python初识