【问题标题】:Fast Data structure for finding strict subsets (from a given list)用于查找严格子集的快速数据结构(来自给定列表)
【发布时间】:2011-09-24 15:20:48
【问题描述】:

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

我当然可以线性搜索并每次检查,但我正在为更大的集合和子集(如果重要的话)寻找一种有效的数据结构。子集的数量通常是成千上万,但如果它有所作为,我会对它可能是数亿的情况感兴趣。子集的大小通常以 10s 为单位。

我正在用 C++ 编程

谢谢

【问题讨论】:

  • 有哪些元素?它们的域有多大,即总共可以有多少个不同的值?
  • 元素是图的连通社区中的节点。出于所有意图和目的,它们可以被视为从 0 到最大值(以 10 秒为单位)的整数。
  • 那么您可能应该将您的集合表示为std::bitset,或者,如果您想要通用,则表示为boost::dynamic_bitset。那么“A 是 B 的子集”就是 A & ~B == 0。您需要为许多不同的 A 找到所有集合 B 还是只找到一对? IE。事先创建一个大型结构是否有意义,或者对不同的 A 进行几次详尽的迭代就足够了?
  • 数据是静态的吗? (你能在集合中添加新元素吗?你能修改集合中的现有元素吗?)
  • @yi_h:我相信 10s 是一个介于 10-99 之间的数字。类似于 100,一个介于 100-999 之间的数字。

标签: algorithm performance data-structures set


【解决方案1】:

从数学上讲,您应该为您的集合构造Hasse diagram,这将是部分有序集合,其中包含您的集合的顶点和包含给定的箭头。本质上,如果A 严格包含B 并且没有C 这样A 严格包含CC 严格包含@987654330,则您想创建一个带有箭头A --> Bdirected, acyclic graph @。

这实际上将是一个排序的 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)
}

为了使这个和所有子例程更快,您可以将每个集合编码为一个二进制,其中如果iC 中,则数字i1,否则为0。这使得测试遏制和确定等级变得微不足道。

如果您有所有可能的子集,上述方法有效。由于您可能遗漏了一些内容,因此您必须检查更多内容。对于伪代码,您需要将rank(C)-1 更改为最大整数l < rank(C),这样HasseDiagram 的某些元素的等级为lrank(C)+1 也是如此。然后,当您将集合 C 添加到图表时:

  1. 如果A 覆盖C,那么您只需检查也被A 覆盖的较低排名集B

  2. 如果C 覆盖B,那么您只需检查排名较高的集合A 也被B 覆盖。

X 覆盖Y”我的意思是有一个箭头X -> Y,而不仅仅是一条路径。

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

【讨论】:

  • 实际上,我想这正是我想要创建的图表。我认为这样做的方法是让所有集合都搜索严格子集的集合。感谢您的术语!
  • @zenna:您可以遍历集合,使用二等分算法将集合插入到正确的位置来创建图形。我以前在 Maple 中实现过这个,它非常有效,但它使用了他们的许多图形函数。我看看能不能提炼出这个想法。
  • 为了澄清,你说我应该构建一个哈斯图,我想我间接的问题是,如何快速构建一个哈斯图?值得考虑的是,我没有考虑所有可能的子集,我有大量但较小的可能性。
  • 抱歉,我在看到您的回复之前写了该评论。使用二等分法,二等分集可能对我无效。我想我可以有一些拒绝政策。嗯
  • @PengOne:我对伪代码是否真的有效感到困惑。考虑{{1}, {2}, {3}, {2,3}, {1,2,3}}。那么{1} 排名为 1,{1, 2, 3} 排名为 3,{1}{1, 2, 3} 的子集,并且没有中间子集。你的first_rank_belowfirst_rank_above 让我觉得你可能会错过这种关系。
【解决方案2】:

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

  1. 在树中此点或以下的最小集合中的附加元素数。 (0 表示该节点在树中。)
  2. 表示树中该子集以下所有子集的交集的位集。
  3. 指向将较大整数映射到包含该整数作为下一个元素的子树的数组的指针。作为一种特殊情况,如果树中只有一个低于该子集的子集,则该指针可能为空。 (无需填写树中未填充的部分。)

给定这棵树和一个子集,您可以对集合的所有子集进行递归和回溯搜索。在您的搜索中,您从子集的第一个元素开始,查找包含该元素的所有子集,然后搜索不包含该元素的所有子集。

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

理论上遍历这棵树可能是时间O(n)但是在实践中,您会很早就修剪树的分支,并且不会遍历大多数其他子集。信封计算的背面表明,如果您在具有n &lt;&lt; 2<sup>k</sup>k 元素宇宙中有n 随机集,那么对树的搜索是O(n<sup>0.5</sup>k)。 (在每个整数中,一半时间在您的集合中搜索子集,然后将搜索分成 2 个,一半时间不在您的集合中,您消除一半空间。在@987654332 之后@整数你有2<sup>j/2</sup>搜索大小的集合集合2<sup>-j</sup>n。因此,当您将搜索缩小到要比较的单个其他子集时,有O(n<sup>0.5</sup>)搜索正在进行。最终比较位图是O(k)。)

注意:我相信这个粗略计算的结果是,每个epsilon &gt; 0 的平均性能是o(n<sup>0.5+epsilon</sup>),但收敛速度很慢。更准确地说,我怀疑性能的算术平均值是n<sup>0.5 + O(sqrt(log(n)))</sup>)。但是sqrt(log(n)) 部分需要很长时间才能收敛。

请注意,使用树中此时或下方的最小集合中的附加元素的数量可以让您的搜索轻松过滤掉所有太大而不能成为子集的集合。根据您的数据集,这可能会或可能不会导致有用的加速。

【讨论】:

  • 我对 O(sqrt(n)) 开销计算有点怀疑。你能更详细地介绍一下吗?具体来说,您如何估计您不会访问特定分支机构的概率?除此之外,很好的答案。
  • @Mikola:在你的宇宙中的每个整数中,其他子集的一半包含它,而另一半不包含。因此,您的每个递归搜索平均减少了一半。因此,如果有 100 万个其他子集,则需要查看大约 20 个整数才能使您的幸存搜索分别属于 1 个其他集合。平均而言,这些整数中有 10 个在您的集合中,而 10 个不在,导致平均 1024 次搜索。
  • @Mikola:但是这是一个中位数,而不是算术平均值。如果您的集合中只有 8 个整数,那么您只有 256 次搜索。如果您的集合中有 12 个,那么您将进行 4k 次搜索。这种不对称性不小。我将对此添加评论。
  • 我认为我们在这里使用了不同的 n。我的 n 是通用集中元素的数量(即我们正在查询的所有集合的并集)。似乎您正在使用 n 来指代正在搜索的所有集合中的集合数。我同意你的观点,假设均匀分布,平均返回的集合数为 O(sqrt(#sets being searched on)),但是搜索这个集合的开销是树中访问的其他节点的数量(也就是它的每个元素的高度)。
  • 这个开销应该是 O(log(sqrt(#sets being searched))),渐近 O(#elements in universe/2)。
【解决方案3】:

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

假设你有一个全域 U,它有 n 个不同的元素,并让你正在搜索的所有集合的集合由 U 的所有子集组成,其中正好有 k 个元素。那么,这里没有任何一对集合是严格包含在彼此中的。因此,在最坏的情况下,您将不得不搜索所有 n 选择 k 个可能的集合!换句话说,在最坏的情况下,使用他提出的数据结构并不比简单的线性搜索好。

显然,您可以做得比这更好,并且要使用的正确数据结构将是一个 trie: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 的答案,这或多或少地得出了关于该问题的相同结论(以我在他的帖子中移入 cmets 的一些技术挑剔为模。)

编辑 2:意识到这实际上只是二元决策图的一个特例。现在真的没有足够的精力来修复这篇文章,所以就让它保持原样吧。也许明天修复它。 http://en.wikipedia.org/wiki/Binary_decision_diagram

【讨论】:

  • 我正要评论说我们的解决方案非常相似(考虑到问题的开放时间,我很惊讶它们的发布时间如此之近),但我认为您的复杂性分析需要更多工作。
  • 是的,我实际上正在研究它,但我认为我已经写了足够的东西来快速发布早期版本。我目前正在用更多细节来充实它。
  • @Mikola:哈斯图适用于任何poset。它不需要是所有子集的集合。所以我正在构建的确实是他的集合的包含给出的poset的Hasse图。此外,一旦有了哈斯图,您描述的案例就可以轻松解决,因为没有要搜索的孩子。但是,设置 Hasse 图非常耗时。
  • @zenna:是的,我稍微修改了代码以包含一些改进,但忘记更新示例。
【解决方案4】:

这很有趣。我喜欢 PengOne 建议的哈斯图方法,但我认为你可以使用素数技巧快速构建哈斯图。假设所有集合的并集导致自然数 1 到 N。将这些数字中的每一个映射到相应的素数,例如:

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

接下来,通过将与集合中的数字对应的每个素数相乘,计算每个集合的“分数”。例如,集合 {1,2,3} 的得分为 2*3*5 = 30。现在,要使集合 A 成为另一个集合 B score(A) 的真子集,必须除以 score(B)(得分对于 {1,2},{2,3} 和 {1,3} 分别是 6、15 和 10,每个除以 30)。使用这个分数来构建你的哈斯图。

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

【讨论】:

  • 那行不通,子集中有 10 个元素...您必须检查 2^(n-1) 个不同的组合。
  • 嗯不是真的,它是一组,顺序无关紧要。集合 {2,1} 和 {1,2} 将具有相同的分数。为什么需要检查 2^(n-1) 个组合?
  • 这是一个有趣的解决方案。我不确定为什么 yi_H 建议您需要检查 2^(n-1) 个解决方案。我想知道是否有任何警告。
  • 对不起,我误解了。但是为什么这比位域检查好呢?它肯定需要更多的计算
  • 有一个刺眼的一个好吧。当你将它们相乘时,数字的大小会变得很大。有几种解决方法(我认为),例如在模数大数空间中进行数学运算,但这会导致额外的复杂性。只要您的数字不会太大,bigint 库就可以解决问题。
【解决方案5】:

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

【讨论】:

  • 如果我没记错的话,这只是真正实现它们用于图形,而不是任何有效的算法意义。
  • 看起来很可爱,但好像有点过时了?
猜你喜欢
  • 1970-01-01
  • 2011-02-05
  • 2014-06-23
  • 1970-01-01
  • 2011-04-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多