How to find connected components?
Asked Answered
S

4

16

I'm writing a function get_connected_components for a class Graph:

def get_connected_components(self):
    path=[]
    for i in self.graph.keys():
        q=self.graph[i]
        while q:
            print(q)
            v=q.pop(0)
            if not v in path:
                path=path+[v]
    return path

My graph is:

{0: [(0, 1), (0, 2), (0, 3)], 1: [], 2: [(2, 1)], 3: [(3, 4), (3, 5)], \
4: [(4, 3), (4, 5)], 5: [(5, 3), (5, 4), (5, 7)], 6: [(6, 8)], 7: [], \
8: [(8, 9)], 9: []}

where the keys are the nodes and the values are the edge. My function gives me this connected component:

[(0, 1), (0, 2), (0, 3), (2, 1), (3, 4), (3, 5), (4, 3), (4, 5), (5, 3), \
(5, 4), (5, 7), (6, 8), (8, 9)]

But I would have two different connected components, like:

[[(0, 1), (0, 2), (0, 3), (2, 1), (3, 4), (3, 5), (4, 3), (4, 5), \
(5, 3), (5, 4), (5, 7)],[(6, 8), (8, 9)]]

I don't understand where I made the mistake. Can anyone help me?

Spelter answered 24/4, 2012 at 15:23 Comment(8)
Note that your representation include redundant information, eg. in 3: [(3, 4), (3, 5)]. We already know that the edge is starting from 3!Conoscenti
Do you suggest me to change the values in the dict and put only the node connected and no the edges?Spelter
BTW instead of for i in self.graph.keys(): q=self.graph[i] you can for (i, q) in self.graph.iteritems()Packsaddle
How can you expect to get a result like you want? The only way that you ever modify path is with the statement path = path + [v], which adds an edge to the list. If you want to create a list of lists of edges, then you need to have code that can make more than one list of edges, and add them to the list of list of edges...Lissa
Is there a reason you're creating your own graph? The awesome networkx library has a connected components algorithm built-in.Irrepealable
Yes I want create my own graph to improve my skills in python programmingSpelter
@Irrepealable As a reader of this post and many others, I would always welcome new implementations even naive rather than referring to an existing package.Sweptwing
@Sweptwing I always ask because a lot of times it's homework...Irrepealable
R
5

Let's simplify the graph representation:

myGraph = {0: [1,2,3], 1: [], 2: [1], 3: [4,5],4: [3,5], 5: [3,4,7], 6: [8], 7: [],8: [9], 9: []}

Here we have the function returning a dictionary whose keys are the roots and whose values are the connected components:

def getRoots(aNeigh):
    def findRoot(aNode,aRoot):
        while aNode != aRoot[aNode][0]:
            aNode = aRoot[aNode][0]
        return (aNode,aRoot[aNode][1])
    myRoot = {} 
    for myNode in aNeigh.keys():
        myRoot[myNode] = (myNode,0)  
    for myI in aNeigh: 
        for myJ in aNeigh[myI]: 
            (myRoot_myI,myDepthMyI) = findRoot(myI,myRoot) 
            (myRoot_myJ,myDepthMyJ) = findRoot(myJ,myRoot) 
            if myRoot_myI != myRoot_myJ: 
                myMin = myRoot_myI
                myMax = myRoot_myJ 
                if  myDepthMyI > myDepthMyJ: 
                    myMin = myRoot_myJ
                    myMax = myRoot_myI
                myRoot[myMax] = (myMax,max(myRoot[myMin][1]+1,myRoot[myMax][1]))
                myRoot[myMin] = (myRoot[myMax][0],-1) 
    myToRet = {}
    for myI in aNeigh: 
        if myRoot[myI][0] == myI:
            myToRet[myI] = []
    for myI in aNeigh: 
        myToRet[findRoot(myI,myRoot)[0]].append(myI) 
    return myToRet  

Let's try it:

print getRoots(myGraph)

{8: [6, 8, 9], 1: [0, 1, 2, 3, 4, 5, 7]}

Raymonderaymonds answered 1/10, 2012 at 11:32 Comment(0)
A
26

I like this algorithm:

def connected_components(neighbors):
    seen = set()
    def component(node):
        nodes = set([node])
        while nodes:
            node = nodes.pop()
            seen.add(node)
            nodes |= neighbors[node] - seen
            yield node
    for node in neighbors:
        if node not in seen:
            yield component(node)

Not only is it short and elegant, but also fast. Use it like so (Python 2.7):

old_graph = {
    0: [(0, 1), (0, 2), (0, 3)],
    1: [],
    2: [(2, 1)],
    3: [(3, 4), (3, 5)],
    4: [(4, 3), (4, 5)],
    5: [(5, 3), (5, 4), (5, 7)],
    6: [(6, 8)],
    7: [],
    8: [(8, 9)],
    9: []}

edges = {v for k, vs in old_graph.items() for v in vs}
graph = defaultdict(set)

for v1, v2 in edges:
    graph[v1].add(v2)
    graph[v2].add(v1)

components = []
for component in connected_components(graph):
    c = set(component)
    components.append([edge for edges in old_graph.values()
                            for edge in edges
                            if c.intersection(edge)])

print(components)

The result is:

[[(0, 1), (0, 2), (0, 3), (2, 1), (3, 4), (3, 5), (4, 3), (4, 5), (5, 3), (5, 4), (5, 7)],
 [(6, 8), (8, 9)]]

Thanks, aparpara for spotting the bug.

Allodial answered 12/12, 2012 at 9:52 Comment(2)
Strictly speaking, it is incorrect. E.g. it gives 2 components for {1: {2}, 2 : set(), 3: {2}}, while the accepted answer correctly gives 1. But it can be easily fixed by making the graph bi-directional before applying the algorithm.Fakieh
@aparpara: fixed it.Allodial
M
6

The previous answer is great. Anyway, it took to me a bit to understand what was going on. So, I refactored the code in this way that is easier to read for me. I leave here the code in case someone founds it easier too (it runs in python 3.6)

def get_all_connected_groups(graph):
    already_seen = set()
    result = []
    for node in graph:
        if node not in already_seen:
            connected_group, already_seen = get_connected_group(node, already_seen)
            result.append(connected_group)
    return result


def get_connected_group(node, already_seen):
        result = []
        nodes = set([node])
        while nodes:
            node = nodes.pop()
            already_seen.add(node)
            nodes = nodes or graph[node] - already_seen
            result.append(node)
        return result, already_seen


graph = {
     0: {0, 1, 2, 3},
     1: set(),
     2: {1, 2},
     3: {3, 4, 5},
     4: {3, 4, 5},
     5: {3, 4, 5, 7},
     6: {6, 8},
     7: set(),
     8: {8, 9},
     9: set()}

components = get_all_connected_groups(graph)
print(components)

Result:

Out[0]: [[0, 1, 2, 3, 4, 5, 7], [6, 8, 9]] 

Also, I simplified the input and output. I think it's a bit more clear to print all the nodes that are in a group

Mulford answered 1/6, 2018 at 8:31 Comment(3)
"nodes = nodes or graph[node] - already_seen" should be "nodes.update(graph[node] - already_seen)"Historiated
@Historiated if graph[5] = {3, 4, 5, 8}, shouldn't we get get one connected componentBuskined
I corrected the error in the line "nodes = nodes or graph[node] - already_seen" to 'nodes.update(n for n in graph[node] if n not in already_seen)'Hiltan
R
5

Let's simplify the graph representation:

myGraph = {0: [1,2,3], 1: [], 2: [1], 3: [4,5],4: [3,5], 5: [3,4,7], 6: [8], 7: [],8: [9], 9: []}

Here we have the function returning a dictionary whose keys are the roots and whose values are the connected components:

def getRoots(aNeigh):
    def findRoot(aNode,aRoot):
        while aNode != aRoot[aNode][0]:
            aNode = aRoot[aNode][0]
        return (aNode,aRoot[aNode][1])
    myRoot = {} 
    for myNode in aNeigh.keys():
        myRoot[myNode] = (myNode,0)  
    for myI in aNeigh: 
        for myJ in aNeigh[myI]: 
            (myRoot_myI,myDepthMyI) = findRoot(myI,myRoot) 
            (myRoot_myJ,myDepthMyJ) = findRoot(myJ,myRoot) 
            if myRoot_myI != myRoot_myJ: 
                myMin = myRoot_myI
                myMax = myRoot_myJ 
                if  myDepthMyI > myDepthMyJ: 
                    myMin = myRoot_myJ
                    myMax = myRoot_myI
                myRoot[myMax] = (myMax,max(myRoot[myMin][1]+1,myRoot[myMax][1]))
                myRoot[myMin] = (myRoot[myMax][0],-1) 
    myToRet = {}
    for myI in aNeigh: 
        if myRoot[myI][0] == myI:
            myToRet[myI] = []
    for myI in aNeigh: 
        myToRet[findRoot(myI,myRoot)[0]].append(myI) 
    return myToRet  

Let's try it:

print getRoots(myGraph)

{8: [6, 8, 9], 1: [0, 1, 2, 3, 4, 5, 7]}

Raymonderaymonds answered 1/10, 2012 at 11:32 Comment(0)
D
1

If you represent the graph using an adjacency list, you can use this generator function (implementing BFS) to get all connected components:

from collections import deque

def connected_components(graph):
    seen = set()

    for root in range(len(graph)):
        if root not in seen:
            seen.add(root)
            component = []
            queue = deque([root])

            while queue:
                node = queue.popleft()
                component.append(node)
                for neighbor in graph[node]:
                    if neighbor not in seen:
                        seen.add(neighbor)
                        queue.append(neighbor)
            yield component

Demo:

graph = [
    [1, 2, 3],  # neighbors of node "0"
    [0, 2],     # neighbors of node "1"
    [0, 1],     # ...
    [0, 4, 5],
    [3, 5],
    [3, 4, 7],
    [8],
    [5],
    [9, 6],
    [8]
]

print(list(connected_components(graph)))  # [[0, 1, 2, 3, 4, 5, 7], [6, 8, 9]]
Dey answered 21/9, 2019 at 12:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.