【问题标题】:Quickly checking if set is superset of stored sets快速检查集合是否是存储集合的超集
【发布时间】:2012-03-10 07:40:18
【问题描述】:

问题

我得到了 N 个 C 布尔值数组。我想将它们组织成一个数据结构,使我能够尽可能快地执行以下操作:给定一个新数组,如果该数组是任何存储数组的“超集”,则返回 true。对于超集,我的意思是:如果 A[i] 对于 B[i] 为真的每个 i 都为真,则 A 是 B 的超集。如果 B[i] 为假,那么 A[i] 可以是任何东西。

或者,就集合而不是数组而言:

将 N 个集合(每个集合包含 C 个可能的元素)存储到一个数据结构中,这样您就可以快速查找给定集合是否是任何存储集合的超集。

构建数据结构可以花费尽可能长的时间,但是查找应该尽可能高效,并且数据结构不能占用太多空间。

一些上下文

我认为这本身就是一个有趣的问题,但是对于我真正想要解决的问题,您可以假设以下内容:

  • N = 10000
  • C = 1000
  • 存储的数组是稀疏的
  • 查找的数组是随机的(所以不是稀疏的)

到目前为止我的想法

  1. 对于 O(NC) 查找:只需迭代所有数组。不过这太慢了。

  2. 对于 O(C) 查找:我在这里有一个很长的描述,但正如 Amit 在 cmets 中指出的那样,它基本上是一个 BDD。虽然这具有很高的查找速度,但它具有指数级数量的节点。 N 和 C 这么大,太占空间了。

我希望在这个 O(N*C) 和 O(C) 解决方案之间,可能存在一个不需要指数级空间的 O(log(N)*C) 解决方案。

编辑:我提出的一个新想法

  • 对于 O(sqrt(N)C) 查找:将数组存储为 prefix trie。查找数组 A 时,如果 A[i]=0,则转到相应的子树,但如果 A[i]=1,则访问 both 个子树。

    我的直觉告诉我,如果您假设存储的数组是随机的,这应该使查找的(平均)复杂度为 O(sqrt(N)C)。但是:1.它们不是,数组是稀疏的。 2. 这只是直觉,我无法证明。

我将尝试这个新想法和 BDD 方法,看看这两种方法中哪一种效果最好。

但与此同时,这个问题不是经常发生吗?它没有名字吗?之前没有研究过吗?真的感觉我在这里重新发明轮子。

【问题讨论】:

  • 如果数组是静态的,您可以为每个可能的对 (a,b) 预先计算 is_subset(a,b)。需要 10K*10K 位图:= 100M/CHAR_BIT。显然,查找将是 O(1)。
  • @wildplasser:查找的数组不是 N 个存储集之一。 (否则结果将始终为真)。因此,对于查找,我看不到如何使用此位图。此外,所有存储的集合都不会是彼此的子集,因为如果 A 是 B 的子集,我也可以只存储 A。
  • 对不起,我读得太快了。使用一些否定逻辑,并将位集扩展到 64 位之外,也许可以调整这个(与 BDD 非常相似):stackoverflow.com/questions/9246017/…
  • 你的做法其实和BDD很像。至少知道这样的想法已经存在可以节省您的实施时间。请注意,BDD 通常用于最多数百个变量的通用公式。
  • 由于要求的结果应该是一个布尔值,这确实可以作为 BDD 来实现。可变排序可以通过将频率(在 sum(N) 中)最接近 1/2 的位(在 C 中)放在靠近顶部的位置来引导。

标签: algorithm data-structures set complexity-theory time-complexity


【解决方案1】:

只是为了给prefix trie解决方案补充一些背景资料,最近发现了以下论文:

I.Savnik:快速子集和超集查询的索引数据结构CD-ARES, IFIP LNCS, 2013.

本文提出了 set-trie 数据结构(容器),它为使用 trie 数据结构的集合集的高效存储和查询提供支持,支持查找所有超集/子集等操作集合中的给定集合。

对于任何对实际实现感兴趣的 python 用户,我想出了一个部分基于上述论文的 python3 包。它包含一个基于 trie 的集合容器和一个映射容器,其中键是集合。你可以在github找到它。

【讨论】:

    【解决方案2】:

    我认为前缀树是一个很好的开始。

    由于你的数组很稀疏,我会另外批量测试它们。如果(B1 ∪ B2) ⊂ A,则两者都包括在内。所以这个想法是成对地对数组进行 OR-pack,并重复直到只有一个“根”数组(它只需要两倍的空间)。它允许您更早地对您的问题回答“是”,这主要是有用的如果您不需要知道实际包含数组

    您可以独立地为每个数组应用一个保持排序的哈希函数。

    即:B ⊂ A ⇒ h(B) ≺ h(A)

    ORing 位在一起就是这样一个功能,但您也可以在数组的适当分区中计算每个 1 位。在这里,您可以更快地消除候选者(对特定数组回答“否”)。

    【讨论】:

      【解决方案3】:

      您可以通过首先将集合列表减少到“最小”集合来简化问题:只保留那些不是任何其他集合的超集的集合。问题仍然存在,因为如果某个输入集 A 是您删除的某个集 B 的超集,那么它也是未删除的 B 的至少一个“最小”子集 C 的超集.这样做的好处是您倾向于消除大集合,从而降低问题的成本。

      从那里我会使用某种 ID3 或C4.5 算法。

      【讨论】:

        【解决方案4】:

        在 trie 解决方案和@mmihaltz 提到的论文的基础上,还可以通过使用现有的 python 高效 trie 实现来实现一种查找子集的方法。下面我使用包datrie。唯一的缺点是必须将键转换为字符串,这可以通过"".join(chr(i) for i in myset) 完成。然而,这将元素的范围限制在 110000 左右。

        from datrie import BaseTrie, BaseState
        
        def existsSubset(trie, setarr, trieState=None):
        
            if trieState is None:
                trieState = BaseState(trie)
        
            trieState2 = BaseState(trie)
            trieState.copy_to(trieState2)
            for i, elem in enumerate(setarr):
                if trieState2.walk(elem):
                    if trieState2.is_terminal() or existsSubset(trie, setarr[i:], trieState2): 
                        return True
                    trieState.copy_to(trieState2)
            return False
        
        

        trie 可以像字典一样使用,但必须在开头提供可能元素的范围:

        alphabet = "".join(chr(i) for i in range(100))
        trie = BaseTrie(alphabet)
        
        for subset in sets:
           trie["".join(chr(i) for i in subset)] = 0 # the assigned value does not matter
        
        

        请注意,上面的 trie 实现仅适用于大于(且不等于)0 的键。否则,整数到字符的映射将无法正常工作。这个问题可以通过索引移位来解决。

        一个也涵盖元素转换的cython实现可以在here找到。

        【讨论】:

          猜你喜欢
          • 2022-01-04
          • 2018-08-11
          • 1970-01-01
          • 2013-04-20
          • 2012-11-06
          • 1970-01-01
          • 1970-01-01
          • 2017-05-28
          相关资源
          最近更新 更多