Комбинации взвешенных элементов в наборе, где взвешенная сумма равна фиксированному целому числу (в питоне) - PullRequest
2 голосов
/ 04 ноября 2011

Я хотел бы найти все возможные комбинации взвешенных элементов в наборе, где сумма их весов точно равна заданному весу W

Скажем, я хочу выбрать kэлементы из набора { 'A', 'B', 'C', 'D', 'E' }, где 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 (не ниже).

Я хочу реализовать это вPython, но любые подсказки теории cs помогут.Бонусные баллы за элегантность!

Ответы [ 3 ]

3 голосов
/ 04 ноября 2011

Все циклы 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 одним на sorted.

1 голос
/ 04 ноября 2011

Есть ли у вас верхняя граница количества предметов в таких наборах?Если вы это сделаете, и его значение будет не более 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
0 голосов
/ 04 ноября 2011

Хитрость в том, чтобы сделать это (несколько) эффективно, состоит в том, чтобы создать наборы элементов, которые имеют одинаковый вес, используя первые k элементов.

Начните с пустого набора в k = 0, затем создайте свои комбинации дляk с использованием комбинаций из k-1.Если у вас не может быть отрицательных весов, вы можете обрезать комбинации с общим весом больше W.

Вот как это выглядит на вашем примере:

comb [k, w] - наборэлементов с общим весом w с использованием первых k элементов.
Для наборов используются фигурные скобки.
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'}

Тогда ответом является гребень [5,4], который упрощается до:

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

Предоставление всех комбинаций.

...