A Primer on Search Problems: From DFS to A* Search
July 2, 2023 / 7 min read
Last Updated: May 4, 2024Introduction
Whether you're navigating a social network, finding the shortest path in a city, or solving complex puzzles, search algorithms are crucial. We'll explore various search algorithms, from Depth-First Search (DFS) to A* Search, breaking down their use cases and how they work. By the end, you'll have a solid understanding of these algorithms and how to apply them to solve real-world problems.
Formulating a Search Problem
To tackle a search problem, we need to define a few key components:
- State: The initial state where the agent starts.
- Actions: Possible actions the agent can take and their outcomes.
- Performance Measure: Criteria to determine if the agent has reached its goal.
- Goal: The desired state the agent aims to achieve.
A solution to a search problem is a sequence of actions that form a path to the goal state.
Dimensions for Evaluation
To evaluate search algorithms, we consider:
- Completeness: Does the algorithm always find a solution if one exists?
- Optimality: Does it find the least cost solution?
- Time Complexity: How many nodes are generated in the worst case?
- Space Complexity: How many nodes are stored in memory at the same time in the worst case?
Complexity Variables
- b: Maximum branching factor of the search tree (number of children per node).
- d: Depth of the shallowest goal node (distance from the start node in terms of edge traversals).
- m: Maximum length of any path in the state space.
Uninformed Search
Uninformed or blind search strategies rely only on the information available in the problem definition. They treat all non-goal nodes in the frontier equally and differ in their tree expansion order, implemented by different queue structures (LIFO, FIFO, priority).
1. Depth-First Search (DFS)
Depth-First Search (DFS) is a fundamental graph traversal algorithm used to explore nodes and edges of a graph. Starting from a root node, DFS explores as far as possible along each branch before backtracking, making it suitable for searching tree structures and discovering connectivity in graphs.
DFS Code
Depth-First Search (DFS)
1def dfs(start, graph):2stack = [start]3reached = set()4reached.add(start)56while stack:7current = stack.pop()8for neighbor in graph.neighbors(current):9if neighbor not in reached:10stack.append(neighbor)11reached.add(neighbor)
DFS Complexity
- Complete? No - DFS fails with infinite-depth spaces or spaces with loops. To avoid getting stuck, check for repeated states.
- Time Complexity?
- Can be very bad if (maximum depth) is much larger than (depth of the solution). - Space Complexity?
- Requires linear space for each node's siblings and children. - Optimal? No - DFS may find a suboptimal solution if the optimal goal is at a lower depth than the path DFS takes.
DFS Use Case (For you Leetcode People!)
Finding the Number of Connected Components (Undirected Graph)
- Examples: 547. Number of Provinces, 200. Number of Islands
Reordering Routes
Finding Strongly Connected Components in Directed Graphs
- Kosaraju's Algorithm: Perform DFS on the original graph, reverse the graph, and perform DFS on the reversed graph in the order of decreasing finish times. Each DFS run reveals a strongly connected component.
2. Breadth-First Search (BFS)
Breadth-First Search (BFS) explores nodes level by level, making it ideal for finding the shortest path in an unweighted graph. Starting from the root node, BFS expands all its neighbors before moving on to the next level.
BFS Code
Breadth-First Search (BFS)
1from queue import Queue23def bfs(start, graph):4frontier = Queue()5frontier.put(start)6reached = set()7reached.add(start)89while not frontier.empty():10current = frontier.get()11for neighbor in graph.neighbors(current):12if neighbor not in reached:13frontier.put(neighbor)14reached.add(neighbor)
BFS Complexity
- Complete? Yes, if the branching factor
is finite. - Time Complexity?
- Space Complexity?
- Optimal? Yes, if all step costs are equal.
BFS Use Case
Shortest Path in Unweighted Graphs
- Example: Finding the shortest path in a maze or unweighted network.
Finding All Nodes Within a Certain Distance
- Example: Social network analysis to find all friends within a certain number of connections.
3. Iterative Deepening Depth-First Search (IDDFS)
**Iterative Deepening Depth-First Search (IDDFS) **combines the space efficiency of DFS with the completeness of BFS. It performs DFS with increasing depth limits until a solution is found.
IDDFS Code
Iterative Deepening Depth-First Search (IDDFS)
1def iddfs(start, goal, graph):2def dls(node, depth):3if depth == 0 and node == goal:4return [node]5if depth > 0:6for neighbor in graph.neighbors(node):7path = dls(neighbor, depth - 1)8if path:9return [node] + path10return None1112depth = 013while True:14path = dls(start, depth)15if path:16return path17depth += 1
IDDFS Complexity
- Complete? Yes, if no infinite paths.
- Time Complexity?
- Space Complexity?
- Optimal? Yes, if all step costs are equal.
Informed Search
Informed search algorithms use additional information (heuristics) to guide the search process more efficiently towards the goal. By incorporating heuristics, these algorithms can significantly reduce the search space and find optimal solutions more quickly than uninformed methods. Key examples include Dijkstra's Algorithm, Greedy Best-First Search, and A* Search.
1. Uniform Cost Search (Dijkstra's Algorithm)
Dijkstra's algorithm is used for finding the shortest paths between nodes in a weighted graph. It expands the least-cost node first, making it optimal for graphs with varying edge weights.
Dijkstra's Code
Uniform Cost Search (Dijkstra)
1from queue import PriorityQueue23def dijkstra(start, goal, graph):4frontier = PriorityQueue()5frontier.put((0, start))6came_from = {}7cost_so_far = {}8came_from[start] = None9cost_so_far[start] = 01011while not frontier.empty():12current_priority, current = frontier.get()1314if current == goal:15break1617for next in graph.neighbors(current):18new_cost = cost_so_far[current] + graph.cost(current, next)19if next not in cost_so_far or new_cost < cost_so_far[next]:20cost_so_far[next] = new_cost21priority = new_cost22frontier.put((priority, next))23came_from[next] = current
Dijkstra's Complexity
- Complete? Yes.
- Time Complexity?
- Space Complexity?
- Optimal? Yes, for graphs with non-negative weights.
2. Greedy Best-First Search
Greedy Best-First Introduction
Greedy Best-First Search uses heuristics to guide its search, prioritizing nodes that appear closest to the goal. It is faster than other methods but may not always find the optimal path.
Greedy Best-First Code
Greedy Best-First Search
1from queue import PriorityQueue23def greedy_best_first_search(start, goal, graph, heuristic):4frontier = PriorityQueue()5frontier.put((0, start))6came_from = {}7came_from[start] = None89while not frontier.empty():10current_priority, current = frontier.get()1112if current == goal:13break1415for next in graph.neighbors(current):16if next not in came_from:17priority = heuristic(goal, next)18frontier.put((priority, next))19came_from[next] = current
Greedy Best-First Complexity
- Complete? No, unless a 'seen' set is used.
- Time Complexity?
- Space Complexity?
- **Optimal?
** No, it may not find the shortest path.
3. A* Search
A* search combines the strengths of Dijkstra's algorithm and Greedy Best-First Search by using both the cost so far (
A* Code
A* Search
1from queue import PriorityQueue23def a_star_search(start, goal, graph, heuristic):4frontier = PriorityQueue()5frontier.put((0, start))6came_from = {}7cost_so_far = {}8came_from[start] = None9cost_so_far[start] = 01011while not frontier.empty():12current_priority, current = frontier.get()1314if current == goal:15break1617for next in graph.neighbors(current):18new_cost = cost_so_far[current] + graph.cost(current, next)19if next not in cost_so_far or new_cost < cost_so_far[next]:20cost_so_far[next] = new_cost21priority = new_cost + heuristic(goal, next)22frontier.put((priority, next))23came_from[next] = current
A* Complexity
- Complete? Yes, if the heuristic is admissible (never overestimates the true cost).
- Time Complexity?
- Space Complexity?
- Optimal? Yes, if the heuristic is admissible.
Have a wonderful day.
– Frank
From DFS, BFS, IDDFS, UCS, Greedy, to A* - a comprehensive journy in the world of search.