Нет общего решения k-комбинации мультимножества?


Проблема, которую нужно решить

Решите k-комбинацию, входные данные которой могут содержать повторяющиеся элементы, и верните только уникальные комбинации.

например, ввод [0,1,2,2,4], k = 2, результат:

{(0, 1), (2, 4), (1, 2), (0, 4), (1, 4), (0, 2), (2, 2)}


Решение, о котором я могу думать

Единственное общее решение, которое я могу придумать, — это использовать set или map для извлечения уникальных результатов.

например с питоном

from itertools import combinations
print (set(combinations([0, 1, 2, 2, 4], 2)))

Вопросы

  • Есть ли общее решение для этого без использования набора/карты для фильтрации.

Я могу генерировать комбинации уникальных элементов по порядку (см.: https://stackoverflow.com/a/76048486), но если есть повторяющиеся элементы, то будут повторяющиеся комбинации.

Я читал это: https://math.stackexchange.com/a/554237 Кажется, там говорится, что нет общего решения для получения всех уникальных комбинаций, хотя есть формула для подсчета уникальных комбинаций.

Но я не уверен.


@Update — Резюме и советы

  • Согласно ответам, есть способы сделать это как с помощью итерации, так и рекурсии.
  • При переводе с python на go:
    • yield в python нужно chan в go, или вам нужно вернуть все комбинации (что может быть огромным для большого ввода).
    • else Python для for и while опускается, когда break.
    • list slicing в python создаст копию (ссылки?), в то время как нарезка слайса go будет повторно использовать тот же базовый массив (так что в go, если копируемый слайс будет изменен позже, и вы не хотите, чтобы это изменение в копию, то вы должны скопировать/добавить ее в новый фрагмент).

@JosephWood Это вопрос algorithm, и я не вижу подходящего ответа. (Кстати, я совершил ту же ошибку, пометил как дубликат этого и отказался, когда понял, что это не python вопрос...)

Kelly Bundy 20.04.2023 02:00

@KellyBundy, кстати, не знал о more_itertools.distinct_combinations. Я должен проверить это.

Joseph Wood 20.04.2023 02:44

@JosephWood Для меня sympy.multiset_combinations не звучит обычно. Можно ли использовать in в Java или Go?

Kelly Bundy 20.04.2023 02:50

Использование "set" значительно упрощает задачу, устраняет ненужную сложность, так что это жизнеспособный способ.

MBo 20.04.2023 05:23

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

Eric 20.04.2023 09:42

@Eric Усилия по предотвращению дублирования для большого массива могут занять гораздо больше времени. Конечно, это необходимо для оценки баланса. Например, мы можем сортировать элементы, чтобы упростить предотвращение дублирования использования, но сама сортировка сравнима с построением множества.

MBo 20.04.2023 09:57

@MBo В случае перестановки существует алгоритм предотвращения дублирования в природе, без дополнительного времени или пространства, он даже поддерживает порядок. Проверьте мой ответ с кодом: stackoverflow.com/a/75894614 , а алгоритм объясняется здесь: en.wikipedia.org/wiki/… Хотя я не уверен, можно ли добиться того же для комбинации , и вопрос был задан с целью получить ответ или совет.

Eric 20.04.2023 10:00

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

MBo 20.04.2023 10:17
здесь
MBo 20.04.2023 10:24

@MBo Set работает, я использовал set() в самом вопросе в примере с Python, он работает хорошо. И, я думаю, stackoverflow.com/questions/75887690 это решение для перестановки не включает дублированную перестановку.

Eric 20.04.2023 16:46

@ Эрик Ага, вы используете множество вместо результирующих комбинаций - определенно излишне для больших списков.

MBo 20.04.2023 17:04

@MBo Например, мы можем поместить set внутрь функции и проверить цикл при создании списка возврата. Так что размер набора будет таким же, как и возвращаемый список.

Eric 20.04.2023 17:06

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

MBo 20.04.2023 17:11
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
14
150
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вот код, который генерирует их по одному в Python.

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

def unique_multisets (multiset, k):
    # Find the repetition of each item.
    count = {}
    for x in multiset:
        count[x] = 1 + count.get(x, 0)

    # Turn this into (value, count) pairs.  The reversed is
    # because we will reverse it later in generating the multisets.
    pairs = list(reversed(count.items()))

    # Calculate a running total.
    total = 0
    totals = []
    for p in pairs:
        total += p[1]
        totals.append(total)

    def recurse (partial, idx):
        if k == len(partial):
            yield partial
        elif idx < 0 or totals[idx] < k - len(partial):
            pass # Can't make combinations.
        else:
            (value, repeat) = pairs[idx]
            max_used = min(repeat, k - len(partial))
            for i in range(1 + max_used):
                partial.append(value)
            for i in range(1 + max_used):
                partial.pop()
                yield from recurse(partial, idx - 1)

    yield from recurse([], len(pairs)-1)

for x in unique_multisets([0, 1, 2, 2, 4], 2):
    print(x)

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

Eric 20.04.2023 09:56

@ Эрик Это идет другим путем. Каждую итерацию можно переписать как рекурсию. Но есть рекурсивные функции, которые нельзя переписать как итерацию, стандартным примером является функция Аккермана. Я могу решить это итеративно, но это будет совсем другое решение.

btilly 20.04.2023 15:49

@btilly Это звучит неправильно. Вы имеете в виду какую-то ограниченную «итерацию»?

Kelly Bundy 20.04.2023 16:12

@KellyBundy, именно так: вы можете итеративно вычислять любую рекурсивную функцию, эмулируя стек вызовов с соответствующим сегментированием точек входа/выхода. Примитивно-рекурсивные функции не требуют этого «хака» — вы можете определить количество циклов перед входом в цикл. Непримитивные рекурсивные функции (например, функция Аккермана) требуют явного стека.

Dillon Davis 20.04.2023 16:27

@KellyBundy Я имел в виду «кучу for петель». С помощью стека вызовов вы можете эмулировать что угодно — в первую очередь именно так мы можем запускать рекурсивные функции.

btilly 20.04.2023 16:52

Хм, "связка петель" пока неясна. Но я сомневаюсь, что @Eric имел в виду то, что ты имеешь в виду.

Kelly Bundy 20.04.2023 17:00

Согласно stackoverflow.com/questions/931762, в нескольких ответах говорится, что каждую рекурсивную функцию можно записать в виде итеративной.

Eric 21.04.2023 11:48

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

def combinations(a, k):
    a = sorted(a)
    while True:
        yield a[:k]
        for i in range(k - 1, -1, -1):
            # a[i + 1 :] is sorted. Sort a[i:].
            x = a[i]
            j = i + 1
            while j < len(a):
                if x < a[j]:
                    break
                a[j - 1] = a[j]
                j += 1
            a[j - 1] = x
            # a[j] is the next greatest element after x. If there are enough
            # elements after it, rotate them into position.
            if j + (k - i) <= len(a):
                rotate(a, i, j, j + (k - i))
                break
        else:
            break


def rotate(a, i, j, k):
    reverse(a, i, j)
    reverse(a, j, k)
    reverse(a, i, k)


def reverse(a, i, k):
    for j in range((k - i) // 2):
        a[i + j], a[(k - 1) - j] = a[(k - 1) - j], a[i + j]


print(*combinations([0, 1, 2, 2, 4], 2))
print(*combinations([0, 1, 2, 2, 4], 3))
print(*combinations([0, 0, 0, 1, 1, 1], 3))

Выход:

[0, 1] [0, 2] [0, 4] [1, 2] [1, 4] [2, 2] [2, 4]
[0, 1, 2] [0, 1, 4] [0, 2, 2] [0, 2, 4] [1, 2, 2] [1, 2, 4] [2, 2, 4]
[0, 0, 0] [0, 0, 1] [0, 1, 1] [1, 1, 1]

Я перевел ваш ответ: go.dev/play/p/RyanyA38Vuc как общую версию. Еще один go-перевод, отправляющий комбинации на канал: go.dev/play/p/P1_ik7w8j-Q, что больше похоже на функцию yield в python.

Eric 21.04.2023 11:29
Ответ принят как подходящий

Ниже приведен алгоритм, который я разработал некоторое время назад для этой задачи:

def multiset_combinations(v, m):
    
    v = sorted(v)
    f = [0]
    j = 0

    # Populating f, a list of expanded indices. E.g. For
    #
    #    v = [2, 2, 33, 33, 33, 40]
    #
    # f would be:
    #
    #    [0, 0, 1, 1, 1, 2]
    
    for i in range(1, len(v)):
        if v[i] != v[i - 1]:
            j += 1

        f.append(j)

    v = list(set(v))
    r = [0] * m
    n = len(v)
    
    z   = f[:m]
    idx = [0] * n
    p   = len(f) - m
    
    last = f[-m:]
    last[-1] += 1
    
    # Find the first occurence of each index. E.g. For the
    # same example above, idx would be:
    #
    #   [0, 2, 5]
    
    for i in range(n):
        idx[i] = f.index(i)
    
    # The main algorithm

    while True:
        while z[-1] < n:
            for j in range(m):
                r[j] = v[z[j]]

            z[-1] += 1
            yield r.copy()

        if z == last:
            break
        
        for i in range(m - 2, -1, -1):
            if z[i] != f[p + i]:
                z[i] += 1
                
                for j, k in zip(range(i + 1, m),
                                range(idx[z[i]] + 1, idx[z[i]] + m)):
                    z[j] = f[k]
                
                break

Вызов:

print(*multiset_combination([0, 1, 2, 2, 4], 2))
#> [0, 1] [0, 2] [0, 4] [1, 2] [1, 4] [2, 2] [2, 4]

print(*multiset_combination([0, 1, 2, 2, 4], 3))
#> [0, 1, 2] [0, 1, 4] [0, 2, 2] [0, 2, 4] [1, 2, 2] [1, 2, 4] [2, 2, 4]

print(*multiset_combination([0, 0, 0, 1, 1, 1], 3))
#> [0, 0, 0] [0, 0, 1] [0, 1, 1] [1, 1, 1]

Это также довольно эффективно. Он может генерировать 753564 менее чем за секунду:

v1 = [1,
      2, 2,
      3, 3, 3,
      4, 4, 4, 4,
      5, 5, 5, 5, 5,
      6,
      7, 7,
      8, 8, 8,
      9, 9, 9, 9,
      10, 10, 10, 10, 10,
      11,
      12, 12,
      13, 13, 13,
      14, 14, 14, 14,
      15, 15, 15, 15, 15]
      
def get_time(v, m):
    t1 = time.time()
    list(multiset_combinations(v, m))
    t2 = time.time()
    return t2 - t1
    
get_time(v1, 10)
#> 0.8702290058135986

Ваша версия Python заканчивается на 1.38 s, версия из другого ответа stackoverflow.com/a/76064642 заканчивается на 9.93 s на моем ноутбуке, так что ваша действительно быстрее. Хотя go намного быстрее, чем кажется python.

Eric 21.04.2023 15:33

Я перевел ваш ответ на go: go.dev/play/p/jdQvr6c5b0g , он заканчивается на 0.12 s без печати, а мой перевод другого ответа go.dev/play/p/RyanyA38Vuc занимает 0.20 s на моем ноутбуке. Так что ваш все равно быстрее. Кстати, приведенная выше ссылка не может закончиться в go.dev/play, вероятно, из-за того, что она требует слишком много ресурсов, ее нужно запускать локально.

Eric 21.04.2023 20:06

Я добавил еще один перевод go, который отправляет комбинации на канал go, который больше похож на yield в python: go.dev/play/p/3WivvzV2E0u, теперь его можно запускать онлайн, он занимает 0.15 s на моем ноутбуке, и он общий. .

Eric 21.04.2023 20:29

Другие вопросы по теме