Lecture Notes on 13 Nov 2023 Graphs * A graph is a network of vertices and edges. * In 1736 Leonhard Euler presented the solution to the Bridges of Konigsberg (now Kaliningard) problem - this was the beginning of Graph Theory https://www.cs.utexas.edu/users/mitra/csFall2023/cs313/misc/Konigsberg_Bridges.png * Types of Graphs - (un) weighted, (un) directed - (dis) connected - (in) complete - (a) cyclic - bipartite * Degree of a vertex - number of edges that are incident on the vertex * Eulerian path / cycle (circuit) - visit every edge just once * Hamiltonian path / cycle (circuit) - visit every vertex exactly once * Represent a graph - adjacency matrix - adjacency lists (linked lists) https://www.cs.utexas.edu/users/mitra/csFall2023/cs313/notes/Graph_Representation.pdf * Graph Exercise: - Download the map of the towns in Virginia https://www.cs.utexas.edu/users/mitra/csFall2023/cs313/notes/Virginia.pdf - Download the spreadsheet and create the adjacency matrix https://www.cs.utexas.edu/users/mitra/csFall2023/cs313/notes/Virginia_Towns.xlsx https://www.cs.utexas.edu/users/mitra/csFall2023/cs313/notes/Virginia_Towns.pdf - Create the adjacency list for the towns in Virginia * Definitions - bipartite and isomorphism https://www.cs.utexas.edu/users/mitra/csFall2023/cs313/notes/Graph_Terms.pdf https://www.cs.utexas.edu/users/mitra/csFall2023/cs313/notes/Graph_Images.pdf * Graph Traversals * Depth First Search (DFS) 0. Create a Stack. 1. Select a starting vertex. Make it the current vertex. Mark it visited. Push it on the stack. 2. If possible, visit an adjacent unvisited vertex from the current vertex in order. Make it the current vertez. Mark it visited, and push it on the stack. 3. If you cannot follow step 2, then if possible pop a vertex from the stack. Make it the current vertex. 4. Repeat steps 2 and 3 until the stack is empty. * Breadth First Search (BFS) 0. Create a Queue. 1. Select a starting vertex. Make it the current vertex. Mark it visited. 2. Visit an adjacent unvisited vertex (if there is one) in order from the current vertex. Mark it visited and insert it into the queue. 3. If you cannot carry out step 2 because there are no more unvisited vertices, remove a vertex from the queue (if possible) and make it the current vertex. 4. Repeat steps 2 and 3 until the queue is empty. * Graph Implementation - Download the map of cities in the US https://www.cs.utexas.edu/users/mitra/csFall2020/cs313/notes/US_Cities.pdf - Download the textual representation of this graph https://www.cs.utexas.edu/users/mitra/csFall2020/cs313/notes/graph.txt * Code for Graph import sys class Stack (object): def __init__ (self): self.stack = [] # add an item to the top of the stack def push (self, item): self.stack.append (item) # remove an item from the top of the stack def pop (self): return self.stack.pop() # check the item on the top of the stack def peek (self): return self.stack[-1] # check if the stack if empty def is_empty (self): return (len (self.stack) == 0) # return the number of elements in the stack def size (self): return (len (self.stack)) class Queue (object): def __init__ (self): self.queue = [] # add an item to the end of the queue def enqueue (self, item): self.queue.append (item) # remove an item from the beginning of the queue def dequeue (self): return (self.queue.pop(0)) # check if the queue is empty def is_empty (self): return (len (self.queue) == 0) # return the size of the queue def size (self): return (len (self.queue)) class Vertex (object): def __init__ (self, label): self.label = label self.visited = False # determine if a vertex was visited def was_visited (self): return self.visited # determine the label of the vertex def get_label (self): return self.label # string representation of the vertex def __str__ (self): return str (self.label) class Graph (object): def __init__ (self): self.Vertices = [] self.adjMat = [] # check if a vertex is already in the graph def has_vertex (self, label): nVert = len (self.Vertices) for i in range (nVert): if (label == (self.Vertices[i]).get_label()): return True return False # given the label get the index of a vertex def get_index (self, label): nVert = len (self.Vertices) for i in range (nVert): if (label == (self.Vertices[i]).get_label()): return i return -1 # add a Vertex with a given label to the graph def add_vertex (self, label): if (self.has_vertex (label)): return # add vertex to the list of vertices self.Vertices.append (Vertex (label)) # add a new column in the adjacency matrix nVert = len (self.Vertices) for i in range (nVert - 1): (self.adjMat[i]).append (0) # add a new row for the new vertex new_row = [] for i in range (nVert): new_row.append (0) self.adjMat.append (new_row) # add weighted directed edge to graph def add_directed_edge (self, start, finish, weight = 1): self.adjMat[start][finish] = weight # add weighted undirected edge to graph def add_undirected_edge (self, start, finish, weight = 1): self.adjMat[start][finish] = weight self.adjMat[finish][start] = weight # return an unvisited vertex adjacent to vertex v (index) def get_adj_unvisited_vertex (self, v): nVert = len (self.Vertices) for i in range (nVert): if (self.adjMat[v][i] > 0) and (not (self.Vertices[i]).was_visited()): return i return -1 # do a depth first search in a graph def dfs (self, v): # create the Stack theStack = Stack () # mark the vertex v as visited and push it on the Stack (self.Vertices[v]).visited = True print (self.Vertices[v]) theStack.push (v) # visit all the other vertices according to depth while (not theStack.is_empty()): # get an adjacent unvisited vertex u = self.get_adj_unvisited_vertex (theStack.peek()) if (u == -1): u = theStack.pop() else: (self.Vertices[u]).visited = True print (self.Vertices[u]) theStack.push (u) # the stack is empty, let us rest the flags nVert = len (self.Vertices) for i in range (nVert): (self.Vertices[i]).visited = False # do the breadth first search in a graph def bfs (self, v): return def main(): # create the Graph object cities = Graph() # read the number of vertices line = sys.stdin.readline() line = line.strip() num_vertices = int (line) # read the vertices to the list of Vertices for i in range (num_vertices): line = sys.stdin.readline() city = line.strip() print (city) cities.add_vertex (city) # read the number of edges line = sys.stdin.readline() line = line.strip() num_edges = int (line) print (num_edges) # read each edge and place it in the adjacency matrix for i in range (num_edges): line = sys.stdin.readline() edge = line.strip() print (edge) edge = edge.split() start = int (edge[0]) finish = int (edge[1]) weight = int (edge[2]) cities.add_directed_edge (start, finish, weight) # print the adjacency matrix print ("\nAdjacency Matrix") for i in range (num_vertices): for j in range (num_vertices): print (cities.adjMat[i][j], end = " ") print () print () # read the starting vertex for dfs and bfs line = sys.stdin.readline() start_vertex = line.strip() print (start_vertex) # get the index of the starting vertex start_index = cities.get_index (start_vertex) print (start_index) # do the depth first search print ("\nDepth First Search from " + start_vertex) cities.dfs (start_index) print () if __name__ == "__main__": main()