БЫСТРЫЕ уникальные комбинации (из списка с дубликатами) БЕЗ ЛУКОВ

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

Вопрос заключается в том, как генерировать ON-THE-FLY в функции генератора все уникальные комбинации из уникального списка безвычислительная дорогостоящая потребность в фильтрации дубликатов?

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

Сначала дайте код, иллюстрирующий, как проверить, является ли комбинация comboB дубликат другого (comboA):

comboA = [1,2,2]
comboB = [2,1,2]
print("B is a duplicate of A:", comboA.sort()==comboB.sort())

В данном примере B - это дубликат A, а print() печатает True.

Проблема получения функции генератора, способной обеспечить уникальные комбинации "на лету" в случае неуникального списка, решается здесь: Получение уникальных комбинаций из уникального списка элементов, FASTER?, но предоставленная функция генератора нуждается в поиске и требует памяти, что вызывает проблемы в случае огромного количества комбинаций.

В текущей версии предоставленной функции ответа функция выполняет работу без каких-либо поисков и представляется правильным ответом здесь, НО...

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

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

Здесь приведены некоторые тайминги для иллюстрации текущей ситуации:

-----------------
 k: 6 len(ls): 48
Combos   Used Code                               Time
---------------------------------------------------------
12271512 len(list(combinations(ls,k)))       :  2.036 seconds
12271512 len(list(subbags(ls,k)))            : 50.540 seconds
12271512 len(list(uniqueCombinations(ls,k))) :  8.174 seconds
12271512 len(set(combinations(sorted(ls),k))):  7.233 seconds
---------------------------------------------------------
12271512 len(list(combinations(ls,k)))       :  2.030 seconds
       1 len(list(subbags(ls,k)))            :  0.001 seconds
       1 len(list(uniqueCombinations(ls,k))) :  3.619 seconds
       1 len(set(combinations(sorted(ls),k))):  2.592 seconds

Над таймингами показаны две крайности: нет дубликатов и только дубликатов. Все остальные тайминги находятся между этими двумя.

Моя интерпретация приведенных выше результатов заключается в том, что чистая функция Python (без itertools или других C-скомпилированных модулей) может быть чрезвычайно быстрой, но она может быть намного медленнее в зависимости от того, сколько дубликатов в списке. Таким образом, возможно, нет возможности писать код на С++ для модуля расширения Python.so, обеспечивающего требуемую функциональность.

Ответ 1

Вместо последующей обработки/фильтрации вашего вывода вы можете предварительно обработать свой список ввода. Таким образом, вы можете избежать генерации дубликатов в первую очередь. Предварительная обработка включает в себя либо сортировку (или использование collections.Counter on) ввода. Возможна одна рекурсивная реализация:

def subbags(bag, k):
    a = sorted(bag)
    n = len(a)
    sub = []

    def index_of_next_unique_item(i):
        j = i + 1

        while j < n and a[j] == a[i]:
            j += 1

        return j

    def combinate(i):
        if len(sub) == k:
            yield tuple(sub)
        elif n - i >= k - len(sub):
            sub.append(a[i])
            yield from combinate(i + 1)
            sub.pop()
            yield from combinate(index_of_next_unique_item(i))

    yield from combinate(0)

bag = [1, 2, 3, 1, 2, 1]
k = 3
i = -1

print(sorted(bag), k)
print('---')

for i, subbag in enumerate(subbags(bag, k)):
    print(subbag)

print('---')
print(i + 1)

Вывод:

[1, 1, 1, 2, 2, 3] 3
---
(1, 1, 1)
(1, 1, 2)
(1, 1, 3)
(1, 2, 2)
(1, 2, 3)
(2, 2, 3)
---
6

Требуется некоторое пространство стека для рекурсии, но при этом + сортировка ввода должна использовать существенно меньшее время + память, чем генерация и отбрасывание повторов.

Ответ 2

Современное состояние, вдохновленное первоначально 50, а не 100-кратным бонусами, на данный момент (вместо модуля расширения Python, полностью написанного на C):

Эффективный алгоритм и реализация, который лучше, чем очевидный подход (set + combinations) в лучшем (и среднем) случае, и конкурирует с ним в худшем случае.

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

def iterFastUniqueCombos(lstList, comboSize, percUniqueThresh=60):

    lstListSorted = sorted(lstList)
    lenListSorted = len(lstListSorted)

    percUnique = 100.0 - 100.0*(lenListSorted-len(set(lstListSorted)))/lenListSorted

    lstComboCandidate = []
    setUniqueCombos = set()

    def idxNextUnique(idxItemOfList):
        idxNextUniqueCandidate = idxItemOfList + 1
        while (
                idxNextUniqueCandidate < lenListSorted 
                    and 
                lstListSorted[idxNextUniqueCandidate] == lstListSorted[idxItemOfList]
        ): # while
            idxNextUniqueCandidate += 1
        idxNextUnique = idxNextUniqueCandidate
        return idxNextUnique

    def combinate(idxItemOfList):
        if len(lstComboCandidate) == sizeOfCombo:
            yield tuple(lstComboCandidate)
        elif lenListSorted - idxItemOfList >= sizeOfCombo - len(lstComboCandidate):
            lstComboCandidate.append(lstListSorted[idxItemOfList])
            yield from combinate(idxItemOfList + 1)
            lstComboCandidate.pop()
            yield from combinate(idxNextUnique(idxItemOfList))

    if percUnique > percUniqueThresh:
        from itertools import combinations
        allCombos = combinations(lstListSorted, comboSize)
        for comboCandidate in allCombos:
            if comboCandidate in setUniqueCombos:
                continue
            yield comboCandidate
            setUniqueCombos.add(comboCandidate)
    else:
        yield from combinate(0)
    #:if/else    
#:def iterFastUniqueCombos()

Ниже приведенные тайминги показывают, что приведенная выше функция генератора iterFastUniqueCombos() обеспечивает явное преимущество over uniqueCombinations() в случае, если список содержит менее 60 процентов уникальных элементов и не хуже, чем на функции (set + combinations) на основе (set + combinations) на основе uniqueCombinations() в противоположном случае, когда он становится намного быстрее, чем iterUniqueCombos() один (из-за переключения между вариант (set + combinations) и (no lookups) с порогом 60% для количества уникальных элементов в списке):

===========  sizeOfCombo: 6   sizeOfList: 48   noOfUniqueInList 1   percUnique   2
Combos: 12271512  print(len(list(combinations(lst,k))))           :   2.04968 seconds.
Combos:        1  print(len(list(      iterUniqueCombos(lst,k)))) :   0.00011 seconds.
Combos:        1  print(len(list(  iterFastUniqueCombos(lst,k)))) :   0.00008 seconds.
Combos:        1  print(len(list(    uniqueCombinations(lst,k)))) :   3.61812 seconds.

==========  sizeOfCombo: 6   sizeOfList: 48   noOfUniqueInList 48   percUnique 100
Combos: 12271512  print(len(list(combinations(lst,k))))           :   1.99383 seconds.
Combos: 12271512  print(len(list(      iterUniqueCombos(lst,k)))) :  49.72461 seconds.
Combos: 12271512  print(len(list(  iterFastUniqueCombos(lst,k)))) :   8.07997 seconds.
Combos: 12271512  print(len(list(    uniqueCombinations(lst,k)))) :   8.11974 seconds.

==========  sizeOfCombo: 6   sizeOfList: 48   noOfUniqueInList 27   percUnique  56
Combos: 12271512  print(len(list(combinations(lst,k))))           :   2.02774 seconds.
Combos:   534704  print(len(list(      iterUniqueCombos(lst,k)))) :   1.60052 seconds.
Combos:   534704  print(len(list(  iterFastUniqueCombos(lst,k)))) :   1.62002 seconds.
Combos:   534704  print(len(list(    uniqueCombinations(lst,k)))) :   3.41156 seconds.

==========  sizeOfCombo: 6   sizeOfList: 48   noOfUniqueInList 31   percUnique  64
Combos: 12271512  print(len(list(combinations(lst,k))))           :   2.03539 seconds.
Combos:  1114062  print(len(list(      iterUniqueCombos(lst,k)))) :   3.49330 seconds.
Combos:  1114062  print(len(list(  iterFastUniqueCombos(lst,k)))) :   3.64474 seconds.
Combos:  1114062  print(len(list(    uniqueCombinations(lst,k)))) :   3.61857 seconds.