munotes®

Connectivity and Connected Components

Get access to whole semester resourcesSemester Pass

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 A

One 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

munotes.in312

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).")
munotes.in313

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.")
munotes.in314

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.
munotes.in315

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.

munotes.in316

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.

Issue
Done!