Найдите наборы, содержащие хотя бы один элемент из других наборов - PullRequest
0 голосов
/ 28 мая 2020

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

Пример:

In: s1 = {1, 2, 3}; s2 = {3, 4, 5}; s3 = {5, 6}

Out: [{1, 4, 6}, {1, 5}, {2, 4, 6}, {2, 5}, {3, 5}, {3, 6}]

Моя идея заключалась в том, чтобы итеративно добавлять один набор за другим к решению:

result = f(s1, f(s2, f(s3, ...)))

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

function f(newSet, setOfSets):
   Step 1: 
      return all elements of setOfSets that share an element with newSet

   Step 2: 
      for each remaining element setE of setOfSets:
         for each element e of newSet:
            return union(setE, {e})

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

Как я могу достичь цели, не определяя полное декартово произведение на шаге 2?

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

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

Ответы [ 2 ]

1 голос
/ 03 июня 2020

Я придумал решение, основанное на структуре данных tr ie, как описано здесь . Попытки позволяют относительно быстро определить, является ли один из сохраненных наборов подмножеством другого данного набора ( Савник, 2013 ).

Тогда решение выглядит следующим образом:

  • Создать tr ie
  • Перебирать данные наборы
    • На каждой итерации go через наборы в tr ie и проверьте, не пересекаются ли они с новым набором.
    • Если есть, продолжайте; если нет, добавьте соответствующие новые наборы в tr ie, если они не являются надмножествами наборов в tr ie.

Время выполнения в худшем случае - O (nm c) , где m - максимальное количество решений, если мы рассматриваем только n '<= n </em> входных наборов, а c - временной фактор из поисков подмножества.

Код ниже. Я реализовал алгоритм, основанный на пакете python datr ie, который представляет собой оболочку вокруг эффективной C реализации tr ie. Приведенный ниже код находится на языке Cython, но его можно легко преобразовать в чистый python, удалив / обменивая команды cython specifici c.

Расширенная реализация tr ie:

from datrie cimport BaseTrie, BaseState, BaseIterator

cdef bint has_subset_c(BaseTrie trie, BaseState trieState, str setarr, 
                        int index, int size):
    cdef BaseState trieState2 = BaseState(trie)
    cdef int i
    trieState.copy_to(trieState2)
    for i in range(index, size):
        if trieState2.walk(setarr[i]):
            if trieState2.is_terminal() or has_subset_c(trie, trieState2, setarr, 
                                                        i, size): 
                return True
            trieState.copy_to(trieState2)
    return False


cdef class SetTrie():
    def __init__(self, alphabet, initSet=[]):
        if not hasattr(alphabet, "__iter__"):
            alphabet = range(alphabet)
        self.trie = BaseTrie("".join(chr(i) for i in alphabet))
        self.touched = False
        for i in initSet:
            self.trie[chr(i)] = 0
            if not self.touched:
                self.touched = True

    def has_subset(self, superset):
        cdef BaseState trieState = BaseState(self.trie)
        setarr = "".join(chr(i) for i in superset)
        return bool(has_subset_c(self.trie, trieState, setarr, 0, len(setarr)))

    def extend(self, sets):
        for s in sets:
            self.trie["".join(chr(i) for i in s)] = 0
            if not self.touched:
                self.touched = True

    def delete_supersets(self):
        cdef str elem 
        cdef BaseState trieState = BaseState(self.trie)
        cdef BaseIterator trieIter = BaseIterator(BaseState(self.trie))
        if trieIter.next():
            elem = trieIter.key()
            while trieIter.next():
                self.trie._delitem(elem)
                if not has_subset_c(self.trie, trieState, elem, 0, len(elem)):
                    self.trie._setitem(elem, 0)
                elem = trieIter.key()
            if has_subset_c(self.trie, trieState, elem, 0, len(elem)):
                val = self.trie.pop(elem)
                if not has_subset_c(self.trie, trieState, elem, 0, len(elem)):
                    self.trie._setitem(elem, val)


    def update_by_settrie(self, SetTrie setTrie, maxSize=inf, initialize=True):
        cdef BaseIterator trieIter = BaseIterator(BaseState(setTrie.trie))
        cdef str s
        if initialize and not self.touched and trieIter.next():
            for s in trieIter.key():
                self.trie._setitem(s, 0)
            self.touched = True

        while trieIter.next():
            self.update(set(trieIter.key()), maxSize, True)

    def update(self, otherSet, maxSize=inf, isStrSet=False):
        if not isStrSet:
            otherSet = set(chr(i) for i in otherSet)
        cdef str subset, newSubset, elem
        cdef list disjointList = []
        cdef BaseTrie trie = self.trie
        cdef int l
        cdef BaseIterator trieIter = BaseIterator(BaseState(self.trie))
        if trieIter.next():
            subset = trieIter.key()
            while trieIter.next():
                if otherSet.isdisjoint(subset):
                    disjointList.append(subset)
                    trie._delitem(subset)
                subset = trieIter.key()
            if otherSet.isdisjoint(subset):
                disjointList.append(subset)
                trie._delitem(subset)

        cdef BaseState trieState = BaseState(self.trie)
        for subset in disjointList:
            l = len(subset)
            if l < maxSize:
                if l+1 > self.maxSizeBound:
                    self.maxSizeBound = l+1
                for elem in otherSet:
                    newSubset = subset + elem
                    trieState.rewind()
                    if not has_subset_c(self.trie, trieState, newSubset, 0, 
                                        len(newSubset)):
                        trie[newSubset] = 0

    def get_frozensets(self):
        return (frozenset(ord(t) for t in subset) for subset in self.trie)

    def clear(self):
        self.touched = False
        self.trie.clear()

    def prune(self, maxSize):
        cdef bint changed = False
        cdef BaseIterator trieIter 
        cdef str k
        if self.maxSizeBound > maxSize:
            self.maxSizeBound = maxSize
            trieIter = BaseIterator(BaseState(self.trie))
            k = ''
            while trieIter.next():
                if len(k) > maxSize:
                    self.trie._delitem(k)
                    changed = True
                k = trieIter.key()
            if len(k) > maxSize:
                self.trie._delitem(k)
                changed = True
        return changed

    def __nonzero__(self):
        return self.touched

    def __repr__(self):
        return str([set(ord(t) for t in subset) for subset in self.trie])

Это можно использовать следующим образом:

def cover_sets(sets):
    strie = SetTrie(range(10), *([i] for i in sets[0]))
    for s in sets[1:]:
        strie.update(s)
    return strie.get_frozensets()

Время:

from timeit import timeit
s1 = {1, 2, 3}
s2 = {3, 4, 5}
s3 = {5, 6}
%timeit cover_sets([s1, s2, s3])

Результат:

37.8 µs ± 2.97 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Обратите внимание, что реализация tr ie выше работает только с ключи больше (и не равны) 0. В противном случае преобразование целого числа в символ не работает должным образом. Эту проблему можно решить с помощью сдвига индекса.

1 голос
/ 31 мая 2020

Поскольку ваше пространство настолько ограничено - всего 20 значений на выбор - избейте эту штуку до смерти тупым инструментом:

  1. Преобразуйте каждый из ваших целевых наборов (те, которые будут покрыта) в битовую карту. В данном случае это будет соответствовать целому числу из 20 бит, одна битовая позиция для каждого из 20 значений.
  2. Создайте список кандидатов, охватывающий растровые изображения, целые числа от 0 до (2 ^ 20-1)
  3. Возьмите целые числа по порядку. Используйте битовые операции, чтобы определить, есть ли у каждого целевого набора общий бит 1 с кандидатом. Если все удовлетворяют условию basi c, кандидат проверяется.
  4. Когда вы проверяете кандидата, удалите все супернаборы целых чисел из списка кандидатов.
  5. Когда у вас закончится кандидаты, ваши проверяющие кандидаты являются желаемой коллекцией. В приведенном ниже коде я просто печатаю каждое из них по мере их идентификации.

Код:

from time import time
start = time()

s1 = {1, 2, 3}
s2 = {3, 4, 5}
s3 = {5, 6}

# Convert each set to its bit-map
point_set = [7, 28, 48]

# make list of all possible covering bitmaps
cover = list(range(2**20))

while cover:
    # Pop any item from remaining covering sets
    candidate = cover.pop(0)
    # Does this bitmap have a bit in common with each target set?
    if all((candidate & point) for point in point_set):
        print(candidate)

        # Remove all candidates that are supersets of the successful covering one.
        superset = set([other for other in cover if (candidate & ~other) == 0])
        cover = [item for item in cover if item not in superset]
        print(time() - start, "lag time")

print(time() - start, "seconds")

Вывод - я не преобразовал целые числа-кандидаты обратно в их составные элементы. Это простая задача.

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

Эти 33 секунды находятся на моем стареющем настольном компьютере; ваш ноутбук или другая платформа почти наверняка быстрее. Я верю, что любое улучшение от более эффективного алгоритма легко компенсируется тем, что этот алгоритм быстро реализуется и его легче понять.

17
0.4029195308685303 lag time
18
0.6517734527587891 lag time
20
0.8456630706787109 lag time
36
1.0555419921875 lag time
41
1.2604553699493408 lag time
42
1.381387710571289 lag time
33.005757570266724 seconds
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...