Я пытаюсь создать ряд ограниченных перестановок списка элементов. У каждого элемента есть категория, и мне нужно найти такие комбинации элементов, чтобы в каждой комбинации не было нескольких элементов из одной и той же категории. Для иллюстрации вот несколько примеров данных:

   Name      | Category
   ==========|==========
1. Orange    | fruit
2. Apple     | fruit
3. GI-Joe    | toy
4. VCR       | electronics
5. Racquet   | sporting goods

Комбинации будут ограничены длиной три, мне не нужны все комбинации любой длины. Итак, набор комбинаций для приведенного выше списка может быть:

(Orange, GI-Joe, VCR)
(Orange, GI-Joe, Racquet)
(Orange, VCR, Racquet)
(Apple,  GI-Joe, VCR)
(Apple,  GI-Joe, Racquet)
... and so on.

Я делаю это довольно часто, по разным спискам. Списки никогда не будут содержать более 40 элементов, но понятно, что это может создать тысячи комбинаций (хотя, вероятно, будет около 10 уникальных категорий для каждого списка, что несколько ограничивает его).

Я придумал псевдопитон для того, чтобы реализовать его рекурсивно. Прошло слишком много времени с тех пор, как я изучал комбинаторику, но насколько я помню, это, по сути, подмножество комбинаций набора, что-то вроде C (длина списка, желаемый размер). Вероятно, есть некоторые библиотечные модули, которые могут сделать это чище (или, по крайней мере, более производительно)

Мне было интересно, может быть, есть подход лучше, чем то, что у меня есть (возможно, тот, который как-то использует itertools.combinations):

# For the sake of this problem, let's assume the items are hashable so they
# can be added to a set.

def combinate(items, size=3):
    assert size >=2, "You jerk, don't try it."
    def _combinate(index, candidate):
        if len(candidate) == size:
            results.add(candidate)
            return
        candidate_cats = set(x.category for x in candidate)
        for i in range(index, len(items)):
            item = items[i]
            if item.category not in candidate_cats:
                _combinate(i, candidate + (item, ))

    results = set()
    for i, item in enumerate(items[:(1-size)]):
        _combinate(i, (item, ))

    return results
4
Crast 10 Сен 2010 в 20:44

2 ответа

Лучший ответ

Наивный подход:

#!/usr/bin/env python

import itertools

items = {
    'fruits' : ('Orange', 'Apple'),
    'toys' : ('GI-Joe', ),
    'electronics' : ('VCR', ),
    'sporting_goods' : ('Racquet', )
}

def combinate(items, size=3):
    if size > len(items):
        raise Exception("Lower the `size` or add more products, dude!")

    for cats in itertools.combinations(items.keys(), size):
        cat_items = [[products for products in items[cat]] for cat in cats]
        for x in itertools.product(*cat_items):
            yield zip(cats, x)

if __name__ == '__main__':
    for x in combinate(items):
        print x

Будет давать:

# ==> 
#
# [('electronics', 'VCR'), ('toys', 'GI-Joe'), ('sporting_goods', 'Racquet')]
# [('electronics', 'VCR'), ('toys', 'GI-Joe'), ('fruits', 'Orange')]
# [('electronics', 'VCR'), ('toys', 'GI-Joe'), ('fruits', 'Apple')]
# [('electronics', 'VCR'), ('sporting_goods', 'Racquet'), ('fruits', 'Orange')]
# [('electronics', 'VCR'), ('sporting_goods', 'Racquet'), ('fruits', 'Apple')]
# [('toys', 'GI-Joe'), ('sporting_goods', 'Racquet'), ('fruits', 'Orange')]
# [('toys', 'GI-Joe'), ('sporting_goods', 'Racquet'), ('fruits', 'Apple')]
2
miku 10 Сен 2010 в 20:39

Вы стремитесь создать декартово произведение элементов, взятых из набора из category.

Разделить на несколько наборов относительно просто:

item_set[category].append(item)

При правильном создании экземпляра (например, collections.defaultdict для {{X0}) }, а затем itertools.product даст вам желаемый результат.

1
msw 10 Сен 2010 в 16:56