Как перечислять комбинации, фильтруя повторы

#python #recursion #combinations

Вопрос:

У меня есть список возможных вариантов: [[1], [2, 4], [4], [5, 6, 2], [5, 3]] .

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

Так [1, 2, 4, 5, 3] что это допустимый вариант. Но [1, 4, 4, 5, 3] это не так. Я разрешаю не делать выбор ни в одном подсписке, поэтому [1,4, None,5,3] действителен, как в [1, None, None, None, None] , и [None, None, None, None, None] .

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

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

редактировать: с подробностями реального случая: я хотел бы применить его к списку из 25 подсписков, каждый из которых может содержать 1-25 элементов. В действительности, каждый подсписк будет содержать не более 15 элементов, в среднем 2-4.

Таким образом, простое решение list(itertools.product(*choices)) для последующей фильтрации исключено.

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

Я попытался рекурсивно построить дерево, где, например, корневой узел имеет полный список вариантов, дочерний узел делает первый выбор [1] и имеет обновленный список вариантов, где » 1 » удаляется из всех вариантов списка[1:].

Однако я изо всех сил пытаюсь реализовать рекурсию.

Можете ли вы помочь мне с любыми другими подходами?

Комментарии:

1. Не могли бы вы предоставить реалистичный тестовый пример максимального размера?

2. Я хотел бы применить его к списку из 25 подсписков, каждый из которых может содержать 1-25 элементов (на самом деле целые числа 1-25). Реально, в каждом подсписке будет не более 15 элементов, в среднем 4-5

3. Похоже, что вас интересуют только уникальные комбинации: т. е. комбинация, берущая одну 4 из [2, 4] , совпадает с комбинацией, берущей эту единственную 4 из [4]. Правильно ли это? Также важно знать, сколько общих элементов у этих списков: они в основном разные или в основном одинаковые, или это неизвестно?

4. разве это не то, что происходит? docs.python.org/3/library/itertools.html#itertools.combinations

5. Понял, значит, возврат должен быть в основном упорядоченной последовательностью, выглядящей примерно так [None, 2, None, None, 3, ..., 20, None] ?

Ответ №1:

Другой способ генерировать все допустимые выходные данные с минимальным использованием памяти-это перебирать элементы, а не списки. Используйте поиск по глубине, чтобы с самого начала генерировать только допустимые выходные данные. Это означает, что нам нужно отслеживать три вещи на каждом уровне нашей DFS: текущий элемент, который можно добавить, списки, которые уже используются, и порядок, в котором мы использовали эти предыдущие списки.

Чтобы помочь в нашем поиске, мы предварительно обрабатываем варианты, сопоставляя каждый элемент набору возможных списков, в которых он может находиться, что создает своего рода Двойную версию проблемы. Это также генерирует списки примерно в порядке «максимального непустого выбора в первую очередь».

Поскольку вы указали, что количество уникальных элементов N равно количеству списков, этот подход выполняется со O(N * |output|) временем, и мы везде используем генераторы для экономии памяти.

 import collections
from typing import Dict, Generator, List, Optional, Set

choices = [[1], [2, 4], [4], [5, 6, 2], [5, 3]]
element_to_containing_lists: Dict[int, Set[int]] = collections.defaultdict(set)

for i, choice_list in enumerate(choices):
    for x in choice_list:
        element_to_containing_lists[x].add(i)

all_unique_elements = sorted(element_to_containing_lists)


def dfs(used_list_indices: Set[int],
        next_element_index: int,
        used_list_ordered_indices: List[Optional[int]]) -> Generator[List[Optional[int]]]:
    if next_element_index == len(all_unique_elements):
        yield used_list_ordered_indices
    else:
        # If we use the element, find an unused list index
        for possible_list_to_use in element_to_containing_lists[
                                        all_unique_elements[next_element_index]] - used_list_indices:
            yield from dfs(used_list_indices | {possible_list_to_use},
                           next_element_index   1,
                           used_list_ordered_indices   [possible_list_to_use])

        # If we don't use the element: Add None as a sentinel value
        yield from dfs(used_list_indices,
                       next_element_index   1,
                       used_list_ordered_indices   [None])


for element_to_used_list in dfs(set(), 0, []):
    list_to_chosen_element = ['N'] * len(choices)
    for x, y in zip(all_unique_elements, element_to_used_list):
        if y is not None:
            list_to_chosen_element[y] = x
    print(*list_to_chosen_element, sep='  ')
 

Первые 10 строк вывода:

 1  2  4  5  3
1  2  4  6  3
1  2  4  N  3
1  2  N  5  3
1  2  N  6  3
1  2  N  N  3
1  2  4  5  N
1  2  4  6  5
1  2  4  N  5
 

Возможно, это можно оптимизировать для выполнения во O(|output|) времени, используя битовую маску для «используемых списков», а не набор их индексов.

Комментарии:

1. Это очень умно. Хотя потребуется пара дней, чтобы переварить. Ценю это

Ответ №2:

Не превращайте результат в список. product это генератор. Используйте это в своих интересах. filter это тоже генератор. Таким образом, вы сохраняете в памяти только одну комбинацию. Иногда один вывод product будет отброшен внутренне без вашего ведома, но он не займет никакого дополнительного места.

 def criterion(x):
    k = [i for i in x if i is not None]
    return len(k) == len(set(k))

choices = [[1], [2, 4], [4], [5, 6, 2], [5, 3]]
for c in choices:
    c.append(None)
filter(criterion, product(*choices))
 

Комментарии:

1. Хотя выглядит медленно. Они сказали, что в среднем 2-4 элемента, вы добавляете по 1 к каждому, и 4^25 больше 1e15.

2. Это работает для минимального примера, который я привел — цените это. Я думаю, что @don’talkjustcode прав, хотя я запускаю его на своем полном примере, и, похоже, это займет некоторое время. Смогу ли я также отсортировать результат, выбрав (например) 100 комбинаций с наименьшим количеством None вариантов?

3. @AdZinu. Вы могли бы сделать несколько хитроумных цепочек, чтобы сначала просмотреть варианты, не связанные с отсутствием,

4. @AdZinu. Ваш набор данных огромен. Ты не сможешь уйти от этого, что бы ты ни делал. Мое решение может быть не очень быстрым, но оно в значительной степени гарантирует, что оно не забьет вашу память, независимо от того, сколько материала вы обрабатываете.

Ответ №3:

Очень похоже на предыдущий ответ. Не так быстро, но я думаю, что это легче понять. Простая рекурсивная реализация.

 def pick_all(choices):
    # The result on an empty choices list is just a single empty list
    if not choices:
        yield []
        return
    # Pluck off the first choice
    first_choice, *remainder = choices

    # Recursively find all solutions to the smaller problem.
    for result in pick_all(remainder):
        # We can always add None to the front
        yield [None, *result]
        for item in first_choice:
            # And we can add any item in this choice that's not already there.
            if item not in result:
                yield [item, *result]
 

Ответ №4:

 [set(dataset) for dataset in itertools.product(*datasets)]
 

Создает список комбинаций наборов. Наборы похожи на списки, но автоматически удаляют существующие значения. Этот код не гарантирует, что наборы имеют длину 5. Наборы заметок также неупорядочены.


 print([dataset for dataset in itertools.product(*datasets) if len(set(dataset)) == len(dataset)])
 

Список комбинаций и отфильтровывает любые списки с повторяющимися значениями, убедившись, что набор данных имеет ту же длину, что и исходный список.


 datasets = [[1], [2, 4], [4], [5, 6, 2], [5, 3]]
datasets = [[*dataset, None] for dataset in datasets]

new_dataset = []
for dataset in itertools.product(*datasets):
    without_nones = [n for n in dataset if n is not None]
    if len(without_nones) == len(set(without_nones)):
        new_dataset.append(dataset)

print(new_dataset)
 

Создает список комбинаций, длина которых всегда равна 5, не имеет повторяющихся чисел и может иметь неограниченное количество нулей.

Вероятно, это должен быть генератор, и я подозреваю, что есть более простой способ выполнить все, для чего вам нужен список в этом формате.

Комментарии:

1. Не for dataset in itertool.product(*datasets) было бы сначала перечисления, а затем фильтрации? Итак, для гораздо большего списка исходных наборов данных это будет дорого с точки зрения вычислений? В ответе MadPhysicist также использовались наборы для проверки дубликатов

2. @AdZinu Да, так и было бы. Однако, если вы допускаете неограниченное количество None s в каждом подсписке, то операция слишком сложна для компилятора списка или выражения генератора. В Python производительность не должна быть вашим главным приоритетом, и если вы так беспокоитесь о скорости (например, с большими наборами), то вам обязательно следует изучить такие модули, как numpy. Мне также любопытно, для чего вам нужны длинные списки 25^25.

3. Больше похоже на 4^25, так как не в каждом подсписке есть полные 25 элементов. Вполне возможно, что то, как я сформулировал вопрос, — это совершенно неправильный подход. Однако потребуется пара дней, чтобы переварить ответы. Ценю помощь от всех