Breadth-First Search (BFS)

Similar to depth first search, it is a searching algorithm that takes a graph in input & searches it. However, the searching approach it follows is to search all the nodes at one level before moving down to the next level. This is unlike DFS, where nodes at the deeper levels are explored first.

DFS vs BFS traversal
DFS vs BFS comparison. In BFS, nodes at same level are explored first. But in DFS, nodes at lower levels are explored first.

Implementing BFS algorithm

BFS explores all nodes at the current depth level before moving deeper, making it a “layered” approach. BFS is great for finding the shortest path in an unweighted graph because it finds nodes level by level.

Algorithm’s Steps

  1. Create a set called visited to store all the nodes which are explored (initially empty).
  2. Create a queue and add the starting node to it.
  3. Loop over the queue, until it gets empty. In each loop cycle do:
    • De-queue the node from queue (remove node from queue).
    • Process the node (by default print or do nothing).
    • Add the node to visited.
    • Put all the node’s neighbors to queue for the successive cycles to process.
  4. When all the nodes will be explored in graph, queue will eventually get empty and algorithm will return.
from collections import deque

def bfs(graph, start_node):
    visited = set()
    queue = deque([start_node])

    while queue:
        node = queue.popleft() # remove from left side of queue
        if node not in visited:
            visited.add(node)
            print(node)  # Process the node (e.g., print it)

            for neighbor in graph[node]:
                queue.append(neighbor) # append to right-side of queue

# test graph:
graph = {
    'A': ['B', 'C'], # A node is connected to 'B' adn 'C'
    'B': ['D', 'E'], # B is connected to 'D' and 'E'
    'C': ['F'], # C is connected to 'F'
    'D': [], # D connecteed to nothing
    'E': ['F'], # ...
    'F': [] # ...
}

bfs(graph, 'A') # test the code

What is BFS used for?

  1. BFS can be used for finding shortest path in graph with no cost/weights. For weighted graphs, A-star algorithm or Dijkstra algorithm are better suited.
  2. It can be used for searching as its primary puirpose.
  3. It is better in cases if we know that the thing we are searching for is on the top levels of the graph (and not deeper).
  4. Can be used for maze game AI, similar to DFS.

For smaller graphs, there is no noticeable difference between DFS vs BFS performance.

Converting our simple maze game from DFS to BFS

I just replaced dfs() to bfs() in maze game code. And it now solves the maze problem using BFS algorithm. The code is written in Pygame library.

import pygame
import time

# Initialize Pygame
pygame.init()

# Constants
SCREEN_WIDTH = 500
SCREEN_HEIGHT = 500
CELL_SIZE = 100
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)

# Set up the display
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("DFS Maze Solver")

# Maze example
maze = [
    [0, 1, 0, 0, 0],
    [0, 1, 0, 1, 0],
    [0, 0, 0, 1, 0],
    [0, 1, 1, 1, 0],
    [0, 0, 0, 0, 0]
]

start = (0, 0)
end = (4, 4)

# Convert the maze to a graph representation
def maze_to_graph(maze):
    graph = {}
    rows, cols = len(maze), len(maze[0])
    for i in range(rows):
        for j in range(cols):
            if maze[i][j] == 0:
                graph[(i, j)] = []
                for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                    nx, ny = i + dx, j + dy
                    if 0 <= nx < rows and 0 <= ny < cols and maze[nx][ny] == 0:
                        graph[(i, j)].append((nx, ny))
    return graph


def bfs(graph, start):
    visited = set()
    queue = [start]
    path = []

    while queue:
        vertex = queue.pop(0)  # Pop from the front of the queue
        if vertex not in visited:
            visited.add(vertex)
            path.append(vertex)
            # Visualization
            screen.fill(WHITE)
            draw_maze(maze, path)
            pygame.display.flip()
            time.sleep(0.5)  # pause to visualize
            queue.extend(neighbor for neighbor in graph[vertex] if neighbor not in visited)

    return path

# Draw the maze
def draw_maze(maze, path):
    rows, cols = len(maze), len(maze[0])
    for i in range(rows):
        for j in range(cols):
            color = WHITE if maze[i][j] == 0 else BLACK
            pygame.draw.rect(screen, color, (j * CELL_SIZE, i * CELL_SIZE, CELL_SIZE, CELL_SIZE))
    for (x, y) in path:
        pygame.draw.rect(screen, RED, (y * CELL_SIZE, x * CELL_SIZE, CELL_SIZE, CELL_SIZE))
    pygame.draw.rect(screen, GREEN, (start[1] * CELL_SIZE, start[0] * CELL_SIZE, CELL_SIZE, CELL_SIZE))
    pygame.draw.rect(screen, GREEN, (end[1] * CELL_SIZE, end[0] * CELL_SIZE, CELL_SIZE, CELL_SIZE))

# Main function
def main():
    graph = maze_to_graph(maze)
    bfs(graph, start)
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
        screen.fill(WHITE)
        draw_maze(maze, [])
        pygame.display.flip()
    pygame.quit()

if __name__ == "__main__":
    main()

Thank you for reading <3

Leave a Reply

Your email address will not be published. Required fields are marked *