获取在python中相互交叉的集列表

时间:2014-07-14 18:44:00

标签: python performance set intersection

给定一个集合列表,我想获得彼此相交的集合列表。基本上我想要的是列表清单s.t.对于输出中的每个列表,该列表中的所有集都具有非空交集,并且在同一列表中至少有另一个集合。

我希望我能够解释我的问题。希望以下示例和帖子的其余部分应该进一步澄清它。

鉴于,

sets = [
    set([1,3]), # A
    set([2,3,5]), # B
    set([21,22]), # C
    set([1,9]), # D
    set([5]), # E
    set([18,21]), # F
]

我想要的输出是:

[
    [
        set([1,3]), # A, shares elements with B
        set([2,3,5]), # B, shares elements with A 
        set([1,9]), # D, shares elements with A
        set([5]), # E shares elements with B
    ],
    [
        set([21,22]), # C shares elements with F
        set([18,21]), # F shares elements with C
    ]
]

输出中集合的顺序无关紧要。

我想用非常快的算法实现这个目标。表演是我的第一要求。

目前,我的解决方案创建了一个图表,其中包含与原始列表中的集合一样多的节点。然后,如果这些集合具有非空交集,则它在表示集合A和B的节点之间的该图中创建边缘。比它计算这样一个图形的连通分量,它给出了我预期的结果。

我想知道是否有更快的方法可以使用不涉及图形的算法。

最佳, 安德烈

2 个答案:

答案 0 :(得分:1)

正如@MartijnPieters正确地说的那样,这个问题需要图表,networkx将会帮助你。

突出点

  1. 图表的节点应为
  2. 如果集合相交,则图表之间存在边缘
  3. 从结果图中,找到所有connected components
  4. <强>实施

    def intersecting_sets(sets):
        import networkx as nx
        G = nx.Graph()
        # Nodes of the graph should be hashable
        sets = map(frozenset, sets)
        for to_node in sets:
            for from_node in sets:
                # off-course you don't want a self loop
                # and only interested in intersecting nodes 
                if to_node != from_node and to_node & from_node:
                    G.add_edge(to_node, from_node)
        # and remember to convert the frozen sets to sets
        return [map(set, lst) for lst in nx.connected_components(G)]
    

    <强>输出

    >>> intersecting_sets(sets)
    [[set([2, 3, 5]), set([1, 3]), set([5]), set([1, 9])], [set([21, 22]), set([18, 21])]]
    >>> pprint.pprint(intersecting_sets(sets))
    [[set([2, 3, 5]), set([1, 3]), set([5]), set([1, 9])],
     [set([21, 22]), set([18, 21])]]
    

答案 1 :(得分:1)

我只看到在sets上进行多次传递的解决方案(通常是O(N ^ 2))。因此,出于好奇,我想看看是否可以在sets上只传递一次(即O(N))。

使用原始问题的输入集,这是一个如何做的例子 算法遍历列表中的每个集合。

[
    set([1,3]), # A
    set([2,3,5]), # B
    set([21,22]), # C
    set([1,9]), # D
    set([5]), # E
    set([18,21]), # F
]

我们从Set A(set([1,3]))开始,并假设它是新结果列表L1的一部分。 我们还为我们的当前结果列表(目前为L1)维护一个指针C1。 这个指针的目的将在后面解释:

L1 = [set([1,3]),]
C1 = Pointer(L1)

对于集合A中的每个整数,我们还更新映射M:

{
    1: C1, # Pointer(L1)
    3: C1, # Pointer(L1)
}

下一个项目是设置B(set([2,3,5])),我们假设它是新结果的一部分 列表L2。

L2 = [set([2,3,5]),]
C2 = Pointer(L2)

再一次,我们迭代这个集合的成员并更新我们的映射M.明确地, 我们看到2不在M中,我们更新它以便我们:

{
    1: C1, # Pointer(L1)
    3: C1, # Pointer(L1)
    2: C2, # Pointer(L2)
}

但是,当我们看到3时,我们注意到它已经指向另一个结果 列表,L1。 这表示有一个交叉点,我们需要做两个 事情:

  1. 更新C2以指向L1
  2. 将Set B添加到L1
  3. 我们应该以M看起来像:

    {
        1: C1, # Pointer(L1)
        3: C1, # Pointer(L1)
        2: C2, # Pointer(L1)
    }
    

    L1现在应该是:

    [[set([1,3]), set([2,3,5])]
    

    这就是我们需要Pointer类的原因:我们不能只将C2分配给新的 指针()实例。如果我们这样做,那么M看起来像:

    {
        1: C1, # Pointer(L1)
        3: C1, # Pointer(L1)
        2: C2, # Pointer(L2) <-- this should be pointing to L1
    }
    

    相反,我们做了类似的事情:

    C2.set(L1)
    

    在我们处理了Set B和Set C之后,M的状态就应该先行了 看起来像:

    {
        1: C1, # Pointer(L1)
        3: C1, # Pointer(L1)
        2: C2, # Pointer(L1)
        5: C2, # Pointer(L1)
        21: C3, # Pointer(L3)
        22: C3, # Pointer(L3)
    }
    

    结果列出:

    [set([1,3]), set([2,3,5])] # L1
    [set([21,22]), ]           # L3
    
    [set([2,3,5]), ]           # L2 (but nothing in M points to it)
    

    当我们看到Set D时,我们再次假设它是新结果列表L4的一部分 创建相应的指针,C4指向L4。但是,当我们看到 看到Set D包含与L1相交的1,我们将C4更改为point 到L1并将Set D添加到L1。

    M的状态是:

    {
        1: C1, # Pointer(L1)
        3: C1, # Pointer(L1)
        2: C2, # Pointer(L1)
        5: C2, # Pointer(L1)
        21: C3, # Pointer(L3)
        22: C3, # Pointer(L3)
        9: C4, # Pointer(L1)
    }
    

    L1现在是:

    [set([1,3]), set([2,3,5]), set([1,9])]
    

    一直走到最后,M的状态将如下所示:

    {
        1: C1, # Pointer(L1)
        3: C1, # Pointer(L1)
        2: C2, # Pointer(L1)
        5: C2, # Pointer(L1)
        21: C3, # Pointer(L3)
        22: C3, # Pointer(L3)
        9: C4, # Pointer(L1)
        18: C6, # Pointer(L3)
    }
    

    列表如下:

    [set([1,3]), set([2,3,5]), set([1,9]), set([5])] # L1
    [set([21,22]), set([18, 21]) # L3
    

    此时,您可以迭代M中的指针并找到所有唯一列表 正在被引用,这将是你的结果。

    在原始代码中,我试图通过维护列表来避免这最后一步 独特的结果集即时。但是,我已经删除了该逻辑以保持代码更简单 因为无论如何,性能提升可能并不那么好。


    这是更新的代码,其中包含Andrea在评论中提到的bug修复程序。

    class Pointer(object):
        """
        Implements a pointer to an object. The actual object can be accessed
        through self.get().
        """
        def __init__(self, o): self.o = o
        def __str__(self): return self.o.__str__()
        def __repr__(self): return '<Pointer to %s>' % id(self.o)
    
        # These two methods make instances hashed on self.o rather than self
        # so Pointers to the same object will be considered the same in
        # dicts, sets, etc.
        def __eq__(self, x): return x.o is self.o
        def __hash__(self): return id(self.o)
    
        def get(self): return self.o
        def set(self, obj): self.o = obj.o
        def id(self): return id(self.o)
    
    def connected_sets(s):
        M = {}
        for C in s:
            # We assume C belongs to a new result list
            P = Pointer(list())
            added = False # to check if we've already added C to a result list
            for item in C:
                # There was an intersection detected, so the P points to this
                # intersecting set. Also add this C to the intersecting set.
                if item in M:
                    # The two pointers point to different objects.
                    if P.id() != M[item].id():
                        # If this was a regular assignment of a set to a new set, this
                        # wouldn't work since references to the old set would not be
                        # updated.
                        P.set(M[item])
                        M[item].o.append(C)
                        added = True
                # Otherwise, we can add it to P which, at this point, could point to
                # a stand-alone set or an intersecting set.
                else:
                    if not added:
                        P.o.append(C)
                        added = True
                    M[item] = P
    
        return M
    
    if __name__ == '__main__':
        sets = [
            set([1,3]), # A
            set([2,3,5]), # B
            set([21,22]), # C
            set([1,9]), # D
            set([5]), # E
            set([18,21]), # F
        ]
    
        #sets = [
        #    set([1,5]), # A
        #    set([1,5]), # B
        #]
    
        M = connected_sets(sets)
        import pprint
        pprint.pprint(
            # create list of unique lists referenced in M
            [x.o for x in set(M.values())]
            )
    

    输出为:

    [[set([1, 3]), set([2, 3, 5]), set([1, 9]), set([5])],
     [set([21, 22]), set([18, 21])]]
    

    它似乎对我有用,但我很好奇它与你的解决方案相比如何表现。使用timeit模块,我将其与使用networkx的上述答案进行了比较,我的脚本在同一数据集上的速度提高了近3倍。