生成非交叉分区的Python代码

huangapple go评论59阅读模式
英文:

Generating noncrossing partitions in python

问题

我想生成集合 S = [1,2,3,4,...,n] 的所有非交叉分区,其中非交叉分区是这样的一个分区,其中不存在元素 a < b < c < d,其中 a、c 在同一块中,而 b、d 在不同的块中。

有人能帮助我吗?

我有一个可以做到这一点的算法,但它运行得非常慢,并且在尝试计算集合 S = [1,2,3,4,...,14] 上的非交叉分区时会出现内存错误。

英文:

I want to generate all noncrossing partitions of a set S= [1,2,3,4,...,n], where a noncrossing partition is a partition where there does not exist elements a < b < c < d where a,c are in the same block, and b,d are in the same but different block.

Can somebody help me with this?

I have an algorithm that does this, but it works very slow, and I get a memory error when trying to calculate the noncrossing partitions on the set S= [1,2,3,4,...,14].

答案1

得分: 0

我已经创建了一个相当经过优化的实现如下。请注意,它会改变分区列表(出于速度考虑),所以你要么需要在它们被生成时使用分区,要么使用深复制它们。如果可能的话,最好选择前者,因为这样会明显更快。

def make_partitions(elements):
    yield from _make_partitions(sorted(elements, reverse=True), [], []) 

def _make_partitions(elements, active_partitions, inactive_partitions):
    if not elements:
        yield active_partitions + inactive_partitions
        return

    elem = elements.pop()

    # 创建一个新的分区
    active_partitions.append([elem])
    yield from _make_partitions(elements, active_partitions, inactive_partitions)
    active_partitions.pop()

    # 将元素依次添加到每个现有分区中
    size = len(active_partitions)
    for part in active_partitions[::-1]:
        part.append(elem)
        yield from _make_partitions(elements, active_partitions, inactive_partitions)
        part.pop()

        # 移除可能会导致交叉的分区
        inactive_partitions.append(active_partitions.pop())
    
    # 添加回被移除的分区
    for _ in range(size):
        active_partitions.append(inactive_partitions.pop())

    elements.append(elem)

用法:

for partitions in make_partitions([1, 2, 3, 4, 5]):
    print(partitions)

输出:

[[1], [2], [3], [4], [5]]
[[1], [2], [3], [4, 5]]
[[1], [2], [3, 5], [4]]
[[1], [2, 5], [4], [3]]
[[1, 5], [4], [3], [2]]
[[1], [2], [3, 4], [5]]
[[1], [2], [3, 4, 5]]
[[1], [2, 5], [3, 4]]
[[1, 5], [3, 4], [2]]
[[1], [2, 4], [5], [3]]
[[1], [2, 4, 5], [3]]
[[1, 5], [3], [2, 4]]
[[1, 4], [5], [3], [2]]
[[1, 4, 5], [3], [2]]
[[1], [2, 3], [4], [5]]
[[1], [2, 3], [4, 5]]
[[1], [2, 3, 5], [4]]
[[1, 5], [4], [2, 3]]
[[1], [2, 3, 4], [5]]
[[1], [2, 3, 4, 5]]
[[1, 5], [2, 3, 4]]
[[1, 4], [5], [2, 3]]
[[1, 4, 5], [2, 3]]
[[1, 3], [4], [5], [2]]
[[1, 3], [4, 5], [2]]
[[1, 3, 5], [2], [4]]
[[1, 3, 4], [5], [2]]
[[1, 3, 4, 5], [2]]
[[1, 2], [3], [4], [5]]
[[1, 2], [3], [4, 5]]
[[1, 2], [3, 5], [4]]
[[1, 2, 5], [4], [3]]
[[1, 2], [3, 4], [5]]
[[1, 2], [3, 4, 5]]
[[1, 2, 5], [3, 4]]
[[1, 2, 4], [5], [3]]
[[1, 2, 4, 5], [3]]
[[1, 2, 3], [4], [5]]
[[1, 2, 3], [4, 5]]
[[1, 2, 3, 5], [4]]
[[1, 2, 3, 4], [5]]
[[1, 2, 3, 4, 5]]

如上所述,如果你需要保留这些分区,你需要对它们进行深复制:

from copy import deepcopy

results = [deepcopy(partitions) for partitions in make_partitions(list(range(6)))]

在我的机器上,这个方法可以在短短几秒内生成一组包含14个不交叉元素的分区。

解释

我通过考虑每种可能性来生成每个分区:

  • 为它创建一个新的分区
  • 将它添加到现有分区中(每个分区都要添加)

由于我是递归执行的,并且按照一致的元素顺序进行操作,因此它会列举出集合的所有分区。

但是,我通过删除(使其无效)包含比已经存在于我们选择将新元素添加到其中的分区中的元素更大的元素的分区来跳过交叉分区。因此,如果一个分区包含 [1, 2],另一个包含 [3],当我们将 4 添加到第一个分区时,我会舍弃 [3] 分区,因为添加任何大于 4 的元素都会创建交叉分区(并且由于我们按递增顺序添加元素,我们可以添加到此分区的所有未来元素都会这样做)。

我通过隐式按升序排序活动分区来优化这一步,因此我可以从末尾删除有问题的元素。我还以相反的顺序迭代要添加新元素的分区,以便逐个删除有问题的分区(即将来可能会产生交叉的分区)。这就是为什么我只需要每次弹出一个元素。

英文:

I've created a fairly heavily optimized implementation below. Note that it mutates partition lists (in the name of speed), so you will either need to use the partitions as they are produced, or deepcopy them. It will be noticeably faster to do the former, so opt for that if possible.

def make_partitions(elements):
    yield from _make_partitions(sorted(elements, reverse=True), [], []) 

def _make_partitions(elements, active_partitions, inactive_partitions):
    if not elements:
        yield active_partitions + inactive_partitions
        return

    elem = elements.pop()

    # Make create a new partition
    active_partitions.append([elem])
    yield from _make_partitions(elements, active_partitions, inactive_partitions)
    active_partitions.pop()

    # Add element to each existing partition in turn
    size = len(active_partitions)
    for part in active_partitions[::-1]:
        part.append(elem)
        yield from _make_partitions(elements, active_partitions, inactive_partitions)
        part.pop()

        # Remove partition that would create a cross if new elements were added
        inactive_partitions.append(active_partitions.pop())
    
    # Add back removed partitions
    for _ in range(size):
        active_partitions.append(inactive_partitions.pop())

    elements.append(elem)

Usage:

for partitions in make_partitions([1, 2, 3, 4, 5]):
    print(partitions)

Output:

[[1], [2], [3], [4], [5]]
[[1], [2], [3], [4, 5]]
[[1], [2], [3, 5], [4]]
[[1], [2, 5], [4], [3]]
[[1, 5], [4], [3], [2]]
[[1], [2], [3, 4], [5]]
[[1], [2], [3, 4, 5]]
[[1], [2, 5], [3, 4]]
[[1, 5], [3, 4], [2]]
[[1], [2, 4], [5], [3]]
[[1], [2, 4, 5], [3]]
[[1, 5], [3], [2, 4]]
[[1, 4], [5], [3], [2]]
[[1, 4, 5], [3], [2]]
[[1], [2, 3], [4], [5]]
[[1], [2, 3], [4, 5]]
[[1], [2, 3, 5], [4]]
[[1, 5], [4], [2, 3]]
[[1], [2, 3, 4], [5]]
[[1], [2, 3, 4, 5]]
[[1, 5], [2, 3, 4]]
[[1, 4], [5], [2, 3]]
[[1, 4, 5], [2, 3]]
[[1, 3], [4], [5], [2]]
[[1, 3], [4, 5], [2]]
[[1, 3, 5], [2], [4]]
[[1, 3, 4], [5], [2]]
[[1, 3, 4, 5], [2]]
[[1, 2], [3], [4], [5]]
[[1, 2], [3], [4, 5]]
[[1, 2], [3, 5], [4]]
[[1, 2, 5], [4], [3]]
[[1, 2], [3, 4], [5]]
[[1, 2], [3, 4, 5]]
[[1, 2, 5], [3, 4]]
[[1, 2, 4], [5], [3]]
[[1, 2, 4, 5], [3]]
[[1, 2, 3], [4], [5]]
[[1, 2, 3], [4, 5]]
[[1, 2, 3, 5], [4]]
[[1, 2, 3, 4], [5]]
[[1, 2, 3, 4, 5]]

As mentioned, if you need to preserve the partitions, you need to deepcopy them:

from copy import deepcopy

results = [deepcopy(partitions) for partitions in make_partitions(list(range(6)))]

On my machine, this can produce all non-crossing partitions of a set of 14 distinct elements in just a couple seconds.

Explanation

I generate each partition by considering of each possibility:

  • create a new partition for it
  • add it to an existing partition (for each)

Since I do this recursively, and in consistent element order, it would enumerate all partitions of the set.

However, I skip the crossing partitions by dropping (making inactive) any partitions containing elements larger than the elements already present in the partition we choose to add the new element to. So, if one partition has [1, 2] and another has [3], and we add 4 to the first partition, I would drop the [3] partition, because adding any elements greater than 4 would create a crossing partition (and since we're adding elements in increasing order, all future elements we can add to this partition would do so).

I optimize this step by implicitly ordering the active partitions in increasing order, so I can just remove the offending elements from the end. I also iterate over the partitions to add the new element to in reverse order, so I can incrementally remove offending partitions (ones that would cross in the future), one by one. This is why I only need to pop one element each time.

huangapple
  • 本文由 发表于 2023年2月27日 00:51:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/75573540.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定