是否有一种快速算法可以将集合的所有分区生成为大小为 2 的子集(和一个大小为 1 的子集)?

Posted

技术标签:

【中文标题】是否有一种快速算法可以将集合的所有分区生成为大小为 2 的子集(和一个大小为 1 的子集)?【英文标题】:Is there a fast algorithm for generating all partitions of a set into subsets of size 2 (and one subset of size 1)? 【发布时间】:2021-04-06 13:43:56 【问题描述】:

如标题中所述,我正在尝试生成一组大小为 n 的所有分区,其中所有子集的大小为 2,如果 n 不均匀,则有 ne 单例集。我稍微修改了一些生成所有分区的 SO 代码来得到这个:

def partitionIntoPairs(collection):
    if len(collection) == 1:
        yield [ collection ]
        return

    first = collection[0]
    for smaller in partition2(collection[1:]):
        for n, subset in enumerate(smaller):
            if len(subset):
                yield smaller[:n] + [[ first ] + subset]  + smaller[n+1:]
        yield [ [ first ] ] + smaller

这可行,但遗憾的是太慢了。我的第二个想法是使用 itertools.combinations 为某个集合生成所有对,然后在不删除给定对的情况下为每个 det 递归调用该函数,但我猜这会更慢。实现也不正确,它只返回一个可能的分区,我不确定如何让它返回所有分区:

from itertools import combinations

def partitionIntoPairs2(collection):
    if not collection:
        return []
    elif len(collection) == 1:
        return [(next(iter(collection)))]
    else: 
        pairs = set(combinations(collection, 2))
        for pair in pairs: 
            collection.remove(pair[0])
            collection.remove(pair[1])
            return partition3(collection) + [pair]

我偶然发现了一些用于具有给定数量集的分区的算法,以及生成所有可能分区的算法的各种实现,但据我所知,这些都不能有效地解决我的问题。

所以,提出一个更具体的问题:如果第二种算法是可行的选择,那么正确的实现方式是什么?当然,有没有更快的方法来做到这一点?如果是,怎么做?

【问题讨论】:

【参考方案1】:

分区应被视为一个集合,仅按顺序不同的两个分区应被视为同一个分区。所以数字集只有3个分区(1,2,3,4)。

分区数应为 N!/(N/2)!/2^(N/2)。使用斯特林公式,它大约是。 Sqrt(2)*(N/e)^(N/2) 其中 e=2.71828... 非常大。

我利用 @VirtualScooter 的代码并提供了递归版本的 Partition,它比他的 itertools 版本运行得更快(注意这不是苹果与苹果的比较,因为我的 Partition 没有重复)。


import itertools
import timeit
t3 = (1, 2, 3)
t4 = (1, 2, 3, 4)
t6 = (1, 2, 3, 4, 5, 6)

def grouper(iterable, n, fillvalue=None):
    """Collect data into fixed-length chunks or blocks.
        Code from Python itertools page
    """
    # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    return itertools.zip_longest(*args, fillvalue=fillvalue)

def partitionIntoPairs(collection):
    perms = itertools.permutations(collection)
    for p in perms:
        group = list(grouper(p, 2))
        if group[-1][-1] is None:
            group[-1] = (group[-1][0],)
        yield group

def Partition(Indexable):
    if len(Indexable)<=2:
        yield [Indexable]
    elif len(Indexable)%2==1:
        for i,x in enumerate(Indexable):
            for s in Partition(Indexable[:i]+Indexable[i+1:]):
                yield [[x]]+s
    else:
        for i,x in enumerate(Indexable):
            if i==0:
                x0=x
            else:
                for s in Partition(Indexable[1:i]+Indexable[i+1:]):
                    yield [[x0,x]]+s
def comp_timeit(collection, repeats=1_000):
    s1 = f"l1 = list(Partition(collection))"
    s2 = f"l1 = list(partitionIntoPairs(collection))"
    t1 = timeit.timeit(s1, globals=globals(),number=repeats)
    t2 = timeit.timeit(s2, globals=globals(),number=repeats)
    print(f"partition, repeats:_ runs: t1:,.4f")
    print(f"itertools, repeats:_ runs: t2:,.4f")
for p in Partition(t4):
    print(p)
comp_timeit(t3)
comp_timeit(t4)
comp_timeit(t6)

【讨论】:

你是对的。我已经更新了我的答案以使生成的集合独一无二。您的算法仍然是最快的,因为现成的算法需要过滤子集大小 > 2 的组合。【参考方案2】:

此递归生成器函数yields 在分区长度与原始输入相同时进行分区,并且仅当它可以添加到正在进行的子分区或保留单个子分区时才进行递归调用(如果 @987654322 @):

data = 1, 2, 3
def partition(d, m, c = []):
   if len(l:=[j for k in c for j in k]) == len(d):
      yield c
   for i in filter(lambda x:x not in l, d):
      if not c or len(c[-1]) == m:
         yield from partition(d, m, c=c+[[i]])
      else:
         if sum(len(i) == 1 for i in c) == 1 and len(data)%2:
            yield from partition(d, m, c=c+[[i]])
         yield from partition(d, m, c=[*c[:-1], c[-1]+[i]])


print(list(partition(list(data), 2)))

输出:

[[[1], [2, 3]], [[1, 2], [3]], [[1], [3, 2]], [[1, 3], [2]], [[2], [1, 3]], [[2, 1], [3]], [[2], [3, 1]], [[2, 3], [1]], [[3], [1, 2]], [[3, 1], [2]], [[3], [2, 1]], [[3, 2], [1]]]

len(data)%2 == 0:

data = 1, 2, 3, 4
print(list(partition(list(data), 2)))

输出:

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

【讨论】:

【参考方案3】:

这可以通过itertools 完成,可能比递归算法更快, 就像partition 在另一个答案(https://***.com/a/66972507/5660315)中一样。我在我的 timeit 序列中测量了 t6 的 4.5s 运行时间,如下所示, mi_partition 的时间低于 0.2 秒。

第一个想法是首先列出集合的所有排列,然后拆分 每个子集中,使用来自itertools 文档的grouper 算法 页。然后,如果适用,我们会为最终的奇数子集剔除填充物。

正如@Bing Wang 指出的那样,重复出现在这种类型的序列中。所以, 相反,我调用了more_itertools.set_partitions 函数,它 减少重复。这也会生成长度更大的子集 大于 2,所以这些都被 itertools.filterfalse 过滤掉了。

import itertools
import timeit
import more_itertools

t3 = (1, 2, 3)
t4 = (1, 2, 3, 4)
t6 = (1, 2, 3, 4, 5, 6)

def mi_partition(collection):
    k = len(collection) // 2 + len(collection) % 2
    s1 = more_itertools.set_partitions(collection, k)
    if False:
        p1, p2 = itertools.tee(s1)
        print(len(list(p1)))
        s1 = p2
    return itertools.filterfalse(lambda x: any(len(y)>2 for y in x), s1)

print(list(mi_partition(t3)))
print(list(mi_partition(t4)))

输出:

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

Partition 算法的小时序比较来自 @Bing Wang 的回答表明他们的解决方案更快:

def comp_timeit(collection, repeats=1_000):
    s3 = f"l1 = list(mi_partition(collection))"
    s4 = f"l1 = list(Partition(collection))"
    t3 = timeit.timeit(s3, globals=globals(),number=repeats)
    print(f"more_itertools, repeats:_ runs: t3:,.4f")
    t4 = timeit.timeit(s4, globals=globals(),number=repeats)
    print(f"Partition, repeats:_ runs: t4:,.4f")
comp_timeit(t3)
comp_timeit(t4)
comp_timeit(t6)

下面的输出。请注意,对于 t3 到 t4,结果列表有 两种情况下的长度为 3,而对于 t5,长度为 15。 似乎Partitions 解决方案稍微快一些,可能 因为它不需要过滤任何解决方案。对于 t6, set_partitions(t6, 3) 生成 90 个分区,只有 15 个 将其纳入最终答案。

more_itertools, 1_000 runs: 0.0051
Partition, 1_000 runs: 0.0024
more_itertools, 1_000 runs: 0.0111
Partition, 1_000 runs: 0.0026
more_itertools, 1_000 runs: 0.1333
Partition, 1_000 runs: 0.0160```

【讨论】:

【参考方案4】:

您的示例没有显示“所有子集”的含义。 如果您需要获取给定集合中所有可能的值对,请尝试使用 set() 和 freezeset()

my_set = 1,2,3,

res = set()
for value in my_set:
    current_set = set()
    current_set.add(value)
    for value in my_set:
        new_set = current_set.copy()
        new_set.add(value)
        res.add(frozenset(new_set))
        
if not len(my_set) % 2:
    res = [list(new_set) for new_set in res if len(new_set) > 1]
else:
    res = list(map(list, res))
print(res)

【讨论】:

这不是我的问题,我希望将给定大小 n 集合的所有分区 (en.wikipedia.org/wiki/Partition_of_a_set) 生成大小为 2 的子集。如果 n 不是偶数,那么可能成为分区中的一个单例集。

以上是关于是否有一种快速算法可以将集合的所有分区生成为大小为 2 的子集(和一个大小为 1 的子集)?的主要内容,如果未能解决你的问题,请参考以下文章

是否有一种算法可以手动将十六进制的二进制补码转换为十进制?

PHP算法从单个集合中生成特定大小的所有组合

快速排除成员是不是在集合中

在哈希分区中使用啥算法,它可以在查看任何数据之前将数据划分为相似大小的分区

最小生成树

是否有一种算法可以为给定范围内的每个条目找到最短的二进制表示?