Depth First Search (DFS) for Artificial Intelligence

Depth-first search contributes to its effectiveness and optimization in artificial intelligence. From algorithmic insights to real-world implementations, DFS plays a huge role in optimizing AI systems. Let’s dive into the fundamentals of DFS, its significance in artificial intelligence, and its practical applications.

Table of Content

  • What is a Depth-First Search in AI?
  • Edge classes in a Depth-first search tree based on a spanning tree:
  • Depth First Search(DFS) Algorithm
  • DFS Behavior Across Different State Space Structures
  • DFS Implementation in Robotics Pathfinding
  • Applications of DFS in AI
  • Conclusion
  • FAQs on Depth First Search(DFS) for Artificial Intelligence

What is a Depth-First Search in AI?

Depth-first search is a traversing algorithm used in tree and graph-like data structures. It generally starts by exploring the deepest node in the frontier. Starting at the root node, the algorithm proceeds to search to the deepest level of the search tree until nodes with no successors are reached. Suppose the node with unexpanded successors is encountered then the search backtracks to the next deepest node to explore alternative paths.

Search operation of the depth-first search

Depth-first search (DFS) explores a graph by selecting a path and traversing it as deeply as possible before backtracking.

  • Originally it starts at the root node, then it expands all of its one branch until it reaches a dead end, then backtracks to the most recent unexplored node, repeating until all nodes are visited or a specific condition is met. ( As shown in the above image, starting from node A, DFS explores its successor B, then proceeds to its descendants until reaching a dead end at node D. It then backtracks to node B and explores its remaining successors i.e E. )
  • This systematic exploration continues until all nodes are visited or the search terminates. (In our case after exploring all the nodes of B. DFS explores the right side node i.e C then F and and then G. After exploring the node G. All the nodes are visited. It will terminate.

Key characteristics of DFS

In simple terms, the DFS algorithms in AI holds the power of extending the current path as deeply as possible before considering the other options.

  • DFS is not cost-optimal since it doesn’t guarantee to find the shortest paths.
  • DFS uses the simple principle to keep track of visited nodes: It uses a stack to keep track of nodes that have been visited so far which helps in the backtracking of the graph. When the DFS encounters a new node, it adds it to the stack to explore its neighbours. If it reaches a node with no successors (leaf node), it works by backtracking such as popping nodes off the stack to explore the alternative paths.
  • Backtracking search: The variant of DFS is called backtracking search which uses less memory than traditional depth-first search. Rather than generating all the successors, the backtracking search enables the DFS to generate only one successor at a time. This approach allows dynamic state modification, such as generating successors by directly modifying the current state description instead of allocating the memory to a brand-new state. Thus reducing the memory requirements to store one state description and path of actions.

Edge classes in a Depth-first search tree based on a spanning tree

The edges of the depth-first search tree can be divided into four classes based on the spanning tree, they are

  • Forward edges: The forward edge is responsible for pointing from a node of the tree to one of its successors.
  • Back edges: The back edge holds the power of directing its edge from a node of the tree to one of its ancestors.
  • Tree edges: When DFS explores the new vertex from the current vertex, the edge connecting them is called a tree edge. It is essential while constructing a DFS spanning tree because it represents the paths to be followed during the traversal.
  • Cross edges: Cross edges are the edges that connect two vertices that are neither ancestors nor descendants of each other in the DFS tree.

Edge classes in DFS based on spanning tree

Depth First Search(DFS) Algorithm

Take a look at the below Pseudocode which explains the working of DFS

function findPath(robot, start, goal):
stack ← empty stack
visited ← empty set
stack.push(start)

while not stack.isEmpty( ):
current ← stack.pop( )
if current == goal:
return "Path found"

visited.add(current)
neighbors ← robot.getNeighbors(current)

for neighbor in neighbors:
if neighbor not in visted:
stack.push(neighbor)
return "Path not found"

Step wise DFS Pseudocode Explanatioin

  1. Initialize an empty stack and an empty set for visited vertices.
  2. Push the start vertex onto the stack.
  3. While the stack is not empty:
    • Pop the current vertex.
    • If it’s the goal vertex, return “Path found”.
    • Add the current vertex to the visited set.
    • Get the neighbors of the current vertex.
    • For each neighbor not visited, push it onto the stack.
  4. If the loop completes without finding the goal, return “Path not found”.

Time complexity of DFS:

  • Explicit Time Complexity: This time complexity arises because DFS traverses each vertex and edge exactly once in the worst-case scenario. This complexity is for explicit traversing of DFS without any repetition. The time complexity of DFS is commonly represented as
         O(|V| + |E|)

Where,

  • V- represents the number of vertices,
  • E – represents the number of edges.
  • Implicit Time Complexity: This implicit time complexity is appropriate while considering the time complexity in terms of the number of nodes visited in the tree instead of the number of edges and vertices in a graph. For implicit traversing of DFS can be simply depicted as,
           O(bd)

where,

  • b – represents the branching factor,
  • d – represents the depth of the search.

Space complexity of DFS:

          O(|V|)

Where,

  • V – represents the number of vertices.

This is because DFS typically uses additional data structures like stack or recursion stack to keep track of visited vertices.

  • Implicit Space Complexity: For an implicit search in a graph, DFS’s space complexity can be represented as follows:
           O(bd)

where,

  • b – represents the branching factor,
  • d – represents the depth of the search.

This space complexity is said to be considered when the space is required to store the nodes on the current path during the search.

DFS Behavior Across Different State Space Structures

  • Finite state spaces that are trees: In this scenario, DFS is efficient and complete. It explore all possible states without revisiting any state which ensures that it systematically covers the entire state space.
  • Infinite state spaces: Since DFS is not a systematic one in the infinite state space, it can typically get stuck while traversing down an infinite path even if there are no cycles. Thus lack of systematic exploration renders DFS incomplete for infinite state spaces, as it may not cover the entire space or it may never terminate. Such that makes the Depth-first search incomplete.
  • Cyclic state spaces: DFS can stuck in an infinite loop when dealing with the cyclic state spaces. To address this issue, some implementations of DFS incorporate a cycle-checking mechanism to prevent revisiting states and entering an infinite loop.
  • Acyclic state spaces: Even though it is capable of exploring the entire state spaces, the acyclic state spaces may lead to expanding the same state many times via different paths.

DFS Implementation in Robotics Pathfinding

DFS can be used to find a path from a start node to a goal node in a maze or grid-based environment. The DFS algorithm systematically explores all possible paths from the start node, one branch at a time until it finds a path that leads to the goal. The below figures show the maze which contains the initial state of the robot, the obstacles, and the goal state. Here, the goal node is in the position of (0,5) where the robot needs to traverse through the maze to find the path to reach the goal.

Step 1: Define Maze dimensions and obstacles

We define a maze_size representing the dimension of the maze, in our case, it’s a 6×6 grid. A list of coordinates that represents the positions of obstacles within a maze is also specified. The robot is positioned at the initial state (0,0) and aims to reach the goal state (0,5).

Maze environment

Maze dimensions and obstacles

Python
# Maze dimensions and obstacles
maze_size = 6
obstacles = [(0,1),(1,1),(3,2),(3,3),(3,4),(3,5),(0,4),(4,1),(4,2),(4,3)]
start = (0,0)
goal = (0,5)

Step 2: Define a is_valid function

The is_valid function checks whether a given position of (x,y) is valid such that it inspects that it’s within the bounds of the maze and not obstructed by any obstacles.

Python
def is_valid(x,y):
  return 0 <= x < maze_size and 0 <= y < maze_size and (x,y) not in obstacles

Step 3: Define dfs function (Depth-first search):

In the below code, we define the dfs function to implement the DFS algorithm. It uses a stack to keep a record of the nodes it visits, and it iterates through each node that helps in exploring its neighbors recursively until it finds the goal node or it exhausts all possibilities.

  • The empty stack stores nodes to be explored, and the empty set is used to keep track of nodes that have been already visited to avoid revisiting them.
  • If the current node is found to be the goal node, it reverses the path so that the robot can follow and it finally returns “Path found”.
  • If no path is found, it returns “No path found”. This is an exhaustive search approach, it ensures that all the potential paths are explored and provides a comprehensive solution to the maze-navigating problem.
Python
def dfs (current, visited, path):
  x, y = current
  if current == goal:
    path.append(current)
    return True
  visited.add(current)
  moves = [(x-1,y), (x+1, y), (x, y-1), (x, y+1)]
  for move in moves:
    if is_valid(*move) and move not in visited:
      if dfs(move, visited, path):
        path.append(current)
        return True
  return False

Step 4: Call DFS function to find the path

Python
#Call DFS function to find the path
visited = set()
path = []
if dfs(start, visited, path):
  path.reverse()
  print("Path found:")
  for position in path:
    print(position)
else:
  print("No path found!")
    

Complete Code of Robots Pathfinding

Python
# Maze dimensions and obstacles
maze_size = 6
obstacles = [(0,1),(1,1),(3,2),(3,3),(3,4),(3,5),(0,4),(4,1),(4,2),(4,3)]
start = (0,0)
goal = (0,5)

# checks whether a given position of (x,y) is valid to move or not
def is_valid(x,y):
  return 0 <= x < maze_size and 0 <= y < maze_size and (x,y) not in obstacles

#Dfs function (Depth-first search)
def dfs (current, visited, path):
  x, y = current
  if current == goal:
    path.append(current)
    return True
  visited.add(current)
  moves = [(x-1,y), (x+1, y), (x, y-1), (x, y+1)]
  for move in moves:
    if is_valid(*move) and move not in visited:
      if dfs(move, visited, path):
        path.append(current)
        return True
  return False

#Call DFS function to find the path
visited = set()
path = []
if dfs(start, visited, path):
  path.reverse()
  print("Path found:")
  for position in path:
    print(position)
else:
  print("No path found!")

Output:

Path found:
(0, 0)
(1, 0)
(2, 0)
(3, 0)
(3, 1)
(2, 1)
(2, 2)
(1, 2)
(0, 2)
(0, 3)
(1, 3)
(2, 3)
(2, 4)
(1, 4)
(1, 5)
(0, 5)

Output explanation

Step 1: We first start at the initial position (0,0), it is where the robot begins its exploration.

Step 2: The robot moves right along the top row of the maze such as (1,0), (2,0),(3,0).

Step 3: Now, the robot moves down to the second row to the position of (3,1).

Step 4: The robot follows a path to the left (2,1), then down (2,2), then right (1,2), and then down again (0,2).

Step 5: Now, the robot moves right to reach the third row to the position (0,3).

Step 6: The robot moves right along the third row such as passing the positions of (1,3) and (2,3).

Step 7: The robot moves down to the fourth row (2,4) and then it moves left (1,4).

Step 8: The robot moves right to the last column (1,5).

Step 9: Finally, the robot reaches the goal node at the bottom-left corner(0,5) of the maze.

Output representation

Applications of DFS in AI

  • Maze generation: The Maze generation is comprised of designing a layout of passages and walls within a maze. This maze generation makes use of a randomized approach of the Depth-first search algorithm because it leverages the recursive method and stack. For instance, assume that the space is a large grid of cells where each cell holds the four walls. The DFS performs by selecting any random neighbor at first that has not been visited. It removes the wall between the two cells that are not connected and then it adds the new cell to the stack. This process continues until there is no more solution can be generated, resulting in a complete maze.
  • Puzzle-solving: Puzzle-solving including Japanese nonograms can employ Depth-first search as a method for systematically exploring possible solutions. In Japanese nonograms, DFS is utilized to explore different combinations of filled and empty cells in the grid.
  • Pathfinding in robotics: DFS can be employed for pathfinding in robotics, especially in scenarios where simplicity, memory efficiency, and adaptability are important considerations.

Conclusion

The Depth-first search emerges as a crucial algorithm in artificial intelligence that offers both theoretical insights and practical applications. DFS servers as a foundational tool for traversing complex state spaces. As the technology evolves, DFS remains a cornerstone algorithm for driving advancements and pushing the boundaries of AI research and applications.

FAQs on Depth First Search(DFS) for Artificial Intelligence

Q. Is Depth-first search suitable for finding an optimal solution?

DFS is not cost-optimal as it doesn’t guarantee finding the shortest paths as BFS does. While it is efficient in exploring all the possible paths, it may not always result in the most optimal solution in terms of path length. Other algorithms like Dijkstra’s or A* may be more suitable for finding the optimal paths.

Q. How does a Depth-first search handle the backtracking?

DFS uses backtracking to explore the alternative path when a dead end is encountered. It works by popping off the nodes from the stack that helps to retrace its steps to previous nodes and explores different branches of the tree. This allows DFS to systematically explore the entire state space.

Q. What strategies can be employed to mitigate DFS’s limitations in infinite state spaces?

Implementations of DFS in an infinite state space may incorporate cycle-checking mechanisms to prevent entering infinite loops. Additionally, limiting the depth of the search or imposing heuristics to guide the search towards promising paths can help mitigate the risk of getting stuck in infinite paths.

Q. Does Depth-first search encounter issues with memory consumption?

While DFS can be memory efficient due to its stack-based approach and backtracking mechanism, it may encounter issues with memory consumption in certain scenarios. For large state spaces or deeply nested search trees, the recursive nature of DFS may lead to stack overflow errors or excessive memory usage.

Q. Can Depth-first search be parallelized for efficient exploration of large state spaces?

While DFS is inherently sequential due to its stack-based traversal, parallelized versions of DFS exist that leverage multi-threading or distributed computing techniques. By partitioning the search space and exploring multiple branches concurrently the parallel DFS implementations can potentially improve exploration efficiency in large state spaces.



Contact Us