Connectivity and Connected Components
Chapter Ninety-Six
Syllabus topic Outcome 5, "solve shortest path and connectivity problems"
Pages 312 to 316 of 411
In one line
A graph is connected when every vertex can be reached from every other; a connected component is a maximal set of vertices that can all reach one another; and both are decided by running a traversal and counting what it reached.
The definitions, precisely
Reachable. w is reachable from v when there is a path from v to w.
Connected graph. An undirected graph is connected when every vertex is reachable from every other vertex. Equivalently: one traversal from any single vertex reaches all V.
Connected component. A maximal connected piece. "Maximal" is the load-bearing word: a component cannot be made larger by adding another vertex of the graph, because if it could, that vertex was reachable and belonged in it already.
Disconnected graph. A graph with more than one component.
Isolated vertex. A vertex of degree zero. It is a component all by itself, which students regularly forget to count.
For directed graphs the words change, and the distinction is examinable:
Strongly connected. Every vertex can reach every other, following the arrow directions.
Weakly connected. Not strongly connected, but connected if the directions are ignored.
Deciding connectivity with one traversal
from collections import deque
def adjacency(pairs, vertices):
graph = {v: [] for v in vertices}
for a, b in pairs:
graph[a].append(b)
graph[b].append(a)
for v in graph:
graph[v].sort()
return graph
def reachable_from(graph, start):
"""Every vertex reachable from start. BFS, but DFS gives the same set."""
seen = {start}
queue = deque([start])
while queue:
v = queue.popleft()
for w in graph[v]:
if w not in seen:
seen.add(w)
queue.append(w)
return seen
def is_connected(graph):
if not graph:
return True # the empty graph, by convention
start = next(iter(graph))
return len(reachable_from(graph, start)) == len(graph)
joined = adjacency([("A", "B"), ("B", "C"), ("C", "D")], "ABCD")
split = adjacency([("A", "B"), ("C", "D")], "ABCD")
lonely = adjacency([("A", "B"), ("B", "C")], "ABCDE") # D and E are isolated
for name, g in (("a path A-B-C-D", joined),
("two pairs A-B and C-D", split),
("a path plus 2 isolated vertices", lonely)):
reached = reachable_from(g, "A")
print("%-34s connected: %-5s reached %d of %d from A"
% (name, is_connected(g), len(reached), len(g)))a path A-B-C-D connected: True reached 4 of 4 from A
two pairs A-B and C-D connected: False reached 2 of 4 from A
a path plus 2 isolated vertices connected: False reached 3 of 5 from AOne traversal settles it, and it costs exactly what the traversal costs: O(V + E).
Finding every component
A single traversal only reaches one component. To find them all, start a fresh traversal from every vertex not yet seen.
components(graph):
seen = empty
count = 0
for each vertex v in the graph:
if v not in seen:
count = count + 1
walk from v, adding everything reached to seen
return count
Connectivity and Connected Components
That outer loop is the whole algorithm, and it does not change the complexity: every vertex is still visited once and every edge still read once, so it is O(V + E) in total, not O(V) traversals.
from collections import deque
def adjacency(pairs, vertices):
graph = {v: [] for v in vertices}
for a, b in pairs:
graph[a].append(b)
graph[b].append(a)
for v in graph:
graph[v].sort()
return graph
def components(graph):
"""Every component, as a list of sorted vertex lists."""
seen = set()
found = []
reads = 0
for start in sorted(graph):
if start in seen:
continue
piece = {start}
seen.add(start)
queue = deque([start])
while queue:
v = queue.popleft()
for w in graph[v]:
reads += 1
if w not in seen:
seen.add(w)
piece.add(w)
queue.append(w)
found.append(sorted(piece))
return found, reads
EDGES = [("A", "B"), ("B", "C"), ("A", "C"), # a triangle
("D", "E"), # a pair
("F", "G"), ("G", "H"), ("H", "F"), ("H", "I")] # a triangle with a tail
VERTICES = "ABCDEFGHIJ" # J is isolated
GRAPH = adjacency(EDGES, VERTICES)
found, reads = components(GRAPH)
print("the graph has %d vertices and %d edges" % (len(GRAPH), len(EDGES)))
print("components found:", len(found))
for i, piece in enumerate(found, 1):
word = "vertex" if len(piece) == 1 else "vertices"
print(" component %d (%d %s): %s" % (i, len(piece), word, " ".join(piece)))
print()
print("every vertex is in exactly one component:",
sorted(v for piece in found for v in piece) == sorted(VERTICES))
print("the isolated vertex J is its own component:", ["J"] in found)
print("adjacency entries read in total:", reads, "= 2E for E =", len(EDGES))
print("so finding ALL components costs one traversal's work, O(V + E).")the graph has 10 vertices and 8 edges
components found: 4
component 1 (3 vertices): A B C
component 2 (2 vertices): D E
component 3 (4 vertices): F G H I
component 4 (1 vertex): J
every vertex is in exactly one component: True
the isolated vertex J is its own component: True
adjacency entries read in total: 16 = 2E for E = 8
so finding ALL components costs one traversal's work, O(V + E).Note the two results worth remembering: every vertex lands in exactly one component, and the isolated vertex counts. A question that gives a vertex with no edges is testing exactly that.
Are two vertices connected to each other
from collections import deque
def adjacency(pairs, vertices):
graph = {v: [] for v in vertices}
for a, b in pairs:
graph[a].append(b)
graph[b].append(a)
for v in graph:
graph[v].sort()
return graph
def label_components(graph):
"""Give every vertex its component number. One pass, then O(1) questions."""
label = {}
number = 0
for start in sorted(graph):
if start in label:
continue
label[start] = number
queue = deque([start])
while queue:
v = queue.popleft()
for w in graph[v]:
if w not in label:
label[w] = number
queue.append(w)
number += 1
return label, number
EDGES = [("A", "B"), ("B", "C"), ("A", "C"), ("D", "E"),
("F", "G"), ("G", "H"), ("H", "F"), ("H", "I")]
GRAPH = adjacency(EDGES, "ABCDEFGHIJ")
label, count = label_components(GRAPH)
print("component number of each vertex:")
print(" " + " ".join("%s:%d" % (v, label[v]) for v in sorted(label)))
print("components:", count)
print()
for a, b in (("A", "C"), ("A", "D"), ("F", "I"), ("J", "A")):
print(" %s and %s connected to each other: %s"
% (a, b, label[a] == label[b]))
print()
print("one O(V + E) pass makes every later question O(1).")Connectivity and Connected Components
component number of each vertex:
A:0 B:0 C:0 D:1 E:1 F:2 G:2 H:2 I:2 J:3
components: 4
A and C connected to each other: True
A and D connected to each other: False
F and I connected to each other: True
J and A connected to each other: False
one O(V + E) pass makes every later question O(1).This is the shape to use when the same graph is asked many questions: label once, answer instantly. The alternative, a fresh traversal per question, is O(V + E) every time.
Two facts worth quoting
A connected graph on V vertices has at least V-1 edges. So a graph with fewer than V-1 edges is certainly disconnected, without running anything.
Exactly V-1 edges and connected means it is a tree: connected with no cycle. That matches what chapter 88 observed from the other direction, that a tree has exactly one path between any two vertices. One more edge creates a cycle, and so a second path.
from collections import deque
def is_connected_count(n, edges):
graph = {i: [] for i in range(n)}
for a, b in edges:
graph[a].append(b)
graph[b].append(a)
seen = {0}
queue = deque([0])
while queue:
v = queue.popleft()
for w in graph[v]:
if w not in seen:
seen.add(w)
queue.append(w)
return len(seen) == n
n = 6
print("on %d vertices, the minimum for a connected graph is %d edges" % (n, n - 1))
print()
too_few = [(0, 1), (1, 2), (2, 3), (3, 4)] # 4 edges, 5 is the minimum
a_tree = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)] # exactly 5
one_more = a_tree + [(0, 5)] # 6 edges: a cycle appears
for name, edges in (("4 edges", too_few), ("5 edges, a path", a_tree),
("6 edges, the path closed", one_more)):
print(" %-26s connected: %-5s edges %d, V-1 = %d"
% (name, is_connected_count(n, edges), len(edges), n - 1))
print()
print("4 edges on 6 vertices cannot be connected, whatever the arrangement:",
len(too_few) < n - 1)
print("5 edges and connected means a tree, so no cycle:", len(a_tree) == n - 1)
print("adding the 6th edge closed a cycle and kept it connected.")Connectivity and Connected Components
on 6 vertices, the minimum for a connected graph is 5 edges
4 edges connected: False edges 4, V-1 = 5
5 edges, a path connected: True edges 5, V-1 = 5
6 edges, the path closed connected: True edges 6, V-1 = 5
4 edges on 6 vertices cannot be connected, whatever the arrangement: True
5 edges and connected means a tree, so no cycle: True
adding the 6th edge closed a cycle and kept it connected.Directed graphs: strongly and weakly connected
from collections import deque
def reachable(out_edges, start, keys):
seen = {start}
queue = deque([start])
while queue:
v = queue.popleft()
for w in out_edges.get(v, []):
if w not in seen:
seen.add(w)
queue.append(w)
return seen
def classify(vertices, arcs):
out = {v: [] for v in vertices}
both = {v: [] for v in vertices}
for a, b in arcs:
out[a].append(b)
both[a].append(b)
both[b].append(a)
strong = all(len(reachable(out, v, vertices)) == len(vertices)
for v in vertices)
weak = len(reachable(both, vertices[0], vertices)) == len(vertices)
if strong:
return "strongly connected"
if weak:
return "weakly connected"
return "disconnected"
cycle = (["A", "B", "C"], [("A", "B"), ("B", "C"), ("C", "A")])
chain = (["A", "B", "C"], [("A", "B"), ("B", "C")])
apart = (["A", "B", "C", "D"], [("A", "B"), ("C", "D")])
for name, (vs, arcs) in (("a directed cycle A->B->C->A", cycle),
("a chain A->B->C", chain),
("A->B and C->D", apart)):
print("%-30s %s" % (name, classify(vs, arcs)))
print()
print("in the chain, C cannot reach A, so it is not strongly connected,")
print("but ignoring the arrows it is one piece, so it is weakly connected.")a directed cycle A->B->C->A strongly connected
a chain A->B->C weakly connected
A->B and C->D disconnected
in the chain, C cannot reach A, so it is not strongly connected,
but ignoring the arrows it is one piece, so it is weakly connected.Where this is used
Networks. Can every machine reach every other? A disconnected network has a cut somewhere, and the components tell you where.
Social graphs. A component is a group with no link at all to the rest.
Image processing. Connected component labelling finds the separate shapes in a picture, treating each pixel as a vertex joined to its neighbours. It is this algorithm exactly.
Spreadsheets, builds and dependency graphs. Components separate work that shares nothing, so each component can be handled independently or in parallel.
Before any other graph work. Many algorithms assume a connected graph, so checking is a sensible first step.
Quick revision
- A graph is connected when one traversal from any vertex reaches all V vertices.
- A connected component is a maximal connected piece; maximal means it cannot be extended.
- An isolated vertex is a component of size one and must be counted.
- All components are found by starting a fresh traversal from each unvisited vertex, and the total cost is still O(V + E).
- Every vertex lies in exactly one component.
- Labelling components once, in O(V + E), makes every later "are these two connected" question O(1).
- A connected graph needs at least V-1 edges, so fewer than V-1 edges is disconnected without checking.
- Connected with exactly V-1 edges means a tree; one more edge makes a cycle.
- Directed: strongly connected means every vertex reaches every other along the arrows; weakly connected means it is one piece only when the arrows are ignored.
Connectivity and Connected Components
Test yourself
1. Define a connected component, and say why the word maximal matters. A maximal set of vertices that can all reach one another. Maximal matters because if another vertex of the graph could be added, it was reachable and so belonged to the component already.
2. How is connectivity decided, and at what cost? Run one traversal from any vertex and check that it reached all V vertices. The cost is the traversal's, O(V + E).
3. Why is finding all components still O(V + E) and not O(V) traversals? Because the outer loop starts a traversal only at an unvisited vertex, so across all of them each vertex is visited once and each edge read once.
4. Is a vertex with no edges a component? Yes, a component of size one, and it must be counted.
5. What is the fastest way to answer many "are u and v connected" questions on one fixed graph? Label every vertex with its component number in one O(V + E) pass; each later question is then an O(1) comparison of two labels.
6. A graph has 6 vertices and 4 edges. Can it be connected? No. A connected graph on V vertices needs at least V-1 edges, so 5 here, whatever the arrangement.
7. Distinguish strongly and weakly connected. Strongly connected: every vertex can reach every other following the arrow directions. Weakly connected: that fails, but the graph is one piece once the directions are ignored. A chain A to B to C is weakly connected, since C cannot reach A.
8. Name a real use of connected component labelling outside networks. Finding the separate shapes in an image, where each pixel is a vertex joined to its neighbours.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.