Breadth First Search (BFS) for Artificial Intelligence

In artificial intelligence, the Breadth-First Search (BFS) algorithm is an essential tool for exploring and navigating various problem spaces. By systematically traversing graph or tree structures, BFS solves tasks such as pathfinding, network routing, and puzzle solving. This article probes into the core concepts of BFS, its algorithms, and practical applications in AI.

Table of Content

  • What is Breadth-First Search?
  • Key characteristics of BFS
  • Breadth First Search (BFS) Algorithms
  • How Breadth-First Search can help in Robot Pathfinding
  • Practical Implementations of BFS in Pathfinding of Robots
  • Conclusion
  • FAQs on Breadth First Search (BFS) for Artificial Intelligence

What is Breadth-First Search?

The Breadth-First Search is a traversing algorithm used to satisfy a given property by searching the tree or graph data structure. It belongs to uninformed or blind search AI algorithms as It operates solely based on the connectivity of nodes and doesn’t prioritize any particular path over another based on heuristic knowledge or domain-specific information. it doesn’t incorporate any additional information beyond the structure of the search space. It is optimal for unweighted graphs and is particularly suitable when all actions have the same cost. Due to its systematic search strategy, BFS can efficiently explore even infinite state spaces. The graph structure of BFS allows to work as follows:

  • Originally it starts at the root node, then it expands all of its successors, it systematically explores all its neighbouring nodes before moving to the next level of nodes. ( As shown in the above image, It starts from the root node A then expands its successors B)
  • This process of extending the root node’s immediate neighbours, then to their neighbours, and so on, lasts until all the nodes within the graph have been visited or until the specific condition is met. From the above image we can observe that after visiting the node B it moves to node C. when the level 1 is completed, it further moves to the next level i.e 2 and explore node D. it will move systematically to node E, node F and node G. After visiting the node G it will terminate.

Key characteristics of BFS

  1. First-in-First-Out (FIFO): The FIFO queue is typically preferred in BFS because the FIFO queue will be faster than a priority queue and often results in the correct order of nodes. When FIFO is applied to BFS, new nodes deeper in the search tree are added to the back of the queue. The old nodes which are swallower than the new nodes get expanded first.
  2. Early goal test: In traditional BFS implementations, the algorithm maintains a set of states reached during the search process. However, instead of storing all reached states, it selectively stores only the set of reached states that allow for an early goal test. This test involves checking whether the newly generated node meets the goal criteria as soon as it is generated.
  3. Cost-optimal: BFS always aims to find a solution with a minimum cost prioritizing the shortest path, when BFS generates nodes at a certain depth d, it has already explored and generated all the nodes at the previous depth d-1. Consequently, if a solution exists within a search space, BFS can discover it as soon as it reaches that depth level. Therefore, BFS is said to be a cost-optimal solution.

Note: Triangular marker indicates the next node to be expanded at each stage.

Breadth First Search (BFS) Algorithms

Breadth First Search (BFS) Pseudocode

The below pseudocode outlines the Breadth-First Search algorithm, here the problem represents the pathfinding problem.

BREATH-FIRST-SEARCH(problem) returns a solution node or failure
node ← NODE(problem.INITIAL)
if problem.IS-GOAL(node.STATE) then return node

frontier ← a FIFO queue, initialized with node
reached ← {problem.INITIAL}

while not IS-EMPTY(frontier) do
node ← POP(frontier)

for each child in EXPAND(problem, node) do
s ← child.STATE

if problem.IS-GOAL(s) then return child

if s is not in reached then
add s to reached
add child to frontier
return failure

BFS Algorithms Implementation using Python

The breadth_first_search function is responsible for implementing the BFS algorithm for pathfinding. As we have seen earlier, the BFS algorithm starts with the initial state and then it visits the neighboring states in a breadth-first manner until the goal state is found or all reachable states are explored. It maintains the FIFO queue called frontier which stores the nodes that are visited. It also uses the set called reached to keep track of the visited states and avoid revisiting them.

Python
def breadth_first_search(problem):
    node = Node(problem.initial)
    if problem.is_goal(node.state):
        return node

    frontier = deque([node])
    reached = {problem.initial}

    while frontier:
        node = frontier.popleft()

        for child in problem.expand(node):
            s = child.state

            if problem.is_goal(s):
                return child
            if s not in reached:
                reached.add(s)
                frontier.append(child)
    return None

How Breadth-First Search can help in Robot Pathfinding

The Breadth-first-search is a method of uninformed search, so our example’s robot does not use any information about how far our robot has traveled from its initial state nor how far the robot is from the goal. Using BFS we begin by positioning the robot at the root node, now it starts looking for the goal by expanding all of neighbor nodes (successors) of the root node.. Here, successors of a node are said to hold all the allowable directions that the robot could travel next. Nodes that are already visited by the robots are not considered as successors. Take a look at the below picture, the robot can travel 8 possible directions.

The initial step in the Breath-first-search algorithm is to begin with the starting node. In this case, the ‘R’ serves as a starting node, so we visit node R first. Additionally, we add the visited node to the FIFO queue. By adding node ‘R’ to the queue, we ensure it is explored in the subsequent steps of the algorithm.

Initial node is visited first

We dequeue node R from the FIFO queue to expand it. Upon expansion, we discover a successor node labeled S, representing the direction south. Since node S has been visited, it is now enqueued in the FIFO queue for further exploration.

Node R is expanded

Similarly, we dequeue the S from the FIFO queue to expand it. We gain two more successor nodes that are labeled as S and E representing the direction of South and South east. These directions are the paths that our robot tends to traverse next. The visited nodes such as S and E are enqueued in the FIFO queue to avoid revisiting them.


Again S is dequeued from the queue and expanded by gaining the other two successors such as S and E. It is also enqueued to the FIFO queue.

Here, we dequeue the node E which leads to the expansion of two more nodes called S and N representing East and North East, respectively. As in the previous steps, we enqueue the newly visited nodes for further exploration.

The FIFO queue continues until the goal is found. Once the goal is discovered, the path that leads to a goal node is traced back up the tree so that it maps out the directions for the robot to reach the goal. In our example, let’s say that the goal node E was found when node SE was expanded. By traversing back up to the tree we can easily trace the path that the robot needs to follow. In this case, the robot needs to begin from the south then move to the southeast, and then finally head to the east to reach the goal.

Practical Implementations of BFS in Pathfinding of Robots

Consider the below example, depicting the scenario where a robot seeks to navigate to its goal. Initially, the robot expands its successors that evaluate the possible moves to progress towards its destination. It is essential to note the presence of obstacles that may impede the robot’s path as it traverses through the nodes. Let’s now understand the concept of pathfinding stepwise.

Step 1: Create a Node a class for the search tree

The Node class represents a node in a search tree that contains the states or grid coordinates of the robots.

Python
class Node:
    def __init__(self, state, parent=None, action=None):
        self.state = state
        self.parent = parent
        self.action = action

Step 2: Create a class for GridProblem and Node

The GridProblem class represents the problem of pathfinding on a grid. It contains attributes for the initial state, goal state, and obstacles on the grid. The three typical methods are fitted in the code such as

  1. is_goal – This method checks if the given state is the goal state.
  2. is_valid_cell – This functions checks that a cell is valid to move or obstacles
  3. expand – This method is in charge of generating child nodes (states) from the given parent node by considering obstacles and grid boundaries.
Python
from collections import deque

class GridProblem:
    def __init__(self, initial_state, goal_state, grid):
        self.initial_state = initial_state
        self.goal_state = goal_state
        self.grid = grid

    def is_goal(self, state):
        return state == self.goal_state

    def is_valid_cell(self, row, col):
        return 0 <= row < len(self.grid) and 0 <= col < len(self.grid[0]) and self.grid[col][row] == 0

    def expand(self, node):
        row, col = node.state
        children = []
        for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
            new_row, new_col = row + dr, col + dc
            if self.is_valid_cell(new_row, new_col):
                child_state = (new_row, new_col)
                child_node = Node(child_state, parent=node)
                children.append(child_node)
        return children

Step 3: Function to reconstruct the Solutions path

Define the function to reconstruct the solution path and print the path

Python
def reconstruct_path(node):
    path = []
    while node:
        path.append(node.state)
        node = node.parent
    return list(reversed(path))


# Function to print the complete path
def print_complete_path(path):
    for step, point in enumerate(path):
        print("Step {}: {}".format(step, point))

Step 4: Define the Traversal Algorithms

This the most important parts of the solutions. We we use the same breadth_first_search algorithm function which is defined above.

Complete Implementations of BFS in Pathfinding of Robots

In this example, we initialize the initial state such as (0,0) and goal state as (0,6) and obstacles on the grid. It creates a GridProblem instance with these parameters and when it calls the BREATH_FIRST_SEARCH function it finds the solution for the problem instance. If it can’t find the solution for the problem instance it returns No solution found.

Grid with Obstacles

Robot (0,0)

Block

(2,0)

(3,0)

Block

(5,0)

Goal

(0,1)

Block

(2,1)

(3,1)

Block

(5,1)

(6,1)

(0,2)

(1,2)

(2,2)

(3,2)

Block

(5,2)

(6,2)

(0,3)

(1,3)

Block

(3,4)

Block

(5,3)

(6,3)

(0,4)

(1,4)

Block

(3,5)

(4,4)

(5,4)

(6,4)

(0,5)

(1,5)

Block

(3,6)

(4,5)

(5,5)

(6,5)

(0,6)

(1,6)

Block

(3,6)

(4,6)

(5,6)

(6,6)

Complete code

Python
from collections import deque


class GridProblem:
    def __init__(self, initial_state, goal_state, grid):
        # Initializes a grid problem instance with initial and goal states, and the grid layout
        self.initial_state = initial_state
        self.goal_state = goal_state
        self.grid = grid

    def is_goal(self, state):
        # Checks if the given state is the goal state
        return state == self.goal_state

    def is_valid_cell(self, row, col):
        # Checks if the given cell coordinates are within the grid boundaries and not blocked
        return 0 <= row < len(self.grid) and 0 <= col < len(self.grid[0]) and self.grid[col][row] == 0

    def expand(self, node):
        # Expands the given node by generating child nodes for valid adjacent cells
        row, col = node.state
        children = []
        for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
            new_row, new_col = row + dr, col + dc
            if self.is_valid_cell(new_row, new_col):
                child_state = (new_row, new_col)
                child_node = Node(child_state, parent=node)
                children.append(child_node)
        return children


class Node:
    def __init__(self, state, parent=None, action=None):
        # Initializes a node with a state, parent node (optional), and action (optional)
        self.state = state
        self.parent = parent
        self.action = action


def breadth_first_search(problem):
    # Performs breadth-first search algorithm to find a solution for the given problem
    node = Node(problem.initial_state)
    if problem.is_goal(node.state):
        return node

    frontier = deque([node])
    reached = {problem.initial_state}

    while frontier:
        node = frontier.popleft()

        for child in problem.expand(node):
            state = child.state

            if problem.is_goal(state):
                return child
            if state not in reached:
                reached.add(state)
                frontier.append(child)
    return None


def reconstruct_path(node):
    # Reconstructs the path from the goal node back to the initial node
    path = []
    while node:
        path.append(node.state)
        node = node.parent
    return list(reversed(path))


def print_complete_path(path):
    # Prints the complete path from start to goal
    if path:
        for step, point in enumerate(path):
            print("Step {}: {}".format(step, point))
    else:
        print("No solution found")


# Example usage and grid definition
"""
    1 : Denotes the obstacles
    0 : Empty space or a non-obstacle cell in the grid
"""
grid = [
    [0, 1, 0, 0, 1, 0, 0],
    [0, 1, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 1, 0, 0],
    [0, 0, 1, 0, 1, 0, 0],
    [0, 0, 1, 0, 0, 0, 0],
    [0, 0, 1, 0, 0, 0, 0],
    [0, 0, 1, 0, 0, 0, 0]
]

# Define initial and goal states
initial_state = (0, 0)
goal_state = (6, 0)

# Define the problem instance
problem = GridProblem(initial_state, goal_state, grid)

# Perform breadth-first search to find a solution
solution_node = breadth_first_search(problem)

# Print solution if found
print('!! Reached the Goal!!' if solution_node else None)
if solution_node:
    print("Solution found!")
    solution_path = reconstruct_path(solution_node)
    print("Complete Path:")
    print_complete_path(solution_path)
else:
    print("No solution found")

Output:

!! Reached the Goal!!
Solution found!
Complete Path:
Step 0: (0, 0)
Step 1: (0, 1)
Step 2: (0, 2)
Step 3: (1, 2)
Step 4: (2, 2)
Step 5: (3, 2)
Step 6: (3, 3)
Step 7: (3, 4)
Step 8: (4, 4)
Step 9: (5, 4)
Step 10: (6, 4)
Step 11: (6, 3)
Step 12: (6, 2)
Step 13: (6, 1)
Step 14: (6, 0)

Output explanation

  1. We first start at the initial position (0,0), it is where the robot begins its exploration.
  2. The robot moves south along below row such as (0,1), and (0,2).
  3. Now, the robot moves east along the column to the position of (1,2).
  4. The robot follows a path to the east (2,2), and (3,2), then south (3,3), and again (3,4).
  5. Again, the robot moves east to towards (4,4), (5,4) and (6,4).
  6. Now the robot moves towards the North along row from (6,4) to (6,3),(6,2),(6,1) and (6,0).
  7. Finally, the robot reaches the goal node i.e (6,0).


The output indicates that the Breadth_First_Search algorithm successfully found a solution to the pathfinding problem. In this case, the goal state is (6,0) that represents the destination that the robot needs to reachPractical use cases in AI.

Conclusion

In conclusion, the Breadth-first-search algorithm stands as a fundamental tool in the artificial intelligence which offers systematic exploration and navigation of diverse problem spaces. Through its methodical traversal of graphs and tree structures, BFS facilitates tasks such as pathfinding, network routing and puzzle solving that contributes to the AI applications significantly.

FAQs on Breadth First Search (BFS) for Artificial Intelligence

Q. What is the difference between Breadth-First Search (BFS) and Depth-First Search (DFS)?

The main difference lies in their traversal strategies. BFS’s data structure is based on a queue that explores adjacent nodes, if it has the same costs and also it can be used to find the shortest path efficiently. On the other hand, DFS is based on stack data structure, similar to BFS it also begins at the root node but it traverses through the nodes as far as possible until it reaches the no unvisited adjacent nodes.

Q. Is Breadth-First Search suitable for solving weighted graphs?

No, BFS is optimal only for unweighted graphs where all the edges in the graphs have the same costs. To solve weighted graphs we can use algorithms like Dijkstra’s or A* (A-star) search.

Q. Can the Breadth-First search be used for finding the shortest path in a maze?

Yes, BFS can be used to obtain the shortest path in the maze. The maze can be represented in a graph structure that is comprised of two core components such as nodes and edges. The BFS uses unweighted graphs that provide the flexibility to search through the nodes and determine the shortest path for the maze.

Q. What is the significance of the FIFO queue in Breadth-First search?

The FIFO queue ensures that nodes are traversed in the order they are discovered that effective leads to find the shortest path first. This queue structure maintains the breadth-wise traversal characteristics of BFS that makes it efficiency for finding the shortest path in an unweighted graphs.

Q. Can Breadth-First search be applied to infinite state spaces?

Yes, BFS can be applied to infinite state spaces, although it may not terminate if there is no solution or if the goal state is unreachable. In such cases, it is common to implement BFS with the depth limitations or to use heuristics to guide the search towards promising areas of the state space.




Contact Us