Пересекающиеся множества таким образом, что результатом является набор множеств с коллективно уникальными элементами

#algorithm #set

#алгоритм #множество

Вопрос:

Допустим, у меня есть следующие наборы:

 X -> {1, 2, 3}  
Y -> {1, 4, 7}  
Z -> {1, 4, 5}
  

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

 A -> {2, 3}: {X}
B -> {7}:    {Y}
C -> {5}:    {Z}
D -> {4}:    {Y, Z}
E -> {1}:    {X, Y, Z}
  

Если свести проблему к минимуму, то должны быть выполнены следующие условия:

  • Для каждого начального набора каждый элемент будет находиться в результирующем наборе, созданном пересечением максимального числа начальных наборов
  • Это означает, что каждый элемент в исходном наборе должен быть ровно в одном результирующем наборе
  • Множества реально бесконечны, что означает, что пошаговый переход по всем допустимым элементам невозможен, но операции с наборами в порядке
  • Все результирующие множества, не содержащие элементов, могут быть проигнорированы

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

 resulting_sets = {}
for sets in powerset(S):
  s = intersection(sets)
  for rs in resulting_sets.keys():
    s -= rs

  if not s.empty():
    resulting_sets[s] = sets # realistically some kind of reference to sets
  

Конечно, вышеописанное довольно неэффективно при O (n ^ 2log (n)) O (2 ^ n * 2 ^ (n / 2)) операциях набора (и для моих целей это может выполняться уже до n ^ 2 раз). Есть ли лучшее решение для такого типа проблем?

Ответ №1:

ОБНОВЛЕНИЕ: не повторяет ни одного набора, использует только операции набора

Этот алгоритм создает результирующие наборы конструктивно, т. е. мы изменяем существующие уникальные наборы элементов и / или добавляем новые каждый раз, когда видим новый исходный набор.

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

Для сложности с точки зрения операций с множествами это должно быть O (n * 2 ^ n). Для решения, опубликованного OP, я думаю, сложность должна быть O (2 ^ (2n)), потому что len(resulting_sets) в худшем случае содержит до 2 ^ n элементов.

 def solution(sets):
    result_sets = [] # list of (unique element set, membership) tuples
    for sid, s in enumerate(sets):
        new_sets = []
        for unique_elements, membership in result_sets:
            # The intersect part has wider membership, while the other part
            # has less unique elements (maybe empty).
            # Wider membership must have not been seen before, so add as new.
            intersect = unique_elements amp; s
            # Special case if all unique elements exist in s, then update
            # in place
            if len(intersect) == len(unique_elements):
                membership.append(sid)
            elif len(intersect) != 0:
                unique_elements -= intersect
                new_sets.append((intersect, membership   [sid]))
            s -= intersect
            if len(s) == 0:
                break
        # Special syntax for Python: there are remaining elements in s
        # This is the part of unseen elements: add as a new result set
        else:
            new_sets.append((s, [sid]))
        result_sets.extend(new_sets)
    print(result_sets)

sets = [{1, 2, 3}, {1, 4, 7}, {1, 4, 5}]
solution(sets)

# output:
# [(set([2, 3]), [0]), (set([1]), [0, 1, 2]), (set([7]), [1]), (set([4]), [1, 2]), (set([5]), [2])]

  

————— оригинальный ответ ниже —————

Идея состоит в том, чтобы найти «принадлежность» каждого уникального элемента, т. Е. к каким множествам он принадлежит. Затем мы создаем словарь для группировки всех элементов по их принадлежности, генерируя запрошенные наборы. Сложность равна O(n * len(множеств)), или O (n ^ 2) в худшем случае.

 def solution(sets):
    union = set().union(*sets)
    numSets = len(sets)
    numElements = len(union)
    memberships = {}
    for e in union:
        membership = tuple(i for i, s in enumerate(sets) if e in s)
        if membership not in memberships:
            memberships[membership] = []
        memberships[membership].append(e)
    print(memberships)

sets = [{1, 2, 3}, {1, 4, 7}, {1, 4, 5}]
solution(sets)

# output:
# {(0, 1, 2): [1], (1, 2): [4], (0,): [2, 3], (1,): [7], (2,): [5]}
  

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

1. Спасибо! Но, к сожалению, я не могу реалистично перебирать каждый элемент. Сами наборы содержат более ~ 2 ^ 64 элементов и отслеживают это через наборы исключений и диапазоны элементов, а не каждый отдельный элемент. В одном из маркерных пунктов указано ограничение

2. @Lindenk Вы вычисляете сложность в терминах количества операций с множествами? Имейте в виду, что пересечение множеств равно O (min (M, N)), а проверка принадлежности равна O (1) в терминах количества элементов.

3. Упс, вы правы. По какой-то причине я предположил, что набор мощности равен n ^ 2, когда на самом деле это 2 ^ n. Вау, это намного хуже