Breadth First Search (BFS) in Python

Tue 30 March 2021
import queue


class BreadthFirstSearch:
    def __init__(self, graph):
        """
        graph: Graph represntation in the form of an adjecency list.
            E.g:  `B <- A -> C`, graph = { 'A' : ['B','C'], }
        """
        self._graph = graph

    def head(self):
        # NOTE: In Python 3.7.0 the insertion-order preservation nature of
        # dict objects has been declared to be an official part of the
        # Python language spec.*
        return next(iter(self._graph.keys()))

    def traverse(self):
        """Traverse all nodes in graph and print the followed path."""

        visited = set()  # Keeps track of visited nodes
        fifo = queue.Queue()  # First in First Out Queue
        log = []

        node = self.head()
        visited.add(node)
        fifo.put(node)

        while not fifo.empty():
            node = fifo.get()
            log.append(node)

            for neighbour in self._graph[node]:
                if neighbour not in visited:
                    visited.add(neighbour)
                    fifo.put(neighbour)

        return "Traverse graph: " + " ".join(log)

    def find_shortest_path(self, target):
        """Find shortest path to `target` node."""

        fifo = queue.Queue()
        fifo.put([self.head()])
        visited = set()

        while not fifo.empty():
            path = fifo.get()
            node = path[-1]  # last node on the path

            # BFS has the extremely useful property that if all edges in a
            # graph are unweighted (or the same weight), then the first time
            # a target node is visited is the shortest path to that node
            # from the head/source node.
            if node == target:
                return f"Shortest path to target: {path}"

            # check if the current node is already in the visited nodes set in
            # order not to recheck it
            elif node not in visited:
                # enumerate all adjacent nodes, construct a new path and push
                # it into the queue
                for current_neighbour in self._graph.get(node, []):
                    new_path = list(path)
                    new_path.append(current_neighbour)
                    fifo.put(new_path)

                visited.add(node)

        return "Target node is not reachable from head node"


#     A
#   +-^---+
#   B     |
# +-^-+   |
# D   E   C
#     ^-+-^
#       F
graph = {
    'A' : ['B','C'],
    'B' : ['D', 'E'],
    'C' : ['F'],
    'D' : [],
    'E' : ['F'],
    'F' : []
}

bfs = BreadthFirstSearch(graph)

print(bfs.traverse())
>> Travers: A B C D E F

print(bfs.find_shortest_path("F"))
>> Shortest path to target: ['A', 'C', 'F']

Category: Snippets [Python]