递归地从 N 中选择 K 项,直到为空

Recursively choose K items from N , until empty

举个例子最能说明问题

从 N 个唯一项目的列表开始 = ['A','B','C','D','E']

选择 k=2 项

这里我们有 Python 实现来显示可能组合的数量:

combos = [c for c in combinations(['A','B','C','D','E'],2)]
combos = [('A', 'B'), ('A', 'C'), ('A', 'D'), ('A', 'E'), ('B', 'C'), ('B', 'D'), ('B', 'E'), ('C', 'D'), ('C', 'E'), ('D', 'E')]

现在,对于这 10 种组合中的每一种,想象一下从原始的 N 列表中取出该集合,并重新计算选择剩余 N-k 项的方法数。

取第一组 ('A', 'B') 剩下 N 项 ['C','D','E']

combos = [c for c in combinations(['C','D','E'],2)]
combos = [('C', 'D'), ('C', 'E'), ('D', 'E')]

再次,我们对 3 种组合中的每一种都重复上述过程,依次从当前列表中取出每一个,最终在这种情况下只在列表中留下一个项目,这就做出了一个简单的最终决定:它不需要任何组合扩展。

所以回顾一下,我一直称之为选择路径: 首先我们选择了 ('A', 'B') 然后我们选择了 ('C', 'D')。 最后我们选择了 ('E')

所以求解路径是一个列表[('A', 'B'), ('C', 'D'), ('E')] and递归已触底,并终止此分支,因为列表中没有更多项目。

如果我们第一次选择 ('A', 'C'),递归会在顶部重新开始,然后继续扩展选项,创建一个新的选择分支路径。

最终结果应该是所有可能选择路径的列表。这将是元组列表的列表,因为解决方案路径本身就是选择的列表。

Final Result should resemble:
[ 
[('A', 'B'), ('C', 'D'), ('E')], 
[('A', 'C'), ('B', 'D'), ('E')], 
[('A', 'D'), ('B', 'C'), ('E')], 
... 
[('D', 'E'), ('A', 'B'), ('C')], 
[('D', 'E'), ('A', 'C'), ('B')], 
... 
]

显然,这只是一个示例,我想将其放大并改变 N 和 K。

我在 python 中的第一次编码尝试并不是很成功。我似乎无法理解我的函数应该 return 每次递归以及如何管理递归级别之间的列表。我真的需要帮助。

def get_combo_path(prod_list, phase, k):
    from itertools import combinations
    from collections import defaultdict
    import copy
    prod_list=copy.deepcopy(prod_list) #Worried about pointers, not sure I need this
    phase=copy.deepcopy(phase) #Worried about pointers, not sure I need this

    prod_combos = [c for c in combinations(prod_list,k)]

    print('prod_list',prod_list)
    for x in prod_combos:
        phase.append(x) #Store which combo we are selecting
        prods_left = list(set(prod_list)-set(x)) #Subtract the set from the list
        print('x',x) #Troubleshooting
        print('phase',phase) #Troubleshooting
        print('prods_left',prods_left) #Troubleshooting
        get_combo_path(prods_left, phase, k) #Recursively call func for each combo
    
    print() #Troubleshooting
    return None #What should I return?
    
#Now to run the function
from collections import defaultdict
prod_list = [chr(ord('a')+p).upper() for p in range(0,5)] #Makes initial list of chars
new_groups = get_combo_path(prod_list, [], 2)

如果有一种方法可以将一个集合的所有分区创建到 equal-size 个没有 'left-over'/更小的容器的容器中,您可以轻松地编写一个函数来获取具有 left-over 的所有分区,通过首先迭代 'left-over' 大小的所有组合并将它们附加到其他元素的每个分区。

使用 Gareth Rees' answer here 中的设置分区功能,您可以这样做:

def partitions(s, r):
    s = set(s)
    assert (len(s) % r == 0)
    if len(s) == 0:
        yield ()
        return
    first = next(iter(s))
    rest = s.difference((first,))
    for c in itertools.combinations(rest, r - 1):
        first_subset = (first,) + c
        for p in partitions(rest.difference(c), r):
            yield (first_subset,) + p

def get_combo_path(prod_list, k):
    """Given distinct elements prod_list, give all partitions into
    bins of size k with possibly 1 remainder bin"""

    prod_set = set(prod_list)
    n = len(prod_set)
    remainder = n % k
    if remainder == 0:
        yield from partitions(prod_set, k)
        return

    for left_overs in itertools.combinations(prod_set, remainder):
        rest = prod_set.difference(left_overs)
        for p in partitions(rest, k):
            yield p + (left_overs,)

for combo in get_combo_path(['A','B','C','D','E'], 2):
    print(combo)

给出:

(('E', 'D'), ('C', 'B'), ('A',))
(('E', 'C'), ('D', 'B'), ('A',))
(('E', 'B'), ('D', 'C'), ('A',))
(('E', 'D'), ('A', 'B'), ('C',))
(('E', 'A'), ('D', 'B'), ('C',))
(('E', 'B'), ('D', 'A'), ('C',))
(('D', 'A'), ('C', 'E'), ('B',))
(('D', 'C'), ('A', 'E'), ('B',))
(('D', 'E'), ('A', 'C'), ('B',))
(('D', 'A'), ('C', 'B'), ('E',))
(('D', 'C'), ('A', 'B'), ('E',))
(('D', 'B'), ('A', 'C'), ('E',))
(('E', 'A'), ('C', 'B'), ('D',))
(('E', 'C'), ('A', 'B'), ('D',))
(('E', 'B'), ('A', 'C'), ('D',))

请注意,这需要不同的元素。如果你想允许重复,你会做同样的事情,但传递 range(len(prod_list)) 而不是 prod_list,并使用结果分区来保存 prod_list 中相应元素的索引。

对@kcsquared 解决方案稍作修改,以便考虑组的选择顺序。

这里的关键是组本身是组合,因此组内的顺序无关紧要,但组的顺序很重要。例如,在我随机给一行人提供主菜和配菜的组合的情况下,盘子里食物的顺序并不重要,但你在该行中的位置可能会决定你是否有鸡肉或牛肉。

def partitions(s, r):
    s = set(s)
    assert (len(s) % r == 0)
    if len(s) == 0:
        yield ()
        return
    for c in itertools.combinations(s, r):
        first_subset = c
        for p in partitions(s.difference(c), r):
            yield (first_subset,) + p

def get_combo_path(prod_list, k):
    """Given distinct elements prod_list, give all partitions into
    bins of size k with possibly 1 remainder bin"""

    prod_set = set(prod_list)
    n = len(prod_set)
    remainder = n % k
    if remainder == 0:
        yield from partitions(prod_set, k)
        return

    for left_overs in itertools.combinations(prod_set, remainder):
        rest = prod_set.difference(left_overs)
        for p in partitions(rest, k):
            yield p + (left_overs,)

结果(将其与原始答案进行比较,看看是否有更多行)

(('E', 'B'), ('D', 'A'), ('C',))
(('E', 'D'), ('B', 'A'), ('C',))
(('E', 'A'), ('D', 'B'), ('C',))
(('B', 'D'), ('E', 'A'), ('C',))
(('B', 'A'), ('E', 'D'), ('C',))
(('D', 'A'), ('E', 'B'), ('C',))
(('C', 'E'), ('B', 'A'), ('D',))
(('C', 'B'), ('E', 'A'), ('D',))
(('C', 'A'), ('E', 'B'), ('D',))
(('E', 'B'), ('C', 'A'), ('D',))
(('E', 'A'), ('C', 'B'), ('D',))
(('B', 'A'), ('C', 'E'), ('D',))
(('C', 'B'), ('D', 'A'), ('E',))
(('C', 'D'), ('B', 'A'), ('E',))
(('C', 'A'), ('D', 'B'), ('E',))
(('B', 'D'), ('C', 'A'), ('E',))
(('B', 'A'), ('C', 'D'), ('E',))
(('D', 'A'), ('C', 'B'), ('E',))
(('C', 'E'), ('D', 'A'), ('B',))
(('C', 'D'), ('E', 'A'), ('B',))
(('C', 'A'), ('E', 'D'), ('B',))
(('E', 'D'), ('C', 'A'), ('B',))
(('E', 'A'), ('C', 'D'), ('B',))
(('D', 'A'), ('C', 'E'), ('B',))
(('C', 'E'), ('D', 'B'), ('A',))
(('C', 'B'), ('E', 'D'), ('A',))
(('C', 'D'), ('E', 'B'), ('A',))
(('E', 'B'), ('C', 'D'), ('A',))
(('E', 'D'), ('C', 'B'), ('A',))
(('B', 'D'), ('C', 'E'), ('A',))