集合中加权元素的组合,其中加权和等于固定整数(在Python中)

发布于 2024-12-13 23:40:29 字数 527 浏览 0 评论 0原文

我想找到一个集合中加权元素的所有可能组合,其中它们的权重总和恰好等于给定的权重 W

假设我想从集合中选择 k 个元素 { 'A', 'B', 'C', 'D', 'E' } 其中 权重 = {'A':2, 'B':1, 'C':3, ' D':2, 'E':1}W = 4

那么这会产生: <代码>('A','B','E') ('广告') ('公元前') ('B'、'D'、'E') ('C','E')

我意识到蛮力方法是找到给定集合的所有排列(使用 itertools.permutations)并拼接出前 k 个元素W 的加权和。但我每组至少处理 20 个元素,这在计算上会很昂贵。

我认为使用背包的变体会有所帮助,其中仅考虑重量(而不是价值),并且重量之和必须等于 W(不劣于)。

我想在 python 中实现这个,但是任何 cs 理论提示都会有帮助。优雅加分!

I would like to find all the possible combinations of weighted elements in a set where the sum of their weights is exactly equal to a given weight W

Say I want to select k elements from the set { 'A', 'B', 'C', 'D', 'E' } where weights = {'A':2, 'B':1, 'C':3, 'D':2, 'E':1} and W = 4.

Then this would yield :
('A','B','E')
('A','D')
('B','C')
('B','D','E')
('C','E')

I realize the brute force way would be to find all permutations of the given set (with itertools.permutations) and splice out the first k elements with a weighted sum of W. But I'm dealing with at least 20 elements per set, which would be computationally expensive.

I think using a variant of knapsack would help, where only weight (not value) is considered and where the sum of weights must be equal to W (not inferior).

I want to implement this in python but any cs-theory hints would help. Bonus points for elegance!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

琉璃梦幻 2024-12-20 23:40:29

循环遍历所有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)) 或替换 中的 list 调用来将其转换为排序元 powerset 用 1 来排序

Looping through all n! permutations is much too expensive. Instead, generate all 2^n subsets.

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]

yields

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

This can be converted to sorted tuples by changing the return part of the list comprehension to tuple(sorted(x)), or by replacing the list call in powerset with one to sorted.

爱殇璃 2024-12-20 23:40:29

您对此类集合中的项目数量有上限吗?如果您这样做,并且最多约为 40 个,则 "meet-in-the -middle”算法,如 Knapsack 上的维基百科页面 可以非常简单,并且比暴力计算的复杂性要低得多。

注意:使用比 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

Do you have an upper bound on the number of items in such sets? If you do and it is at most about 40, then the "meet-in-the-middle" algorithm as described in the Wikipedia page on Knapsack can be quite simple and has significantly lower complexity than a brute-force computation.

Note: Using a more memory-efficient data structure than a Python dict, this could also work on larger sets. An efficient implementation should easily handle sets of size 60.

Here is a sample implementation:

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
二智少女 2024-12-20 23:40:29

(在某种程度上)有效地做到这一点的技巧是使用前 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'
}

给出所有组合。

The trick to doing this (somewhat) efficiently is to create sets of elements that have the same weight using the first k items.

Start with an empty set at k=0, then create your combinations for k using combinations from k-1. Unless you can have negative weights, you can prune combinations with a total weight greater than W.

Here is how it plays out using your example:

comb[k,w] is the set of elements having a total weight w using the first k elements.
Braces are used for sets.
S+e is the set of sets created by adding element e to each member of 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'}

The answer is then comb[5,4], which simplifies to:

{
  {{'B'}+'C'},
  {{'A'}+'D'},
  {
    {{'A'}+'B'},
    {'C'},
    {'B'}+'D'             
  }+'E'
}

Giving all the combinations.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文