用于查找所有最大子集的高效算法

Mar*_*ato 26 algorithm set

我有一组独特的集合(表示为位掩码),并希望消除作为另一个元素的正确子集的所有元素.例如:

input = [{1, 2, 3}, {1, 2}, {2, 3}, {2, 4}, {}]
output = [{1, 2, 3}, {2, 4}]
Run Code Online (Sandbox Code Playgroud)

我无法为此找到一个标准算法,甚至找不到这个问题的名称,所以我称其为"最大子集",因为缺少其他任何东西.这是一个O(n ^ 2)算法(在Python中具体化),假设is_subset_func是O(1):1

def eliminate_subsets(a, cardinality_func, is_subset_func):
    out = []
    for element in sorted(a, reverse=True, key=cardinality_func):
        for existing in out:
            if is_subset_func(element, existing):
                break
        else:
            out.append(element)
    return out
Run Code Online (Sandbox Code Playgroud)

是否有更高效的算法,希望是O(n log n)还是更好?


1 对于常量大小的位掩码,在我的情况下is_subset_func也是如此,它只是element & existing == element在恒定时间内运行.

Gen*_*ene 15

假设您标记了所有输入集.

A={1, 2, 3}, B={1, 2}, C={2, 3}, D={2, 4}, E={}
Run Code Online (Sandbox Code Playgroud)

现在构建中间集,每个元素在Universe中一个,包含它出现的集合的标签:

1={A,B}
2={A,B,C,D}
3={A,C}
4={D}
Run Code Online (Sandbox Code Playgroud)

现在为每个输入集计算其元素的所有标签集的交集:

For A, {A,B} intesect {A,B,C,D} intersect {A,C} = {A}   (*)
Run Code Online (Sandbox Code Playgroud)

如果交集包含除集合本身之外的某些标签,那么它就是该集合的子集.这里没有其他元素,所以答案是否定的.但,

For C, {A,B,C,D} intersect {A,C} = {A,C}, which means that it's a subset of A.
Run Code Online (Sandbox Code Playgroud)

此方法的成本取决于集的实现.假设位图(正如您所暗示的那样).假设有n个最大大小为m和| U |的输入集 宇宙中的物品.然后中间组构造产生| U | 大小为n位的集合,因此有O(| U | n)时间来初始化它们.设置位需要O(nm)时间.(*)如上所述计算每个交叉点需要O(mn); O(mn ^ 2)为所有人.

将所有这些放在一起我们得到O(| U | n)+ O(nm)+ O(mn ^ 2)= O(| U | n + mn ^ 2).使用相同的约定,您的"所有对"算法是O(| U | ^ 2 n ^ 2).由于m <= | U |,因此该算法渐近地更快.它在实践中也可能更快,因为没有精心设置的簿记来增加常数因素.

增加:在线版本

OP询问是否存在该算法的在线版本,即,当输入集一个接一个地到达时,可以递增地保持最大集合集合.答案似乎是肯定的.中间集快速告诉我们新集是否是已经看到的集的子集.但如何快速判断它是否是超集?如果是这样,其中现有的最大集合?因为在这种情况下,那些最大集合不再是最大集合,必须用新的集合替换.

关键是要注意,iff 是(tick'表示集补集)的子集的A超集.BA'B'

在这个灵感之后,我们像以前一样保持中间组.当新输入集S到达时,执行与上述相同的测试:I(e)设为输入元素的中间集e.然后这个测试是

For X = \intersect_{e \in S} . I(e), |X| > 0
Run Code Online (Sandbox Code Playgroud)

(在这种情况下,它大于零而不是如上所述,因为S还没有I.)如果测试成功,则新集合是现有最大集合的(可能是不正确的)子集,因此可以将其丢弃.

否则我们必须添加S为新的最大集合,但在此之前,计算:

Y = \intersect_{e \in S'} . I'(e) = ( \union_{e \in S'} . I(e) )'
Run Code Online (Sandbox Code Playgroud)

再次,tick'设置为补码.联合表单的计算速度可能会快一些.Y包含已被取代的最大集合S.必须从最大集合中删除它们I.最后添加S为最大集并I使用S's元素进行更新.

让我们通过我们的例子.当A到达时,我们把它添加到I和有

1={A}  2={A}  3={A}
Run Code Online (Sandbox Code Playgroud)

B到达时,我们发现X = {A} intersect {A} = {A},所以扔B走,继续.同样的事情发生了C.当D到达时,我们发现X = {A} intersect {} = {},如此继续Y = I'(1) intersect I'(3) = {} intersect {}.这正确地告诉我们最大集合A不包含在内D,因此没有什么可以删除.但它必须作为一个新的最大集添加,并I成为

1={A}  2={A,D}  3={A}  4={D}
Run Code Online (Sandbox Code Playgroud)

到来的E原因没有变化.然后是新组的到来F={2, 3, 4, 5}.我们发现

X = {A} isect {A,D} isect {A} isect {D} isect {}
Run Code Online (Sandbox Code Playgroud)

所以我们不能扔掉F.继续

Y = \intersect_{e in {1}} I'(e) = I'(1) = {D}
Run Code Online (Sandbox Code Playgroud)

这告诉我们D是一个子集F,所以应该在F添加时丢弃,离开

1={A} 2={A,F} 3={A,F} 4={F} 5={F}
Run Code Online (Sandbox Code Playgroud)

由于算法的在线性质,补码的计算既棘手又好.输入补充的Universe只需要包含到目前为止看到的输入元素.中间集的Universe仅包含当前最大集合中的集合的标记.对于许多输入流,该组的大小将随着时间的推移而稳定或减小.

我希望这是有帮助的.

摘要

这里的一般原则是一个强大的想法,即经常在算法设计中的作物.这是反向地图.每当您发现自己进行线性搜索以查找具有给定属性的项目时,请考虑从属性到项目构建地图.构建此地图通常很便宜,并且大大缩短了搜索时间.最重要的例子是一个排列图p[i],它告诉你i在排列数组后第th个元素占据的位置.如果你需要去寻找,在一个给定的位置结束了该项目a,则必须搜索pa,线性时间的操作.另一方面,逆映射pi使得pi[p[i]] == i不再需要计算p(因此其成本是"隐藏的"),但是pi[a]在恒定时间内产生期望的结果.

原始海报的实施

import collections
import operator

def is_power_of_two(n):
    """Returns True iff n is a power of two.  Assumes n > 0."""
    return (n & (n - 1)) == 0

def eliminate_subsets(sequence_of_sets):
    """Return a list of the elements of `sequence_of_sets`, removing all
    elements that are subsets of other elements.  Assumes that each
    element is a set or frozenset and that no element is repeated."""
    # The code below does not handle the case of a sequence containing
    # only the empty set, so let's just handle all easy cases now.
    if len(sequence_of_sets) <= 1:
        return list(sequence_of_sets)
    # We need an indexable sequence so that we can use a bitmap to
    # represent each set.
    if not isinstance(sequence_of_sets, collections.Sequence):
        sequence_of_sets = list(sequence_of_sets)
    # For each element, construct the list of all sets containing that
    # element.
    sets_containing_element = {}
    for i, s in enumerate(sequence_of_sets):
        for element in s:
            try:
                sets_containing_element[element] |= 1 << i
            except KeyError:
                sets_containing_element[element] = 1 << i
    # For each set, if the intersection of all of the lists in which it is
    # contained has length != 1, this set can be eliminated.
    out = [s for s in sequence_of_sets
           if s and is_power_of_two(reduce(
               operator.and_, (sets_containing_element[x] for x in s)))]
    return out
Run Code Online (Sandbox Code Playgroud)