加权元素组合,权重总和等于固定整数(用 Python 实现)
我想找到一个集合中所有可能的加权元素组合,这些组合的权重总和恰好等于一个给定的权重W。
比如,我想从集合{ 'A', 'B', 'C', 'D', 'E' }
中选择k个元素,权重是weights = {'A':2, 'B':1, 'C':3, 'D':2, 'E':1}
,而W = 4
。
这样的话,可能的组合有:
('A','B','E')
('A','D')
('B','C')
('B','D','E')
('C','E')
我知道一种简单的方法是找出给定集合的所有排列(用itertools.permutations
),然后从中挑出前k个元素,看看它们的权重总和是否等于W。但我处理的集合至少有20个元素,这样做会非常耗费计算资源。
我觉得可以用一种变体的背包问题来解决这个问题,只考虑权重(而不是价值),并且权重的总和必须等于W(而不是小于W)。
我想用Python来实现这个,但任何计算机科学理论的提示都会很有帮助。如果能优雅地实现,那就更好了!
3 个回答
要高效地完成这个任务,有个小窍门,就是利用前面 k 个物品来创建一些总重量相同的元素集合。
从 k=0 开始,先创建一个空集合,然后通过从 k-1 的组合来生成 k 的组合。除非你能有负重量,否则可以把总重量超过 W 的组合去掉。
下面用你的例子来说明这个过程:
comb[k,w] 表示用前 k 个元素组成的,总重量为 w 的元素集合。
大括号用来表示集合。
S+e 是通过把元素 e 加到 S 中每个成员上来创建的集合。
comb[0,0]={}
comb[1,0]={comb[0,0]}
comb[1,2]={comb[0,0]+'A'}
comb[2,0]={comb[1,0]}
comb[2,1]={comb[1,0]+'B'}
comb[2,2]={comb[1,2]}
comb[2,3]={comb[1,2]'B'}
comb[3,0]={comb[2,0]}
comb[3,1]={comb[2,1]}
comb[3,2]={comb[2,2]}
comb[3,3]={comb[2,3],comb[2,0]+'C'}
comb[3,4]={comb[2,3]+'C'}
comb[4,0]={comb[3,0]}
comb[4,1]={comb[3,1]}
comb[4,2]={comb[3,2],comb[3,0]+'D'}
comb[4,3]={comb[3,3],comb[3,1]+'D'}
comb[4,4]={comb[3,4],comb[3,2]+'D'}
comb[5,0]={comb[4,0]}
comb[5,1]={comb[4,1],comb[4,0]+'E'}
comb[5,2]={comb[4,2],comb[4,1]+'E'}
comb[5,3]={comb[4,3],comb[4,2]+'E'}
comb[5,4]={comb[4,4],comb[4,3]+'E'}
最后的结果就是 comb[5,4],可以简化为:
{
{{'B'}+'C'},
{{'A'}+'D'},
{
{{'A'}+'B'},
{'C'},
{'B'}+'D'
}+'E'
}
这样就得到了所有的组合。
你有没有想过这些集合里最多能有多少个项目?如果你知道这个数量不超过大约40个,那么可以使用一种叫做“中间相遇算法”的方法,这种算法在维基百科的“背包问题”页面上有介绍。这个算法比起直接暴力计算要简单得多,复杂度也低很多。
注意:如果使用比Python字典更节省内存的数据结构,这个方法也可以适用于更大的集合。一个高效的实现应该能够轻松处理大小为60的集合。
下面是一个示例实现:
from collections import defaultdict
from itertools import chain, combinations, product
# taken from the docs of the itertools module
def powerset(iterable):
"powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
s = list(iterable)
return chain.from_iterable(combinations(s, r) for r in xrange(len(s) + 1))
def gen_sums(weights):
"""Given a set of weights, generate a sum --> subsets mapping.
For each posible sum, this will create a list of subsets of weights
with that sum.
>>> gen_sums({'A':1, 'B':1})
{0: [()], 1: [('A',), ('B',)], 2: [('A', 'B')]}
"""
sums = defaultdict(list)
for weight_items in powerset(weights.items()):
if not weight_items:
sums[0].append(())
else:
keys, weights = zip(*weight_items)
sums[sum(weights)].append(keys)
return dict(sums)
def meet_in_the_middle(weights, target_sum):
"""Find subsets of the given weights with the desired sum.
This uses a simplified meet-in-the-middle algorithm.
>>> weights = {'A':2, 'B':1, 'C':3, 'D':2, 'E':1}
>>> list(meet_in_the_middle(weights, 4))
[('B', 'E', 'D'), ('A', 'D'), ('A', 'B', 'E'), ('C', 'B'), ('C', 'E')]
"""
# split weights into two groups
weights_list = weights.items()
weights_set1 = dict(weights_list[:len(weights)//2])
weights_set2 = dict(weights_list[len(weights_set1):])
# generate sum --> subsets mapping for each group of weights,
# and sort the groups in descending order
set1_sums = sorted(gen_sums(set1).items())
set2_sums = sorted(gen_sums(set2).items(), reverse=True)
# run over the first sorted list, meanwhile going through the
# second list and looking for exact matches
set2_sums = iter(set2_sums)
try:
set2_sum, subsets2 = set2_sums.next()
for set1_sum, subsets1 in set1_sums:
set2_target_sum = target_sum - set1_sum
while set2_sum > set2_target_sum:
set2_sum, subsets2 = set2_sums.next()
if set2_sum == set2_target_sum:
for subset1, subset2 in product(subsets1, subsets2):
yield subset1 + subset2
except StopIteration: # done iterating over set2_sums
pass
遍历所有的 n! 排列组合太耗费资源了。我们可以改为生成所有的 2^n 子集。
from itertools import chain, combinations
def weight(A):
return sum(weights[x] for x in A)
# Copied from example at http://docs.python.org/library/itertools.html
def powerset(iterable):
"powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
s = list(iterable)
return chain.from_iterable(combinations(s, r) for r in xrange(len(s) + 1))
[x for x in powerset({'A', 'B', 'C', 'D', 'E'}) if weight(x) == W]
这样可以得到
[('A', 'D'), ('C', 'B'), ('C', 'E'), ('A', 'B', 'E'), ('B', 'E', 'D')]
我们可以通过把列表推导式的返回部分改成 tuple(sorted(x))
,或者把 powerset
中的 list
调用换成 sorted
来将其转换为排序后的元组。