【问题标题】:Find sets that contain at least one element from other sets从其他集合中查找至少包含一个元素的集合
【发布时间】:2020-05-28 06:18:48
【问题描述】:

假设我们有 n 个集合,并且想要构造 所有最小 个集合,这些集合与每个输入集合至少有一个共同元素。如果不存在作为 S 子集的可接纳集 S',则称集 S 为极小。

一个例子:

In: s1 = {1, 2, 3}; s2 = {3, 4, 5}; s3 = {5, 6}

Out: [{1, 4, 6}, {1, 5}, {2, 4, 6}, {2, 5}, {3, 5}, {3, 6}]

我的想法是迭代地将一组接一组添加到解决方案中:

result = f(s1, f(s2, f(s3, ...)))

其中f 是一个合并函数,如下所示:

function f(newSet, setOfSets):
   Step 1: 
      return all elements of setOfSets that share an element with newSet

   Step 2: 
      for each remaining element setE of setOfSets:
         for each element e of newSet:
            return union(setE, {e})

上述方法的问题是,在步骤 2 中计算的笛卡尔积可能包含在步骤 1 中返回的集合的超集。我正在考虑遍历所有已返回的集合(请参阅Find minimal set of subsets that covers a given set),但这似乎是太复杂,效率低下,希望在我的特殊情况下有更好的解决方案。

如果没有在第 2 步中确定完整的笛卡尔积,我如何实现目标?

请注意,这个问题与the question of finding the smallest set only 有关,但我需要找到所有 集以上述方式最小化。我知道解决方案的数量不会是多项式的。

输入集的数量 n 将是几百个,但这些集仅包含有限范围内的元素(例如大约 20 个不同的值),这也限制了集的大小。如果算法在 O(n^2) 中运行是可以接受的,但它应该基本上是输出集的线性(可能带有对数乘数)。

【问题讨论】:

  • 每个输出集是否应该是输入集并集的子集?我没有看到明确说明,但没有这个限制,可能的输出集的数量将是无限的,因此不可构造!
  • 至少一个还是正好一个?换句话说,为什么{1, 3, 5}不在Out中?
  • n 可能的最大值是多少?
  • @user58697 它不能(总是)完全是一个,因为输入集可能不允许它。例如,输入:{1, 2}, {1}, {2};输出:{1, 2}。这里(唯一的)输出集必须同时具有 1 和 2,即使这不仅仅是第一个输入集中的元素之一。但也许意图是“至少有一个,并且仅是必要的”?毕竟,“最小”这个词被提到了两次。
  • @user58697 Nick Russo 的评论是正确的,它是“至少”而不是“完全”(另请参见问题中的示例)。 “不超过必要”的条件是返回的集合都不能是任何其他返回的集合的超集。

标签: python algorithm set


【解决方案1】:

由于您的空间非常有限——只有 20 个值可供选择——用钝器将这个东西打死:

  1. 将每个目标集(要覆盖的目标集)转换为位图。在您给定的情况下,这将对应于 20 位的整数,20 个值中的每一个都有一个位位置。
  2. 创建候选覆盖位图列表,整数 0 到 (2^20-1)
  3. 按顺序取整数。使用位运算来确定每个目标集是否具有与候选对象相同的1 位。如果都满足基本条件,则候选人通过验证。
  4. 验证候选者时,从候选者列表中删除所有超集整数。
  5. 当您用完候选项时,您的 validates 候选项就是所需的集合。在下面的代码中,我只是将每个标识的内容打印出来。

代码:

from time import time
start = time()

s1 = {1, 2, 3}
s2 = {3, 4, 5}
s3 = {5, 6}

# Convert each set to its bit-map
point_set = [7, 28, 48]

# make list of all possible covering bitmaps
cover = list(range(2**20))

while cover:
    # Pop any item from remaining covering sets
    candidate = cover.pop(0)
    # Does this bitmap have a bit in common with each target set?
    if all((candidate & point) for point in point_set):
        print(candidate)

        # Remove all candidates that are supersets of the successful covering one.
        superset = set([other for other in cover if (candidate & ~other) == 0])
        cover = [item for item in cover if item not in superset]
        print(time() - start, "lag time")

print(time() - start, "seconds")

输出——我没有将候选整数转换回它们的组成元素。这是一项简单的任务。

请注意,此示例中的大部分时间都花在了穷尽整数列表中,这些整数列表不是经过验证的覆盖集的超集,例如所有 32 的倍数(低 6 位都是零,因此与任何覆盖集都不相交)。

这 33 秒是在我老旧的台式电脑上;您的笔记本电脑或其他平台几乎可以肯定更快。我相信从更高效的算法中得到的任何改进很容易被抵消,因为该算法实现速度快且易于理解。

17
0.4029195308685303 lag time
18
0.6517734527587891 lag time
20
0.8456630706787109 lag time
36
1.0555419921875 lag time
41
1.2604553699493408 lag time
42
1.381387710571289 lag time
33.005757570266724 seconds

【讨论】:

  • 这是一个有趣的想法。但是,我想知道该算法的扩展性如何。 20 只是一个数量级,如果有 40 个值,我不希望程序崩溃。也就是说,如果运行时 analsys 在某处包含输入变量数量的因子,那将是可以接受的。但是,据我了解您的算法,它以输入变量数量的指数时间运行。如果我错了,请纠正我。
  • 你说得对;这在时间和空间上都是指数级的。候选列表的长度为 2^N,最终将删除或接受这些元素中的每一个。尽管删除是在有效的列表理解中完成的,但算法仍然是 O(2^N)。由于您指定了“大约 20”的限制,因此理论上的复杂性似乎被实际考虑所覆盖。
【解决方案2】:

我已经提出了一个基于here 描述的 trie 数据结构的解决方案。尝试可以相对快速地确定一个存储的集合是否是另一个给定集合的子集 (Savnik, 2013)。

然后解决方案如下:

  • 创建一个树
  • 遍历给定的集合
    • 在每次迭代中,遍历 trie 中的集合并检查它们是否与新集合不相交。
    • 如果是,请继续;如果不是,则将相应的新集合添加到树中,除非它们是树中集合的超集。

最坏情况的运行时间是 O(n m c),其中 m 是我们仅考虑 n' 的输入集,c 是来自子集查找的时间因子。

代码如下。我已经实现了基于 python 包datrie 的算法,它是一个围绕 trie 的有效 C 实现的包装器。下面的代码在 cython 中,但可以通过删除/交换 cython 特定命令轻松转换为纯 python。

扩展的 trie 实现:

from datrie cimport BaseTrie, BaseState, BaseIterator

cdef bint has_subset_c(BaseTrie trie, BaseState trieState, str setarr, 
                        int index, int size):
    cdef BaseState trieState2 = BaseState(trie)
    cdef int i
    trieState.copy_to(trieState2)
    for i in range(index, size):
        if trieState2.walk(setarr[i]):
            if trieState2.is_terminal() or has_subset_c(trie, trieState2, setarr, 
                                                        i, size): 
                return True
            trieState.copy_to(trieState2)
    return False


cdef class SetTrie():
    def __init__(self, alphabet, initSet=[]):
        if not hasattr(alphabet, "__iter__"):
            alphabet = range(alphabet)
        self.trie = BaseTrie("".join(chr(i) for i in alphabet))
        self.touched = False
        for i in initSet:
            self.trie[chr(i)] = 0
            if not self.touched:
                self.touched = True

    def has_subset(self, superset):
        cdef BaseState trieState = BaseState(self.trie)
        setarr = "".join(chr(i) for i in superset)
        return bool(has_subset_c(self.trie, trieState, setarr, 0, len(setarr)))

    def extend(self, sets):
        for s in sets:
            self.trie["".join(chr(i) for i in s)] = 0
            if not self.touched:
                self.touched = True

    def delete_supersets(self):
        cdef str elem 
        cdef BaseState trieState = BaseState(self.trie)
        cdef BaseIterator trieIter = BaseIterator(BaseState(self.trie))
        if trieIter.next():
            elem = trieIter.key()
            while trieIter.next():
                self.trie._delitem(elem)
                if not has_subset_c(self.trie, trieState, elem, 0, len(elem)):
                    self.trie._setitem(elem, 0)
                elem = trieIter.key()
            if has_subset_c(self.trie, trieState, elem, 0, len(elem)):
                val = self.trie.pop(elem)
                if not has_subset_c(self.trie, trieState, elem, 0, len(elem)):
                    self.trie._setitem(elem, val)


    def update_by_settrie(self, SetTrie setTrie, maxSize=inf, initialize=True):
        cdef BaseIterator trieIter = BaseIterator(BaseState(setTrie.trie))
        cdef str s
        if initialize and not self.touched and trieIter.next():
            for s in trieIter.key():
                self.trie._setitem(s, 0)
            self.touched = True

        while trieIter.next():
            self.update(set(trieIter.key()), maxSize, True)

    def update(self, otherSet, maxSize=inf, isStrSet=False):
        if not isStrSet:
            otherSet = set(chr(i) for i in otherSet)
        cdef str subset, newSubset, elem
        cdef list disjointList = []
        cdef BaseTrie trie = self.trie
        cdef int l
        cdef BaseIterator trieIter = BaseIterator(BaseState(self.trie))
        if trieIter.next():
            subset = trieIter.key()
            while trieIter.next():
                if otherSet.isdisjoint(subset):
                    disjointList.append(subset)
                    trie._delitem(subset)
                subset = trieIter.key()
            if otherSet.isdisjoint(subset):
                disjointList.append(subset)
                trie._delitem(subset)

        cdef BaseState trieState = BaseState(self.trie)
        for subset in disjointList:
            l = len(subset)
            if l < maxSize:
                if l+1 > self.maxSizeBound:
                    self.maxSizeBound = l+1
                for elem in otherSet:
                    newSubset = subset + elem
                    trieState.rewind()
                    if not has_subset_c(self.trie, trieState, newSubset, 0, 
                                        len(newSubset)):
                        trie[newSubset] = 0

    def get_frozensets(self):
        return (frozenset(ord(t) for t in subset) for subset in self.trie)

    def clear(self):
        self.touched = False
        self.trie.clear()

    def prune(self, maxSize):
        cdef bint changed = False
        cdef BaseIterator trieIter 
        cdef str k
        if self.maxSizeBound > maxSize:
            self.maxSizeBound = maxSize
            trieIter = BaseIterator(BaseState(self.trie))
            k = ''
            while trieIter.next():
                if len(k) > maxSize:
                    self.trie._delitem(k)
                    changed = True
                k = trieIter.key()
            if len(k) > maxSize:
                self.trie._delitem(k)
                changed = True
        return changed

    def __nonzero__(self):
        return self.touched

    def __repr__(self):
        return str([set(ord(t) for t in subset) for subset in self.trie])

可以这样使用:

def cover_sets(sets):
    strie = SetTrie(range(10), *([i] for i in sets[0]))
    for s in sets[1:]:
        strie.update(s)
    return strie.get_frozensets()

时间:

from timeit import timeit
s1 = {1, 2, 3}
s2 = {3, 4, 5}
s3 = {5, 6}
%timeit cover_sets([s1, s2, s3])

结果:

37.8 µs ± 2.97 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-10-19
    • 1970-01-01
    • 2011-04-30
    • 2019-12-20
    • 1970-01-01
    • 1970-01-01
    • 2012-03-31
    • 2019-04-15
    相关资源
    最近更新 更多