__init__(V, E): Initialize a new graph with vertex set V and edge set E

vertices(): Return an iterable collection of the vertices

edges(): Return an iterable collection of the edges

addvertex(v): Add vertex to graph that is identified with the v object

addedge(u, v): Add edge to graph between vertices with keys u and v

removeedge(u,v) : Remove the edge u,v from the graph

__contains__(v) : Return True if vertex v in graph; False otherwise

hasedge(u,v) : Return True if edge (u,v) in graph; False otherwise.

nbrs(v) : Return an iterable collection of (out)neighbors of v, i.e., those vertices w such that (v, w) is an edge. (For directed graphs, this is out-neighbors.)

__len__() : Return the number of vertices in the graph.

Defining the EdgeSetGraph class

class EdgeSetGraph:def__init__(self, V = (), E = ()):self._V =set()self._E =set()for v in V: self.addvertex(v)for u,v in E: self.addedge(u,v)def vertices(self):returniter(self._V)def edges(self):returniter(self._E)def addvertex(self, v):self._V.add(v)def addedge(self, u, v):self._E.add((u,v))def removeedge(self, u, v):self._E.remove((u,v))def__contains__(self, v):return v inself._Vdef hasedge(self, u, v):return (u,v) inself._Edef nbrs(self, v):return (w for u,w inself._E if u == v)def__len__(self):returnlen(self._V)

Represents a graph using a set of edges between vertices

How to create an un-directed version of the EdgeSetGraph?

UndirectedEdgeSetGraph class

class UndirectedEdgeSetGraph(EdgeSetGraph):def addedge(self, u, v):self._E.add(frozenset({u,v}))def removeedge(self, u, v):self._E.remove(frozenset({u,v}))def nbrs(self, v):for u, w inself._E:if u == v:yield welif w == v:yield u

UnidirectedEdgeSetGraph subclass of EdgeSetGraph

Standard sets in Python do not support storing a set

The frozenset is an immutable version of the set

Problems with the EdgeSetGraph class

The EdgeSetGraph class is not efficient for large graphs

Very slow to enumerate all edges incident to a vertex!

Key insight: To find the vertices that are incident to a given vertex, it should not be necessary to go through all the edges! Alternatively, can we store a set of neighbors for each vertex?

Creating AdjacencySetGraph

class AdjacencySetGraph:def__init__(self, V = (), E = ()):self._V =set()self._nbrs = {}for v in V: self.addvertex(v)for e in E: self.addedge(*e)def vertices(self):returniter(self._V)def edges(self):for u inself._V:for v inself.nbrs(u):yield (u,v)def addvertex(self, v):self._V.add(v)self._nbrs[v] =set()def addedge(self, u, v):self._nbrs[u].add(v)def removeedge(self, u, v):self._nbrs[u].remove(v)def__contains__(self, v):return v inself._nbrsdef nbrs(self, v):returniter(self._nbrs[v])def__len__(self):returnlen(self._nbrs)

Now nbrs works with return iter(self._nbrs[v])

Using AdjacencySetGraph

G = AdjacencySetGraph({1,2,3}, {(1,2),(2,1),(1,3)})print("Neighbors of 1:", list(G.nbrs(1)))print("Neighbors of 2:", list(G.nbrs(2)))print("Neighbors of 3:", list(G.nbrs(3)))

Neighbors of 1: [2, 3]
Neighbors of 2: [1]
Neighbors of 3: []

Calling the nbrs method is now much faster!

Direct access to the neighbors of a vertex

Preserves same interface in the constructor

Does not explicitly store edges of the graph

How could we implement the un-directed version?

Finding cycles and paths in a graph

A path is a sequence of vertices connected by edges

A non-empty sequence of vertices \((v_0, v_1,\ldots, v_k)\) is a path from \(v_0\) to \(v_k\) as long as \((v_{i-1}, v_i)\in E\) for all \(i \in \{1,\ldots, k\}\).

A path is simple if it does not repeat any vertices

The length of a path is the number of edges in the path

Defining a cycle in a graph

A cycle is a path of length at least one that starts and ends at the same vertex. The cycle length is the number of edges.

A cycle is simple if it’s a cycle and removing last edge makes a simple path (i.e., no repeated vertices until the last one).

How could we implement these definitions in Python?

def iscycle(self, V):"""Return True if and only if the vertices V form a cycle."""returnself.ispath(V) and V[0] == V[-1]def issimplecycle(self, V):"""Return True if and only if the vertices V form a simple cycle."""returnself.iscycle(V) andself.issimplepath(V[:-1])

Enhanced AdjacencySetGraph

class AdjacencySetGraphWithPath:def__init__(self, V = (), E = ()):self._V =set()self._nbrs = {}for v in V: self.addvertex(v)for e in E: self.addedge(*e)def vertices(self):returniter(self._V)def edges(self):for u inself._V:for v inself.nbrs(u):yield (u,v)def addvertex(self, v):self._V.add(v)self._nbrs[v] =set()def addedge(self, u, v):self._nbrs[u].add(v)def removeedge(self, u, v):self._nbrs[u].remove(v)def__contains__(self, v):return v inself._nbrsdef nbrs(self, v):returniter(self._nbrs[v])def__len__(self):returnlen(self._nbrs)def hasedge(self, u, v):return v inself._nbrs[u]def ispath(self, V):return V andall(self.hasedge(V[i-1], V[i]) for i inrange(1, len(V)))def issimplepath(self, V):returnself.ispath(V) andlen(V) ==len(set(V))def iscycle(self, V):returnself.ispath(V) and V[0] == V[-1]def issimplecycle(self, V):returnself.iscycle(V) andself.issimplepath(V[:-1])

Add ispath, issimplepath, iscycle, and issimplecycle

Using AdjacencySetGraphWithPath

G = AdjacencySetGraphWithPath({1,2,3,4}, {(1,2),(3,1), (2,3), (3,4), (4,3)})print("[1,2,3,1] is a path", G.ispath([1,2,3,1]))print("[1,2,3,1] is a simple path", G.issimplepath([1,2,3,1]))print("[1,2,3] is a simple path", G.issimplepath([1,2,3]))print("[1,2,3] is a simple cycle:", G.issimplecycle([1,2,3]))print("[1,2,3,1] is a simple cycle:", G.issimplecycle([1,2,3]))print("[1,2,3,4] is a simple path:", G.issimplepath([1,2,3,4]))print("[1,2,3,4] is a simple cycle:", G.issimplecycle([1,2,3,4]))print("[1,2,3,4,3,1] is a cycle:", G.iscycle([1,2,3,4,3,1]))print("[1,2,3,4,3,1] is a simple cycle:", G.issimplecycle([1,2,3,4,3,1]))

[1,2,3,1] is a path True
[1,2,3,1] is a simple path False
[1,2,3] is a simple path True
[1,2,3] is a simple cycle: False
[1,2,3,1] is a simple cycle: False
[1,2,3,4] is a simple path: True
[1,2,3,4] is a simple cycle: False
[1,2,3,4,3,1] is a cycle: True
[1,2,3,4,3,1] is a simple cycle: False

Traversing a graph

After defining a graph, we can search its nodes for data

However, we have to be super careful to avoid infinite loops!

Why is searching a Tree easier than searching a Graph?

def printall(G, v):print(v)for n in G.nbrs(v): printall(G, n)

printall might yield RecursionError for certain graphs G

Can we implement graph search without this defect?

Traversing a graph with a set

def printall(G, v, visited): visited.add(v)print(v)for n in G.nbrs(v):if n notin visited: printall(G, n, visited)G = AdjacencySetGraphWithPath({1,2,3,4}, {(1,2), (2,3), (3,4), (4,1)})printall(G, 1, set())

1
2
3
4

Intuitively, this approach leaves “bread crumbs” for tracking

Traversal only proceeds to vertices not visited before

Wait, does this really print all the nodes?

Depth-first search (DFS)

def dfs(G, v): visited = {v} _dfs(G, v, visited)return visiteddef _dfs(G, v, visited):for n in G.nbrs(v):if n notin visited: visited.add(n) _dfs(G, n, visited)G = AdjacencySetGraphWithPath({1,2,3,4}, {(1,2), (2,3), (3,4), (4,2)})print('reachable from 1:', dfs(G, 1))print('reachable from 2:', dfs(G, 2))

reachable from 1: {1, 2, 3, 4}
reachable from 2: {2, 3, 4}

Graph reachability is a key concept in graph theory

A depth-first search can be used to find the reachable nodes

Determining graph connectivity

def connected(G, u, v):return v in dfs(G, u)G = AdjacencySetGraphWithPath({1,2,3,4}, {(1,2), (2,3), (3,4), (4,2)})print("1 is connected to 4:", connected(G, 1, 4))print("4 is connected to 3:", connected(G, 4, 3))print("4 is connected to 1:", connected(G, 4, 1))

1 is connected to 4: True
4 is connected to 3: True
4 is connected to 1: False

connected uses the dfs function to determine connectivity

connected returns True if there is a path between u and v

Otherwise, it returns False as there is no connection

Useful in a wide variety of graph theoretic algorithms!

Additional avenues for exploring the Graph

Develop a non-recursion-based traversal algorithm

Implement a breadth-first search (BFS) algorithm

Handle weighted edges in the Graph data structure

Find the shortest path between two vertices in the Graph

Generalizing a Tree, a Graph can model real-world data!