Быстрая структура данных для поиска строгих подмножеств (из заданного списка) - PullRequest
18 голосов
/ 29 июня 2011

У меня есть большой набор наборов, например {{2,4,5} , {4,5}, ...}. Учитывая одно из этих подмножеств, я хотел бы перебрать все другие подмножества, которые являются строгими подмножествами этого подмножества. То есть, если меня интересует набор A, например {2,4,5}, я хочу найти все множества B, где относительное дополнение B / A = {}, пустое множество. Некоторые возможности могут быть {2,4}, {2,5}, но не {2,3}

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

Я программирую на C ++

Спасибо

Ответы [ 5 ]

7 голосов
/ 29 июня 2011

Математически, вы должны построить диаграмму Хассе для ваших множеств, которая будет частично упорядоченным множеством с вершинами ваших множеств и стрелками, заданными сдерживанием.По сути, вы хотите создать направленный ациклический граф со стрелкой A --> B, если A строго содержит B и нет C такого, что A строго содержит C иC строго содержит B.

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

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

Как реализовать это: (в псевдокоде)

for (C in sets) {
    for (B in HasseDiagram at rank rank(C)+1) {
      if (C contains B)
        addArrow(C,B)
    }
    for (A in HasseDiagram at rank rank(C)+1) {
      if (C contains A)
        addArrow(A,C)
    }
    addToDiagram(C)
}

Чтобы сделать это и все подпрограммы быстрыми, вы можете закодировать каждый набор в двоичный код, где цифра i равна 1, если i находится в C и 0 в противном случае.Это делает тестирование сдерживания и определение ранга тривиальным.

Приведенный выше метод работает , если у вас есть все возможные подмножества .Поскольку вы можете пропустить некоторые, вам придется проверить больше вещей.Для псевдокода вам нужно изменить rank(C)-1 на наибольшее целое число l < rank(C), чтобы некоторый элемент HasseDiagram имел ранг l, и аналогично для rank(C)+1.Затем, когда вы добавляете набор C к диаграмме:

  1. Если A охватывает C, то вам нужно только проверить наборы с более низким рейтингом B, которыетакже охватываются A.

  2. Если C охватывает B, то вам нужно только проверить наборы с более высоким рейтингом A, которые также охватывают B.

Под "X охватывает Y" Я имею в виду стрелку X -> Y, а не просто путь.

Кроме того, когда вы вставляете Cмежду A и B, используя одну из вышеуказанных проверок, вам нужно убрать стрелку A --> B при добавлении A --> C и C --> B.

4 голосов
/ 29 июня 2011

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

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

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

Построение этого дерева требует времени и пространства вmost O(n * m * k), где n - количество подмножеств m - среднее количество элементов в подмножестве, а k - размер юниверса элементов, которые могут быть в наборах.Со случайными наборами множеств, которые намного меньше, чем возможная вселенная подмножеств ваших элементов k, вы не создадите большую часть дерева, и для вашего дерева потребуется O(n * m).

В теорииОбход этого дерева может быть временем O(n). Но на практике вы обрежете ветви дерева довольно рано и не пройдете большинство других подмножеств.Обратная сторона расчета конверта предполагает, что если у вас есть n случайных наборов из k вселенной элемента с n << 2<sup>k</sup>, тогда поиск по дереву будет O(n<sup>0.5</sup>k).(В каждом целом числе половину времени в вашем наборе вы ищете для подмножеств, и вы разбиваете свой поиск на 2, а в половине случаев его нет в вашем наборе, и вы убираете половину своего пространства. После j целые числа, которые у вас есть 2<sup>j/2</sup> выполняет поиск наборов наборов размером 2<sup>-j</sup>n. Таким образом, к тому времени, когда вы сводите поиски к отдельным другим подмножествам для сравнения, происходит O(n<sup>0.5</sup>) поиск. Окончательное сравнениебитовые карты равны O(k).)

Примечание : я убедился в этой обратной части расчета конверта, что средняя производительность составляет o(n<sup>0.5+epsilon</sup>) для каждого epsilon > 0, но сходимостьочень медленно.Точнее, я подозреваю, что среднее арифметическое производительности составляет n<sup>0.5 + O(sqrt(log(n)))</sup>).Но для этого элемента sqrt(log(n)) требуется много времени.

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

3 голосов
/ 29 июня 2011

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

Предположим, у вас есть юниверс U, в котором есть n различных элементов, и пусть набор всех искомых наборов состоит из всех подмножеств U сровно k элементов.Тогда верно, что ни одна пара множеств здесь не содержится строго друг в друге;и поэтому в худшем случае вам придется искать по всем n выбрать k возможных наборов!Другими словами, использование предложенной им структуры данных ничуть не лучше наивного линейного поиска в худшем случае.

Очевидно, что вы можете добиться гораздо большего, чем это, и правильная структура данных для использования была бы бесполезной: http://en.wikipedia.org/wiki/Trie

Чтобы приспособить три для работы с наборами вместо просто строк, достаточно установить порядок элементов универсального набора, а затем закодировать каждое из ваших подмножеств в виде двоичной строки конечной длины,где i-й символ равен 0 или 1 в зависимости от того, содержит ли набор i-й элемент.Вот реализация на python

import math

class SetTree:
    def __init__(self, index, key, left, right):
        self.index = index
        self.key = key
        self.left = left
        self.right = right

cached_trees = { }
cached_index = 2

def get_index(T):
    if isinstance(T, SetTree):
        return T.index
    if T:
        return 1
    return 0        

def make_set_tree(key, left, right):
    global cached_trees, cached_index
    code = (key, get_index(left), get_index(right))
    if not code in cached_trees:
        cached_trees[code] = SetTree(cached_index, key, left, right)
        cached_index += 1
    return cached_trees[code]

def compute_freqs(X):
    freqs, total = {}, 0
    for S in X:
        for a in S:
            if a in freqs:
                freqs[a] += 1
            else:
                freqs[a] = 1
            total += 1
    U = [ (-f, a) for a,f in freqs.items() ]
    U.sort()
    return U

#Constructs the tree recursively
def build_tree_rec(X, U):
    if len(X) == 0:
        return False
    if len(U) == 0:
        return True

    key = U[0][1]

    left_elems = [ S for S in X if key in S]

    if len(left_elems) > 0:
        return make_set_tree(key,
            build_tree_rec(left_elems, U[1:]),
            build_tree_rec([ S for S in X if not key in S ], U[1:]))

    return build_tree_rec(X, U[1:])

#Build a search tree recursively
def build_tree(X):
    U = compute_freqs(X)
    return build_tree_rec(X, U)


#Query a set tree to find all subsets contained in a given set
def query_tree(T, S):
    if not isinstance(T, SetTree):
        return [ [] ] if T else []
    if T.key in S:
        return [ U + [ T.key ] for U in query_tree(T.left, S) ] + query_tree(T.right, S)
    return query_tree(T.right, S)

#Debugging function: Converts a tree to a tuple for printing
def tree_to_tuple(T):
    if isinstance(T, SetTree):
        return (T.key, tree_to_tuple(T.left), tree_to_tuple(T.right))
    return T

Теперь вот пример использования:

In [15]: search_tree = set_search.build_tree(set_family)

In [16]: set_search.tree_to_tuple(search_tree)
Out[16]: 
(2,
 (4, (5, True, True), (5, True, (3, True, False))),
 (4, (5, True, False), (1, True, False)))

In [17]: set_search.query_tree(search_tree, set([2,3,4,5]))
Out[17]: [[5, 4, 2], [4, 2], [5, 2], [3, 2], [5, 4]]

In [18]: set_search.query_tree(search_tree, set([1,2,3,4,5]))
Out[18]: [[5, 4, 2], [4, 2], [5, 2], [3, 2], [5, 4], [1]]

In [19]: set_search.query_tree(search_tree, set([2,4,5]))
Out[19]: [[5, 4, 2], [4, 2], [5, 2], [5, 4]]

In [20]: set_search.query_tree(search_tree, set([2,5]))
Out[20]: [[5, 2]]

In [21]: set_search.query_tree(search_tree, set([1]))
Out[21]: [[1]]

In [22]: set_search.query_tree(search_tree, set([15]))
Out[22]: []

Обратите внимание, что объем работы, выполняемой query_tree, пропорционален размеру поддерева, которое представляетнабор всех результатов, возвращаемых query_tree.Таким образом, наша цель состоит в том, чтобы вычислить размер одной из подпунктов (в среднем), а затем в качестве вторичной цели минимизировать это количество.Один из способов сделать это - переупорядочить элементы универсального в терминах нисходящей частоты, чтобы они повторялись как можно меньше раз на нижних уровнях дерева.Эта оптимизация также выполняется в приведенном выше коде.Вторичной оптимизацией является кэширование деревьев, которые уже были найдены, чтобы избежать необходимости переделывать ненужную работу.

РЕДАКТИРОВАТЬ: Сразу после того, как я закончил набирать это, я увидел ответ btilly, который приходит более или менеетот же вывод о проблеме (по модулю некоторых технических прыщей, которые я переместил в комментарии к его сообщению.)

РЕДАКТИРОВАТЬ 2: Понял, что это действительно просто частный случай бинарной диаграммы решений.На самом деле не хватает энергии, чтобы исправить ситуацию прямо сейчас, поэтому оставлю все как есть.Возможно, исправим это завтра.http://en.wikipedia.org/wiki/Binary_decision_diagram

0 голосов
/ 18 января 2013

Посмотрите на эту библиотеку python, которая реализует диаграммы Хассе python-lattice] 1

0 голосов
/ 29 июня 2011

Это интересно. Мне нравится подход к диаграмме Хассе, который предлагает PengOne, но я думаю, что вы можете построить диаграмму Хассе очень быстро, используя прием простых чисел. Допустим, объединение всех множеств приводит к натуральным числам от 1 до N. Отобразите каждое из этих чисел с соответствующими простыми числами, например:

PrimeMap [1] = 2;
PrimeMap [2] = 3;
PrimeMap [3] = 5;

Затем вычислите «оценку» для каждого набора путем умножения каждого из простых чисел, соответствующих числу в наборе. Например, набор {1,2,3} будет иметь оценку 2 * 3 * 5 = 30. Теперь, чтобы набор A был правильным подмножеством другого набора B, оценка (A) должна делить оценку (B) для {1,2}, {2,3} и {1,3} являются 6, 15 и 10, каждый из которых делит 30). Используйте этот счет, чтобы построить свою диаграмму Хассе.

Редактировать: Это похоже на одно из хороших теоретических решений. Вероятно, не путь. Наборы битов, предложенные yi_H, также хороши и не страдают от больших целочисленных проблем.

...