快速查找所有子集
Posted
技术标签:
【中文标题】快速查找所有子集【英文标题】:quick find all sets that are subset 【发布时间】:2018-08-01 14:34:54 【问题描述】:我有一个大字典,里面有frozenset 键。我需要找到给定一个子集的所有键。我看到了明显的方法:
dictionary =
frozenset([1]): 1,
frozenset([2]): 2,
frozenset([3]): 3,
frozenset([3, 4]): 34
biglist= [3, 4, 5]
results = k: v for k, v in dictionary.items() if k.issubset(biglist)
assert results == frozenset([3]): 3, frozenset([3, 4]): 34
但是对于数百万个键来说它非常慢。问题是:这种类型的快速搜索有什么结构吗?
UPD:基本上,我不想遍历所有在每个键上执行 issubset 的键。相反,我可以从 biglist 生成所有可能的集合并检查它是否在字典中:
results =
maxkey = max(dictionary, key=len)
maxlen = len(dictionary[maxkey])
for lenght in range(1, maxlen):
for subset in itertools.combinations(biglist, lenght):
key = frozenset(subset)
if key in dictionary:
results[key] = dictionary[key]
但是这种方法对于长大列表来说也是非常昂贵的。
【问题讨论】:
***.com/questions/728972/… 您是在寻找特定的数据结构,还是算法是否可行:这能回答您的问题吗? 根据biglist
的大小,迭代字典中的元素实际上可能比检查所有子集更快。您是否考虑过使用不同的数据结构,例如像 Trie 这样的东西?
python 是一个更高级别的程序,本质上有点慢。尝试使用 cython 或尝试用 c++ 完全重写代码。在某些情况下,它可能会加快处理速度,最多可以加快 500 倍
@ZararYounis 这不会改变代码的渐近复杂度。如果在 Python 中生成所有子集是指数级的,那么在 C 中仍然如此。
你的大名单有多长?如果您在 dictcomp 中使用 biglist
之前将其设置为一组,情况会如何改善?
【参考方案1】:
根据字典的大小和键的长度,既不检查字典中的所有键也不枚举所有子集并检查它们是一个很好的解决方案。相反,您可以将您的“平面”字典重组为 Trie, or Prefix Tree 之类的东西。这里,集合中的每个元素都将指向树的另一个分支和/或一个实际值:
dictionary =
frozenset([1]): 1,
frozenset([2]): 2,
frozenset([3]): 3,
frozenset([3, 4]): 34
def totree(d):
tree =
for key in d:
t = tree
for x in sorted(key):
t = t.setdefault(x, )
t["value"] = d[key]
return tree
tree = totree(dictionary)
# 1: 'value': 1, 2: 'value': 2, 3: 'value': 3, 4: 'value': 34
现在,您可以递归地检查这些树和yield
每个具有值的键。除了枚举所有子集之外,这只会扩展到目前为止所有元素都在树中的那些分支。
def check_subsets(tree, key, prefix=[]):
if "value" in tree:
yield prefix, tree["value"]
for i, x in enumerate(key):
if x in tree:
yield from check_subsets(tree[x], key[i+1:], prefix+[x])
biglist= [3, 4, 5]
res = list(check_subsets(tree, sorted(biglist)))
# [([3], 3), ([3, 4], 34)]
请注意,树中的键和用于查找的键都必须按排序顺序添加/检查,否则可能会丢失相关的子树。
附录 1:这应该很清楚,但只是为了确保:当然,如果您为每次查找重新构建树,这种方法将无济于事,否则您也可以做一个线性扫描所有键。相反,您必须创建一次树,然后重复使用它进行多次查找,并可能使用添加到集合中的新元素来更新它。
附录 2:除了"value"
,您当然可以使用任何键作为前缀树中该节点处的实际值。您可以使用None
,或者一个很长的字符串或一个很大的随机数,保证不会成为您的任何键集中的元素。您可以通过对 totree
和 check_subtree
函数进行一些修改,也可以定义一个自定义 Tree
类...
class Tree:
def __init__(self, value=None, children=None):
self.value = value
self.children = children or
def __repr__(self):
return "Tree(%r, %r)" % (self.value, self.children)
...但是恕我直言,仅使用带有一些特殊值键的嵌套字典更简单。
【讨论】:
我认为前缀样式的方法在这里是正确的,但我们可以做得比香草前缀树好一点。我认为我们想要构建一个“子集”树,其中每个节点都是(其中一个)它的子集的子节点,它也是树中最长的子集。这样树就更紧凑了,例如“abc”和“acd”都可以是“ac”的子节点。 也许可以,但是在树中的构造和查找都会更加困难。您不能只检查“是树中的下一个元素吗?”但是“是下一个,或者可能是下两个,还是树中的下 n 个元素?”这棵树会更紧凑,但是(除非我在这里遗漏了什么)它有点违背了树的前缀性质。 我已经测试过这个解决方案,它就像一个魅力。快速索引创建和快速搜索。我发现的唯一问题是使用“值”作为键,因为它可能是集合成员。 @demon.mhm 很高兴它成功了。关于值:您可以使用任何(可散列的)对象作为“特殊”键,包括例如None
,或一千个字符的字符串。您还可以为值和子项创建一个单独的 TreeNode
类,而不是对两者都使用 dict。
@demon.mhm 不确定你的意思。您不能只将值添加为前缀键的值,因为前缀键可以同时具有值和子树,如3
的情况。【参考方案2】:
这个答案大致基于前缀树的想法,但它来自一个稍微不同的技巧。基本上,我们想弄清楚当我们使用某种提前停止枚举所有子集时,如何避免触及整个搜索空间。
如果我们将数据安排到“子集树”中,使得节点的所有子节点都是该节点的超集,那么只要我们到达不是当前查询子集的节点,我们就可以停止探索树,因为我们知道它的所有孩子也不会是子集。当我们构建树时,我们希望选择长父节点而不是短父节点,因为这会增加我们搜索中提前停止的数量。
如果你把所有这些放在一起,它看起来像这样:
class SubsetTree:
def __init__(self, key):
self.key = key
self.children = []
def longestSubset(self, query):
if not self.key.issubset(query):
return None
more = (x.longestSubset(query) for x in self.children)
more = filter(lambda i: i is not None, more)
return max(more, key=lambda x: len(x.key), default=self)
def allSubsets(self, query):
if not self.key.issubset(query):
return
if len(self.key) > 0:
yield self.key
for c in self.children:
yield from c.allSubsets(query)
def buildSubtree(sets):
sets = sorted(sets, key=lambda x: len(x))
tree = SubsetTree(frozenset())
for s in sets:
node = SubsetTree(s)
tree.longestSubset(s).children.append(node)
return tree
dictionary =
frozenset([1]): 1,
frozenset([2]): 2,
frozenset([3]): 3,
frozenset([3, 4]): 34
biglist= [3, 4, 5]
subsetTree = buildSubtree(dictionary.keys())
allSubsets = subsetTree.allSubsets(set(biglist))
results = k: dictionary[k] for k in allSubsets
assert results == frozenset([3]): 3, frozenset([3, 4]): 34
【讨论】:
看起来很结实。我会将其标记为解决方案,但在 1.5M 2-20 长度键上对其进行测试给了我大约 10 个小时的计算时间,而这个时间仍然从 50K 键(3%)完成。我会尝试搜索任何优化来补充这个答案 很有趣,但是否可以在以后使用新密钥更新树?似乎必须按长度顺序添加键。但是,我猜可能发生的最坏情况是树中有冗余子集。有一个更大的测试集来进行一些性能比较会很有趣。【参考方案3】:如何将frozenset键和给定集合编码成二进制代码?
然后你可以对frozenset键和给定集合的代码进行bit-and,如果这个结果等于frozenset键的二进制代码,那么键就是给定集合的子集。
这种方式预先计算了给定的集合,我认为它会更快。
在这种情况下:
dictionary =
frozenset([1]): 1, # a
frozenset([2]): 2, # b
frozenset([3]): 3, # c
frozenset([3, 4]): 34 # d
biglist= [3, 4, 5] # z
a: 10000
b: 01000
c: 00100
d: 00110
z: 00111
a & z = 00000 != a = 10000, no
b & z = 00000 != b = 01000, no
c & z = 00100 == c = 00100, yes
d & z = 00110 == d = 00110, yes
【讨论】:
二进制 '&' 不会给我们子集,而是设置交集。此外,它仍然需要扫描所有键来计算。基本上你的建议是建立词汇入口的位矩阵,并通过点积与查询集中相同词汇的向量进行查询。这再次为我们提供了只有交集,我们必须再次与查询进行比较才能仅获取子集。【参考方案4】:在这里有点通用,我认为您可以使用名为Bloom Filter 的数据结构来快速丢弃绝对不在集合中的内容。稍后您可以对可能的候选人进行简单的扫描,希望这最后一步只是您设置的一小部分。
这是布隆过滤器的 Python 实现:https://github.com/axiak/pybloomfiltermmap
引用他们的例子:
>>> fruit = pybloomfilter.BloomFilter(100000, 0.1, '/tmp/words.bloom')
>>> fruit.update(('apple', 'pear', 'orange', 'apple'))
>>> len(fruit)
3
>>> 'mike' in fruit
False
>>> 'apple' in fruit
True
【讨论】:
我不认为布隆过滤器在这里工作,布隆过滤器对检查相等性有好处,它们对检查issubset
没有多大帮助。
我同意这不是解决这个问题的好方法,但绝对实现了一个集合操作来测试给定条目何时是集合的成员。引用的***文章的第一行告诉你。以上是关于快速查找所有子集的主要内容,如果未能解决你的问题,请参考以下文章