用于查找严格子集的快速数据结构(来自给定列表)

时间:2011-06-28 20:00:27

标签: algorithm performance data-structures set

我有很多套,例如{{2,4,5} , {4,5}, ...}. 给定其中一个子集,我想迭代所有其他子集,这些子集是该子集的严格子集。也就是说,如果我对集A感兴趣,例如{2,4,5},我想找到所有集合B,其中相对补集B / A = {},为空集。一些可能性可能是{2,4}{2,5}但不是{2,3}

我当然可以线性搜索并每次检查,但我正在为更大的集合和子集(如果重要)寻找有效的数据结构。子集的数量通常为数十万,但如果它有所不同,我会对它可能达到数亿的情况感兴趣。子集的大小通常为10秒。

我用C ++编程

由于

5 个答案:

答案 0 :(得分:7)

在数学上,你应该为你的集合构造Hasse diagram,这将是部分有序的集合,其顶点是你的集合和由包含给出的箭头。基本上,如果A --> B严格包含A并且没有B严格C,则您希望使用箭头A创建directed, acyclic graph包含CC严格包含B

这实际上将是一个排名的poset,这意味着你可以根据集合的基数来跟踪有向图的“级别”。这有点像创建哈希表以跳转到正确的集合。

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且{{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 BX

  3. Y封面X -> Y”我的意思是有一个箭头C,而不仅仅是一条路径。

    此外,当您使用上述检查之一在AB之间插入A --> B时,您需要在添加{{1}时删除箭头A --> C }和C --> B

答案 1 :(得分:4)

我建议将所有集合存储在树中。树的每个节点将表示包含指定的整数初始列表的所有集合。我希望节点包含以下信息:

  1. 树中此点或下面的最小集合中的其他元素数。 (0表示此节点位于树中。)
  2. 一个位集,表示树中此子集下方所有子集的交集。
  3. 指向数组的指针,该数组将较大的整数映射到包含该子集作为下一个元素的子树。作为一种特殊情况,如果树中只有一个子集,则该指针可以为null。 (没有必要填写树的无人居住的部分。)
  4. 鉴于此树和子集,您可以使用递归和回溯搜索集合的所有子集。在搜索中,您从子集的第一个元素开始,查找包含该元素的所有子集,然后搜索不包含该元素的所有子集。

    构建此树最多占用时间和空间O(n * m * k),其中n是子集数m是每个子集的平均元素数,k是可以在集合中的元素世界的大小。使用比k元素的子集可能范围小得多的随机集合集,您将不会构建大部分树,并且您的树将需要O(n * m)

    理论上,遍历这棵树的时间可能是O(n)在实践中你会很早地修剪树的分支,并且不会遍历大多数其他子集。封套计算的背面表明,如果n元素Universe中有k个随机集n << 2k,那么对该树的搜索是O(n0.5k)。 (在每个整数中,你在集合中搜索子集的时间的一半,你将搜索分成2,一半时间不在你的集合中,你消除了一半的空间。{{ 1}}你有j个整数搜索大小为2j/2的集合的整数。因此,当你将搜索结果缩小到单个其他子集进行比较时,有{{1} }搜索继续。位图的最终比较是2-jn。)

    注意:我相信信封计算后面的每个O(n0.5)的平均效果为O(k),但收敛速度很慢。更确切地说,我怀疑性能的算术平均值是o(n0.5+epsilon)。但epsilon > 0篇需要很长时间才能收敛。

    请注意,使用树中此点或更低位置的最小集合中的附加元素数量,可以使搜索过滤掉所有太大而不能成为子集的集合。根据您的数据集,这可能会或可能不会带来有用的加速。

答案 2 :(得分:3)

PengOne提出的方法可行,但效率不高。要了解它失败的原因,请考虑以下病态示例:

假设你有一个宇宙U,它有n个不同的元素,让你搜索的所有集合的集合包含U的所有子集,其中只有k个元素。那么这里没有一对集合严格地包含在一起;所以在最坏的情况下你必须搜索所有n选择k个可能的集合!换句话说,在最坏的情况下,使用他提出的数据结构并不比天真的线性搜索更好。

显然,你可以做得比这更好,正确使用的数据结构将是一个特里:http://en.wikipedia.org/wiki/Trie

要使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的答案,这或多或少得出了关于这个问题的相同结论(模拟了一些技术挑剔,我已将其转移到他的帖子的评论中。)< / p>

编辑2:意识到这实际上只是二元决策图的一个特例。现在没有足够的精力来修复写入,所以会保持原样。或许明天再修好。 http://en.wikipedia.org/wiki/Binary_decision_diagram

答案 3 :(得分:0)

这很有趣。我喜欢PengOne建议的Hasse图表方法,但我认为你可以使用素数技巧快速构建Hasse图。假设所有集合的并集导致自然数字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)。使用此分数来构建Hasse图表。

编辑:这似乎是一个很好的理论解决方案。可能不是要走的路。 yi_H建议的位集也同样好,不会遇到大整数问题。

答案 4 :(得分:0)

看看这个实现Hasse图的python库python-lattice] 1