Graphs: Depth-First-Search

Imports and Setup

# python
from __future__ import annotations
from enum import Enum

# from pypi
from attrs import define

# this projects
from bowling.data_structures.graphs import graph

DFS Vertex

The Vertices used in the Depth-First Search also keep track of the "times" so it has two more attributes than our original vertex - discovered and finished. Although, as with color and all the other attributes, we could just let the program stick the attributes into the object, I thought it'd be better to create a new class to make it more obvious that there's a difference. I couldn't get the sets to add the vertices when I used inheritance so I'm going to start from scratch instead of inheriting from the original Vertex.

@define(eq=False)
class DFSVertex(graph.Vertex):
    """The Depth-First Search Vertex

    Args:
     identifier: something to identify the node
     color: the 'discovery' state of the node
     distance: The number of edges to the root
     predecessor: The 'parent' of the node in a tree
     discovered: when the node was discovered
     finished: when the node's adjacent nodes where checked
    """
    discovered: int = None
    finished: int = None

    def __str__(self) -> str:
        return (super().__str__() + 
        f", discovered: {self.discovered}, finished: {self.finished}")

Directed Graph

class DirectedGraph(graph.Graph):
    """A Directed Graph"""    

Add

def add(self, source: DFSVertex, target: DFSVertex) -> None:
    """Add an edge from source to target

    Args:
     source: the vertex where the edge starts
     target: the vertex where the edge ends
    """
    self.vertices.add(source)
    self.vertices.add(target)
    self.adjacent[source].add(target)
    return

Depth First Search

The Class

@define
class DepthFirstSearch:
    """depth-first-searcher

    Args:
     graph: the graph to process
    """
    graph: graph.Graph
    time: int=0

The Call

def __call__(self) -> None:
    """Performs the depth-first-search"""
    for vertex in self.graph.vertices:
        vertex.color = graph.Color.WHITE
        vertex.predecessor = None
    self.time = 0
    for vertex in self.graph.vertices:
        if vertex.color is graph.Color.WHITE:
            self.visit_adjacent(vertex)
    return

Visiting Adjacent Nodes

def visit_adjacent(self, vertex: DFSVertex) -> None:
    """Visit the nodes adjacent to the given node

    Args:
     vertex: vertex whose adjacency to visit
    """
    self.time += 1
    vertex.discovered = self.time
    vertex.color = graph.Color.GRAY
    for neighbor in self.graph.adjacent[vertex]:
        if neighbor.color is graph.Color.WHITE:
            neighbor.predecessor = vertex
            self.visit_adjacent(neighbor)
    vertex.color = graph.Color.BLACK
    self.time += 1
    vertex.finished = self.time
    return

Testing It

# pypi
from expects import be, equal, expect
# software under test
from bowling.data_structures.graphs import graph
from bowling.data_structures.graphs.depth_first_search import (
    DepthFirstSearch,
    DFSVertex,
    DirectedGraph
)

graph_1 = DirectedGraph()
node_u = DFSVertex(identifier="u")
node_v = DFSVertex(identifier="v")
node_w = DFSVertex(identifier="w")
node_x = DFSVertex(identifier="x")
node_y = DFSVertex(identifier="y")
node_z = DFSVertex(identifier="z")

graph_1.add(node_u, node_v)
graph_1.add(node_u, node_x)
graph_1.add(node_x, node_v)
graph_1.add(node_v, node_y)
graph_1.add(node_y, node_x)
graph_1.add(node_w, node_y)
graph_1.add(node_w, node_z)
graph_1.add(node_z, node_z)

searcher = DepthFirstSearch(graph=graph_1)
searcher()
for node in graph_1.vertices:
    expect(node.color).to(be(graph.Color.BLACK))

expect(node_u.discovered).to(equal(1))
expect(node_u.finished).to(equal(8))

expect(node_v.discovered).to(equal(2))
expect(node_v.finished).to(equal(7))

expect(node_w.discovered).to(equal(9))
expect(node_w.finished).to(equal(12))

expect(node_x.discovered).to(equal(4))
expect(node_x.finished).to(equal(5))

expect(node_y.discovered).to(equal(3))
expect(node_y.finished).to(equal(6))

expect(node_z.discovered).to(equal(10))
expect(node_z.finished).to(equal(11))

expect(node_u.predecessor).to(be(None))
expect(node_v.predecessor).to(be(node_u))
expect(node_y.predecessor).to(be(node_v))
expect(node_x.predecessor).to(be(node_y))
expect(node_z.predecessor).to(be(node_w))

expect(node_w.predecessor).to(be(None))