真是个好问题!为了与术语保持一致,我会将矩阵中的行称为“输入bags”,并将输入袋中的项目称为“对象”。袋(或multiset)是一个容器,允许成员出现不止一次,但其成员没有固有的顺序(与列表不同)。
我们正在寻找具有以下签名的函数:
set of valid combinations =
generate_combinations(list of input bags, number of objects in valid combination)
因为它有可能是一组有效组合超过Python可以使用的内存,generate_combinations
应该返回一个发电机,而不是一个明确的名单。
一个有效的结果,必须满足以下要求:
- 至少1个从各输入包
- 对象将具有N个对象的总
我假设以下几点:
- 结果中对象的顺序无关紧要
- 输入袋可以包含重复的对象
- 两个输入袋可以包含重复的对象(在退化情况下,两个输入袋可以是相同的)
- 有效结果拉动对象,而无需更换
要求# 1层假设#4暗示number of input bags <= n <= total number of objects in all bags
数据结构
- 由于输入包包含重复值(每个假设#2),我们不能使用set/frozenset来存储这些值。 Python列表适用于此任务。另一个容器可以是collections.Counter,它具有恒定时间的成员资格测试和更好的空间效率,用于许多重复的列表。
- 每个有效的组合也是一个包
- 订单对输入袋的列表无关紧要,所以这可以概括为一袋输入袋。为了理智,我会保持现状。
我将使用Python内置的itertools
模块来生成组合,它在v2.6中引入。如果您正在运行较旧版本的Python,请使用配方。对于这些代码示例,为了清楚起见,我隐式地将生成器转换为列表。
>>> import itertools, collections
>>> input_bags = [Bag([1,2,2,3,5]), Bag([1,4,5,9]), Bag([12])]
>>> output_bag_size = 5
>>> combos = generate_combinations(input_bags, output_bag_size)
>>> combos.next() #an arbitrary example
Bag([1,1,2,4,12])
要认识到,如以上所述的问题可被减少到一个,通过Python的内置itertools模块里,其产生序列的组合是立即可解的。我们需要做的唯一修改是取消要求#1。为了解决减少的问题,将袋子的成员合并成一个袋子。
>>> all_objects = itertools.chain.from_iterable(input_bags)
>>> all_objects
generator that returns [1, 2, 2, 3, 5, 1, 4, 5, 9, 12]
>>> combos = itertools.combinations(all_objects, output_bag_size)
>>> combos
generator that returns [(1,2,2,3,5), (1,2,2,3,1), (1,2,2,3,4), ...]
要恢复要求#1,各有效组合(输出袋)需要包含从任何的袋从每个输入包加上另外的元件1个元件,直到输出袋大小达到用户指定的值。如果忽略来自每个输入袋子的[1个元素]和[任何袋子的附加元素]之间的重叠,则该解决方案就是第一个和第二个可能组合的可能组合的笛卡尔积。
要处理重叠,请将[每个输入袋中的[1个元素]中的元素从[任何袋子中的其他元素]中移除,然后循环移除。
>>> combos_with_one_element_from_each_bag = itertools.product(*input_bags)
>>> for base_combo in combos_with_one_element_from_each_bag:
>>> all_objects = list(itertools.chain.from_iterable(input_bags))
>>> for seen in base_combo:
>>> all_objects.remove(seen)
>>> all_objects_minus_base_combo = all_objects
>>> for remaining_combo in itertools.combinations(all_objects_minus_base_combo, output_bag_size-len(base_combo)):
>>> yield itertools.chain(base_combo, remaining_combo)
remove操作在列表上受支持,但在生成器上不受支持。要避免将all_objects作为列表存储在内存中,请创建一个跳过base_combo中的元素的函数。
>>> def remove_elements(iterable, elements_to_remove):
>>> elements_to_remove_copy = Bag(elements_to_remove) #create a soft copy
>>> for item in iterable:
>>> if item not in elements_to_remove_copy:
>>> yield item
>>> else:
>>> elements_to_remove_copy.remove(item)
袋类的实现可能是这样的:
>>> class Bag(collections.Counter):
>>> def __iter__(self):
>>> return self.elements()
>>> def remove(self, item):
>>> self[item] -= 1
>>> if self[item] <= 0:
>>> del self[item]
完整代码
import itertools, collections
class Bag(collections.Counter):
def __iter__(self):
return self.elements()
def remove(self, item):
self[item] -= 1
if self[item] <= 0:
del self[item]
def __repr__(self):
return 'Bag(%s)' % sorted(self.elements())
def remove_elements(iterable, elements_to_remove):
elements_to_remove_copy = Bag(elements_to_remove) #create a soft copy
for item in iterable:
if item not in elements_to_remove_copy:
yield item
else:
elements_to_remove_copy.remove(item)
def generate_combinations(input_bags, output_bag_size):
combos_with_one_element_from_each_bag = itertools.product(*input_bags)
for base_combo in combos_with_one_element_from_each_bag:
all_objects_minus_base_combo = remove_elements(itertools.chain.from_iterable(input_bags), base_combo)
for remaining_combo in itertools.combinations(all_objects_minus_base_combo, output_bag_size-len(base_combo)):
yield Bag(itertools.chain(base_combo, remaining_combo))
input_bags = [Bag([1,2,2,3,5]), Bag([1,4,5,9]), Bag([12])]
output_bag_size = 5
combos = generate_combinations(input_bags, output_bag_size)
list(combos)
完成这一关通过错误检查码添加(如output_bag_size > = len(input_bags)。
The bene适合的方法是: 1.较少的代码维护(即itertools) 2.没有递归。 Python性能会随着调用堆栈高度而下降,因此最小化递归是有益的。 3.最小的内存消耗。该算法需要超出输入包列表消耗的恒定空间内存。