如何将一个列表分成n组,以所有可能的组长度和组内元素进行组合?

14
我希望将一个列表分成n组,每组长度可以不同,包括所有可能的组合方式。

比如说,我有以下列表:

lst=[1,2,3,4]

如果我指定n=2,那么列表可以被分成1个元素-3个元素或2个元素-2个元素的组。在这两种方式中,有哪些元素放入每个列表的独特组合。
当n=2时,它们将是:
(1),(2,3,4)
(2),(1,3,4)
(3),(1,2,4)
(4),(1,2,3)
(1,2),(3,4)
(1,3),(2,4)
(1,4),(2,3)

当n=1时,它们将是:
(1,2,3,4)

当n=3时,它们将是:

(1),(2),(3,4)
(1),(3),(2,4)
(1),(4),(2,3)
(2),(3),(1,4)
(2),(4),(1,3)
(3),(4),(1,2)

我对长度为0的组不感兴趣,组内顺序无关紧要。
我找到了两个类似的问题,但它们并没有完全回答我的问题。 这个问题将列表拆分为所有组长度为n的组合(我发现@tokland的答案特别有用)。然而,我不是在寻找所有组都具有相同长度。
然后这个问题的第一步获取唯一的拆分位置组合来将列表拆分为n组。但是,这里保留了列表顺序,并且这些组中元素的唯一组合没有确定。
我正在寻找这两个问题的组合 - 列表按组长度和组内元素的所有可能组合拆分为n组。

你想要列表数字的排列组合,但是数字可以被排除吗? - Auden Young
看起来你已经有了一个很好的开始,定义了你想要的结果。现在尝试通过用文字/伪代码写下过程/算法来梳理逻辑,然后尝试将其转化为代码。如果出现问题,在关键点使用打印语句来查看变量的情况。 - wwii
基本上,您要对列表进行分区,但不包括重复项,并按[n]的每个值按短字典序排序。请参见此处 以获取基本分区算法。我怀疑过滤和/或排序结果将比首先以正确顺序生成它更容易。 - user6732794
4个回答

12
我们可以使用来自这个答案的基本递归算法,并对其进行修改,以产生特定长度的划分,而无需生成和过滤不需要的划分。
def sorted_k_partitions(seq, k):
    """Returns a list of all unique k-partitions of `seq`.

    Each partition is a list of parts, and each part is a tuple.

    The parts in each individual partition will be sorted in shortlex
    order (i.e., by length first, then lexicographically).

    The overall list of partitions will then be sorted by the length
    of their first part, the length of their second part, ...,
    the length of their last part, and then lexicographically.
    """
    n = len(seq)
    groups = []  # a list of lists, currently empty

    def generate_partitions(i):
        if i >= n:
            yield list(map(tuple, groups))
        else:
            if n - i > k - len(groups):
                for group in groups:
                    group.append(seq[i])
                    yield from generate_partitions(i + 1)
                    group.pop()

            if len(groups) < k:
                groups.append([seq[i]])
                yield from generate_partitions(i + 1)
                groups.pop()

    result = generate_partitions(0)

    # Sort the parts in each partition in shortlex order
    result = [sorted(ps, key = lambda p: (len(p), p)) for ps in result]
    # Sort partitions by the length of each part, then lexicographically.
    result = sorted(result, key = lambda ps: (*map(len, ps), ps))

    return result

这里涉及许多内容,让我来解释一下。

首先,我们从同一个上述递归算法出发,实现了一个过程化的自底向上(术语?)实现:

def partitions(seq):
    """-> a list of all unique partitions of `seq` in no particular order.

    Each partition is a list of parts, and each part is a tuple.
    """
    n = len(seq)
    groups = []  # a list of lists, currently empty

    def generate_partitions(i):
        if i >= n:
            yield list(map(tuple, groups))
        else:
            for group in groups
                group.append(seq[i])
                yield from generate_partitions(i + 1)
                group.pop()

            groups.append([seq[i]])
            yield from generate_partitions(i + 1)
            groups.pop()

    if n > 0:
        return list(generate_partitions(0))
    else:
        return [[()]]

主算法在嵌套的generate_partitions函数中。基本上,它遍历序列,对于每个项目,它:1)将该项放入工作集中的每个当前组(即部分)中并进行递归;2)将该项放入自己的新组中。

当我们到达序列末尾(i == n)时,我们会生成一个已经构建好的工作集的(深层)副本。

现在,要获取特定长度的分区,我们可以简单地过滤或分组所需的结果,并完成它,但是如果我们只想要一些长度为k的分区,则此方法会执行许多不必要的工作(即递归调用)。

请注意,在上面的函数中,分区的长度(即组数)在以下情况下增加:

            # this adds a new group (or part) to the partition
            groups.append([seq[i]])
            yield from generate_partitions(i + 1)
            groups.pop()

...被执行。因此,我们通过在该块上放置警戒来限制分区的大小,像这样:

def partitions(seq, k):
    ...

    def generate_partitions(i):
        ...

            # only add a new group if the total number would not exceed k
            if len(groups) < k:
                groups.append([seq[i]])
                yield from generate_partitions(i + 1)
                groups.pop()

将新参数和代码行添加到partitions函数中,仅会生成长度高达k的分区。这几乎是我们想要的,然而问题在于for循环仍会有时生成长度小于k的分区。

为了修剪这些递归分支,我们需要仅在确保剩余元素足够扩展工作集至总共k个组时才执行for循环。剩余元素数量或尚未放入组中的元素数量为n-i(或len(seq)-i),k-len(groups)是我们需要添加的新组的数量以产生有效的k分区。如果n-i<=k-len(groups),则我们不能将其浪费并将其添加到当前组中,我们必须创建一个新的组。

因此,我们只需向另一个递归分支添加另一道防护罢了:

    def generate_partitions(i):
        ...

            # only add to current groups if the number of remaining items
            # exceeds the number of required new groups.
            if n - i > k - len(groups):
                for group in groups:
                    group.append(seq[i])
                    yield from generate_partitions(i + 1)
                    group.pop()

            # only add a new group if the total number would not exceed k
            if len(groups) < k:
                groups.append([seq[i]])
                yield from generate_partitions(i + 1)
                groups.pop()

完成以上步骤后,您现在拥有一个运作良好的k分区生成器。您可能可以进一步简化一些递归调用(例如,如果还剩下3个项目,我们需要再分为3组,则已经知道必须将每个项目拆分为单独的组),但我想展示这个函数作为基本算法的轻微修改来生成所有分区。

唯一剩下的任务是对结果进行排序。不幸的是,与其弄清楚如何直接按所需顺序生成分区(这是聪明的狗的练习),我选择了先生成,再进行排序的方式(有点取巧)。

def sorted_k_partitions(seq, k):
    ...
    result = generate_partitions(0)

    # Sort the parts in each partition in shortlex order
    result = [sorted(ps, key = lambda p: (len(p), p)) for ps in result]
    # Sort partitions by the length of each part, then lexicographically.
    result = sorted(result, key = lambda ps: (*map(len, ps), ps))

    return result

这个有点自解释,除了一些关键函数需要注意。第一个函数:

key = lambda p: (len(p), p) 

该方法用于按照长度和序列本身排序,其中在Python中,默认情况下按字典顺序排序。 p代表“部分”。这用于对分区内的部分/组进行排序。该关键字意味着,例如,(4,)(1, 2, 3)之前,因此[(1, 2, 3), (4,)]被排序为[(4,), (1, 2, 3)]

key = lambda ps: (*map(len, ps), ps) 
# or for Python versions <3.5: lambda ps: tuple(map(len, ps)) + (ps,)

这里的 ps 代表“parts”的复数形式。该代码表示按元素长度(它们必须是序列本身)对一个序列进行排序,然后再按序列本身的字典序排序。这用于将分区彼此排序,例如,[(4,), (1, 2, 3)] 排在 [(1, 2), (3, 4)] 前面。

接下来的内容:

seq = [1, 2, 3, 4]

for k in 1, 2, 3, 4:
    for groups in sorted_k_partitions(seq, k):
        print(k, groups)

生成:

1 [(1, 2, 3, 4)]
2 [(1,), (2, 3, 4)]
2 [(2,), (1, 3, 4)]
2 [(3,), (1, 2, 4)]
2 [(4,), (1, 2, 3)]
2 [(1, 2), (3, 4)]
2 [(1, 3), (2, 4)]
2 [(1, 4), (2, 3)]
3 [(1,), (2,), (3, 4)]
3 [(1,), (3,), (2, 4)]
3 [(1,), (4,), (2, 3)]
3 [(2,), (3,), (1, 4)]
3 [(2,), (4,), (1, 3)]
3 [(3,), (4,), (1, 2)]
4 [(1,), (2,), (3,), (4,)]

3
这是一段很好的代码,在我构建的一个概念验证中非常有效(感谢那只懒狗),但现在我需要将其产品化为C#。我试图保持代码不变来进行转换,但失败了。我在这里发起了一个新的问题[链接],如果有人能提供帮助,将不胜感激。 - gatapia

5

最简单的选择是使用more_itertools库。

from more_itertools import set_partitions

a = [1, 2, 3, 4]

# pass as the second argument the number of splits
list(set_patitions(a, 2))
...   
 [[[1], [2, 3, 4]],
 [[1, 2], [3, 4]],
 [[2], [1, 3, 4]],
 [[1, 2, 3], [4]],
 [[2, 3], [1, 4]],
 [[1, 3], [2, 4]],
 [[3], [1, 2, 4]]]
>>>

list(set_patitions(a, 3))
...
[[[1], [2], [3, 4]],
 [[1], [2, 3], [4]],
 [[1], [3], [2, 4]],
 [[1, 2], [3], [4]],
 [[2], [1, 3], [4]],
 [[2], [3], [1, 4]]]
>>>

set_partitions使用生成器,可以立即创建数千个集合。


0

在@friendly_dog提供的有用链接的帮助下,我正在尝试通过调整this帖子中使用的函数来回答自己的问题。我有一个大致可行的解决方案,尽管我担心它不是特别高效并且需要改进。最终我会生成比我需要的更多的分区集,并过滤出仅按排序顺序不同的那些。

首先,我从Set partitions in Python中获取这3个函数:

import itertools
from copy import deepcopy

def slice_by_lengths(lengths, the_list):
    for length in lengths:
        new = []
        for i in range(length):
            new.append(the_list.pop(0))
        yield new

def partition(number):
    return {(x,) + y for x in range(1, number) for y in partition(number-x)} | {(number,)}

def subgrups(my_list):
    partitions = partition(len(my_list))
    permed = []
    for each_partition in partitions:
        permed.append(set(itertools.permutations(each_partition, len(each_partition))))

    for each_tuple in itertools.chain(*permed):
        yield list(slice_by_lengths(each_tuple, deepcopy(my_list)))

我编写了一个函数,它包装了subgrups函数,并将其应用于原始列表的每个排列。我循环遍历这些子组排列,如果它们的长度等于所需的分区数,则以一种允许我识别重复项的方式对它们进行排序。我不确定是否有更好的方法来解决这个问题。
def return_partition(my_list,num_groups):
    filtered=[]
    for perm in itertools.permutations(my_list,len(my_list)):
        for sub_group_perm in subgrups(list(perm)):
            if len(sub_group_perm)==num_groups:
                #sort  within each partition
                sort1=[sorted(i) for i in sub_group_perm]
                #sort by first element of each partition
                sort2=sorted(sort1, key=lambda t:t[0])
                #sort by the number of elements in each partition
                sort3=sorted(sort2, key=lambda t:len(t))
                #if this new sorted set of partitions has not been added, add it
                if sort3 not in filtered:
                    filtered.append(sort3)
    return filtered

在我的原始示例列表上运行它,我看到它产生了期望的输出,在两个分区和三个分区上进行了测试。
>>> for i in return_partition([1,2,3,4],2):
...     print i    
... 
[[1], [2, 3, 4]]
[[4], [1, 2, 3]]
[[1, 2], [3, 4]]
[[3], [1, 2, 4]]
[[1, 3], [2, 4]]
[[2], [1, 3, 4]]
[[1, 4], [2, 3]]
>>> 

>>> for i in return_partition([1,2,3,4],3):
...     print i        
... 
[[1], [4], [2, 3]]
[[3], [4], [1, 2]]
[[1], [2], [3, 4]]
[[1], [3], [2, 4]]
[[2], [4], [1, 3]]
[[2], [3], [1, 4]]
>>> 

0
from more_itertools import set_partition
iterable = '1234'
for part in set_partitions(iterable):
    print([''.join(p) for p in part])



['1234']
['1', '234']
['12', '34']
['2', '134']
['123', '4']
['23', '14']
['13', '24']
['3', '124']
['1', '2', '34']
['1', '23', '4']
['1', '3', '24']
['12', '3', '4']
['2', '13', '4']
['2', '3', '14']
['1', '2', '3', '4']

https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.set_partitions


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接