Search Code
Search Code
1 Search Example
1.1 Tree Diagram Image
1.1.1 Uninformed Search Algorithms**
You have been provided with a graph-based problem where the goal is to navigate from a
starting node to a goal node without any prior knowledge of the path costs. The graph may be
a tree, a grid, or an arbitrary connected structure with nodes and edges. Since no heuristic
information is available, the problem requires the application of uninformed search strategies
to explore the graph systematically.
Your task is to implement and evaluate various uninformed search algorithms, including:
1. Breadth-First Search (BFS): Expands all nodes at the current depth before moving
deeper, ensuring the shortest path in an unweighted graph.
2. Depth-First Search (DFS): Explores as deep as possible before backtracking, which can
be memory efficient but does not guarantee the shortest path.
3. Depth-Limited Search (DLS): A variation of DFS with a predefined depth limit to avoid
infinite recursion and unnecessary exploration.
4. Iterative Deepening Depth-First Search (IDDFS): Repeatedly applies DLS with in-
creasing depth limits, combining the benefits of BFS and DFS.
5. Uniform Cost Search (UCS): Expands the least-cost node first (if edge costs are provided),
ensuring optimality in weighted graphs.
For each algorithm, you need to:
• Implement the search logic to find a path from the given start node to the goal node.
• Track and display the order of nodes explored during the search process.
• Evaluate performance based on:
– Completeness: Whether the algorithm guarantees finding a solution if one exists.
– Optimality: Whether the algorithm guarantees finding the shortest or least-cost path.
– Time Complexity: The number of nodes expanded based on branching factor (b) and
solution depth (d).
– Space Complexity: The amount of memory required during execution.
– Total Nodes Expanded: The number of nodes processed before reaching the goal.
The output should include: - The path found by each algorithm (or failure if no path is found). -
The list of explored nodes in order. - The performance metrics to compare the efficiency of
different search methods.
This ensures that each uninformed search algorithm is systematically evaluated for its
1
effectiveness, trade-offs, and suitability for different graph structures, such as trees, grids, and
networks.
Would you like an example implementation for all uninformed search algorithms? �
class BFS:
def __init__(self, graph):
self.graph = graph
self.explored_nodes = [] # Track explored nodes
self.nodes_expanded = 0 # Count of expanded nodes
self.solution_depth = 0 # Store depth of the solution
while queue:
path = queue.popleft() # Get the first path
node = path[-1] # Last node in the path
if node == goal:
self.solution_depth = level[node] # Store depth of the solution
return path, self.performance_metrics()
def performance_metrics(self):
num_nodes = len(self.graph)
max_branching_factor = max(len(v) for v in self.graph.values()) #␣
↪Maximum number of children per node
2
# Calculate theoretical time and space complexities
if max_branching_factor > 0 and depth > 0:
time_complexity = max_branching_factor ** depth # O(b^d)
space_complexity = max_branching_factor ** depth # O(b^d)
else:
time_complexity = space_complexity = num_nodes # Worst case␣
↪scenario
return {
"Completeness": "Yes (BFS always finds a solution if one exists)",
"Optimality": "Yes (BFS guarantees the shortest path in an␣
↪unweighted graph)",
Explanation Tracking Explored Nodes: The self.explored_nodes list stores the sequence of nodes
explored during the search.
Nodes Expanded: The self.nodes_expanded counter tracks how many nodes were dequeued and
checked.
Performance Metrics:
Completeness: Always finds a path if one exists.
Optimality: BFS guarantees the shortest path.
Time Complexity: O(b^d), where b is the branching factor and d is the depth of the solution.
Space Complexity: O(b^d), since BFS needs to store all nodes at depth d.
3
# Print results
print("BFS Path from A to G:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes (BFS always finds a solution if one exists)
Optimality: Yes (BFS guarantees the shortest path in an unweighted graph)
Time Complexity: O(2^3) � 8
Space Complexity: O(2^3) � 8
Nodes Explored: ['A', 'B', 'C', 'D', 'E', 'F', 'G']
Total Nodes Expanded: 7
# Print results
print("BFS Path from A to C:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes (BFS always finds a solution if one exists)
Optimality: Yes (BFS guarantees the shortest path in an unweighted graph)
Time Complexity: O(2^3) � 8
Space Complexity: O(2^3) � 8
Nodes Explored: ['A', 'B', 'C', 'D', 'E', 'F', 'G']
4
Total Nodes Expanded: 7
path.append(start)
visited.add(start)
self.explored_nodes.append(start) # Log explored nodes
self.nodes_expanded += 1 # Increment expanded node count
self.max_depth = max(self.max_depth, depth) # Track max depth reached
if start == goal:
self.solution_depth = depth # Store solution depth
return path, self.performance_metrics()
if result:
return result, metrics # Return path if found
def performance_metrics(self):
num_nodes = len(self.graph)
max_branching_factor = max(len(v) for v in self.graph.values()) #␣
↪Maximum number of children per node
5
# Space complexity in worst case is O(V) (recursion stack depth)
space_complexity = self.max_depth
return {
"Completeness": "Yes" if self.solution_depth > 0 else "No",
"Optimality": self.is_optimal(),
"Time Complexity": f"O({max_branching_factor}^{depth}) �␣
↪{time_complexity}",
def is_optimal(self):
"""Check if DFS found the shortest path by comparing with BFS."""
bfs_solver = BFS(self.graph) # Use BFS to find shortest path
bfs_path, _ = bfs_solver.bfs_search('A', 'G') # Get BFS shortest path
dfs_path, _ = self.dfs_search('A', 'G') # Get DFS path
class DFS:
def __init__(self, graph):
self.graph = graph
self.explored_nodes = [] # Track explored nodes
self.nodes_expanded = 0 # Count of expanded nodes
self.solution_depth = 0 # Store depth of the solution
self.max_depth = 0 # Track maximum depth reached
while stack:
node, path, depth = stack.pop()
self.explored_nodes.append(node) # Log explored nodes
self.nodes_expanded += 1 # Increment expanded node count
self.max_depth = max(self.max_depth, depth) # Track max depth␣
↪reached
6
if node == goal:
self.solution_depth = depth
return path, self.performance_metrics(path)
while queue:
path = queue.popleft()
node = path[-1]
if node == goal:
return path # BFS always finds the shortest path
return {
"Completeness": "Yes" if dfs_path else "No",
"Optimality": self.is_optimal(dfs_path),
7
"Time Complexity": f"O({max_branching_factor}^{depth}) �␣
↪{time_complexity}",
"Space Complexity": f"O({space_complexity}) (stack depth =␣
↪{space_complexity})",
# Print results
print("DFS Path from A to G:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes
Optimality: Yes
Time Complexity: O(2^3) � 8
8
Space Complexity: O(3) (stack depth = 3)
Nodes Explored: ['A', 'B', 'D', 'G']
Total Nodes Expanded: 4
# Print results
print("DFS Path from A to C:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes
Optimality: No
Time Complexity: O(2^1) � 2
Space Complexity: O(3) (stack depth = 3)
Nodes Explored: ['A', 'B', 'D', 'G', 'E', 'C']
Total Nodes Expanded: 6
class DepthLimitedSearch:
def __init__(self, graph):
self.graph = graph
self.explored_nodes = [] # Track explored nodes
self.nodes_expanded = 0 # Count of expanded nodes
self.solution_depth = None # Depth where solution was found
9
def dls_search(self, start, goal, depth_limit, depth=0, path=None,␣
↪visited=None):
if path is None:
path = [] # Store current path
if visited is None:
visited = set() # Track visited nodes
path.append(start)
visited.add(start)
self.explored_nodes.append(start)
self.nodes_expanded += 1
# Goal found
if start == goal:
self.solution_depth = depth
return path, self.performance_metrics(path)
# Explore neighbors
for neighbor in self.graph.get(start, []):
if neighbor not in visited:
result, metrics = self.dls_search(neighbor, goal, depth_limit,␣
↪depth + 1, path.copy(), visited)
if result:
return result, metrics # Return path if found
while queue:
path = queue.popleft()
node = path[-1]
if node == goal:
return path # BFS always finds the shortest path
10
queue.append(path + [neighbor])
# Space Complexity O(d) (since DLS only stores nodes up to the depth␣
↪limit)
return {
"Completeness": "Yes" if dls_path else "No",
"Optimality": self.is_optimal(dls_path),
"Time Complexity": f"O({max_branching_factor}^{depth}) �␣
↪{time_complexity}" if isinstance(depth, int) else "N/A",
11
'F': [],
'G': []
}
Performance Metrics:
Completeness: No
Optimality: No
Time Complexity: N/A
Space Complexity: N/A
Nodes Explored: ['A', 'B', 'D', 'E', 'C', 'F']
Total Nodes Expanded: 6
Performance Metrics:
Completeness: Yes
Optimality: Yes
Time Complexity: O(2^3) � 8
Space Complexity: O(3) (stack depth = 3)
Nodes Explored: ['A', 'B', 'D', 'E', 'C', 'F', 'A', 'B', 'D', 'G']
Total Nodes Expanded: 10
12
'C': ['F'],
'D': ['G'],
'E': [],
'F': [],
'G': []
}
Performance Metrics:
Completeness: Yes
Optimality: No
Time Complexity: O(2^1) � 2
Space Complexity: O(1) (stack depth = 1)
Nodes Explored: ['A', 'B', 'D', 'E', 'C']
Total Nodes Expanded: 5
Performance Metrics:
Completeness: Yes
Optimality: No
Time Complexity: O(2^1) � 2
Space Complexity: O(1) (stack depth = 1)
Nodes Explored: ['A', 'B', 'D', 'E', 'C', 'A', 'B', 'D', 'G', 'E', 'C']
Total Nodes Expanded: 11
13
1.5 4.Iterative Deepening Depth-First Search (IDDFS)
[71]: class IDDFS:
def __init__(self, graph):
self.graph = graph
self.explored_nodes = [] # Track explored nodes across all iterations
self.nodes_expanded = 0 # Count of total nodes expanded
path.append(start)
visited.add(start)
self.explored_nodes.append(start)
self.nodes_expanded += 1
# Goal found
if start == goal:
return path
# Explore neighbors
for neighbor in self.graph.get(start, []):
if neighbor not in visited:
result = self.dls_search(neighbor, goal, depth_limit, depth +␣
↪1, path.copy(), visited)
if result:
return result # Return path if found
14
return None, self.performance_metrics(max_depth) # Goal not found␣
↪within max depth
# Print results
print("IDDFS Path from A to G:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes
Optimality: Yes
Time Complexity: O(b^3)
Space Complexity: O(3) (stack depth = 3)
Nodes Explored: ['A', 'B', 'D', 'G']
Total Nodes Expanded: 4
15
[73]: # Define the tree structure as an adjacency list
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': ['G'],
'E': [],
'F': [],
'G': []
}
# Print results
print("IDDFS Path from A to C:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes
Optimality: Yes
Time Complexity: O(b^1)
Space Complexity: O(1) (stack depth = 1)
Nodes Explored: ['A', 'B', 'C']
Total Nodes Expanded: 3
class UniformCostSearch:
def __init__(self, graph):
self.graph = graph
self.explored_nodes = [] # Track explored nodes
self.nodes_expanded = 0 # Count of expanded nodes
self.history = [] # Track history of visited paths
16
visited = {} # Dictionary to store the minimum cost to reach a node
while priority_queue:
cost, node, path = heapq.heappop(priority_queue)
self.explored_nodes.append(node)
self.nodes_expanded += 1
[75]: # Define the weighted graph as an adjacency list (node: [(neighbor, cost), ...])
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('D', 2), ('E', 5)],
'C': [('F', 3)],
'D': [('G', 1)],
'E': [],
'F': [],
'G': []
}
17
# Initialize UCS class
ucs_solver = UniformCostSearch(graph)
# Print results
print("UCS Path from A to G:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes
Optimality: Yes
Time Complexity: O((V + E) log V)
Space Complexity: O(V) (Priority queue size � number of nodes)
Total Cost: 4
Nodes Explored: ['A', 'B', 'D', 'C', 'G']
Total Nodes Expanded: 5
Exploration History: [(['A'], 0), (['A', 'B'], 1), (['A', 'B', 'D'], 3), (['A',
'C'], 4), (['A', 'B', 'D', 'G'], 4)]
Exploration History:
Step 1: Path Explored ['A'], Cost: 0
Step 2: Path Explored ['A', 'B'], Cost: 1
Step 3: Path Explored ['A', 'B', 'D'], Cost: 3
Step 4: Path Explored ['A', 'C'], Cost: 4
Step 5: Path Explored ['A', 'B', 'D', 'G'], Cost: 4
[76]: # Define the weighted graph as an adjacency list (node: [(neighbor, cost), ...])
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('D', 2), ('E', 5)],
'C': [('F', 3)],
'D': [('G', 1)],
'E': [],
'F': [],
'G': []
18
}
# Print results
print("UCS Path from A to C:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes
Optimality: Yes
Time Complexity: O((V + E) log V)
Space Complexity: O(V) (Priority queue size � number of nodes)
Total Cost: 4
Nodes Explored: ['A', 'B', 'D', 'C']
Total Nodes Expanded: 4
Exploration History: [(['A'], 0), (['A', 'B'], 1), (['A', 'B', 'D'], 3), (['A',
'C'], 4)]
Exploration History:
Step 1: Path Explored ['A'], Cost: 0
Step 2: Path Explored ['A', 'B'], Cost: 1
Step 3: Path Explored ['A', 'B', 'D'], Cost: 3
Step 4: Path Explored ['A', 'C'], Cost: 4
19
2.1 Your Task
Implement and evaluate various informed search algorithms, including:
1. Greedy Best-First Search (GBFS):
• Expands the node that appears to be closest to the goal based on heuristic values h(n).
class GreedyBestFirstSearch:
def __init__(self, graph, heuristic):
self.graph = graph
self.heuristic = heuristic
self.explored_nodes = [] # Track explored nodes
self.nodes_expanded = 0 # Count of expanded nodes
self.max_queue_size = 0 # Track max priority queue size
self.execution_time = 0 # Track time complexity
self.exploration_history = [] # Track path exploration with heuristic␣
↪values
while priority_queue:
self.max_queue_size = max(self.max_queue_size, len(priority_queue))␣
↪ # Track queue size
heuristic_cost, node, path = heapq.heappop(priority_queue)
self.explored_nodes.append(node)
20
self.nodes_expanded += 1
return {
"Completeness": "Yes" if gbfs_path else "No",
"Optimality": "No (GBFS does not guarantee the shortest path)",
"Time Complexity": f"O({time_complexity} log {num_nodes}) �␣
↪{round(self.execution_time, 2)} ms",
[80]: # Define the graph as an adjacency list (node: [(neighbor, cost), ...])
graph = {
21
'A': [('B', 6), ('C', 4)],
'B': [('D', 3), ('E', 2)],
'C': [('F', 5)],
'D': [('G', 1)],
'E': [],
'F': [],
'G': []
}
# Define the heuristic values for each node (estimated cost to goal)
heuristic = {
'A': 6, 'B': 4, 'C': 2,
'D': 3, 'E': 2, 'F': 5,
'G': 0 # Goal node has heuristic value 0
}
# Print results
print("GBFS Path from A to G:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes
Optimality: No (GBFS does not guarantee the shortest path)
Time Complexity: O(13 log 7) � 0.02 ms
Space Complexity: O(3) (Max queue size = 3)
Nodes Explored: ['A', 'C', 'B', 'E', 'D', 'G']
Total Nodes Expanded: 6
Exploration History: [('A', 6, ['A']), ('C', 2, ['A', 'C']), ('B', 4, ['A',
'B']), ('E', 2, ['A', 'B', 'E']), ('D', 3, ['A', 'B', 'D']), ('G', 0, ['A', 'B',
'D', 'G'])]
22
Exploration History (Node, Heuristic, Path Explored):
Step 1: Node A (h=6), Path Explored: ['A']
Step 2: Node C (h=2), Path Explored: ['A', 'C']
Step 3: Node B (h=4), Path Explored: ['A', 'B']
Step 4: Node E (h=2), Path Explored: ['A', 'B', 'E']
Step 5: Node D (h=3), Path Explored: ['A', 'B', 'D']
Step 6: Node G (h=0), Path Explored: ['A', 'B', 'D', 'G']
[84]: # Define the graph as an adjacency list (node: [(neighbor, cost), ...])
graph = {
'A': [('B', 6), ('C', 4)],
'B': [('D', 3), ('E', 2)],
'C': [('F', 5)],
'D': [('G', 1)],
'E': [],
'F': [],
'G': []
}
# Define the heuristic values for each node (estimated cost to goal)
heuristic = {
'A': 2, 'B': 3, 'G': 4 ,
'D': 3, 'E': 2, 'F': 5,
'C': 0, # Goal node has heuristic value 0
}
# Print results
print("GBFS Path from A to C:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
23
Completeness: Yes
Optimality: No (GBFS does not guarantee the shortest path)
Time Complexity: O(13 log 7) � 0.01 ms
Space Complexity: O(2) (Max queue size = 2)
Nodes Explored: ['A', 'C']
Total Nodes Expanded: 2
Exploration History: [('A', 2, ['A']), ('C', 0, ['A', 'C'])]
class AStarSearch:
def __init__(self, graph, heuristic):
self.graph = graph
self.heuristic = heuristic
self.explored_nodes = [] # Track explored nodes
self.nodes_expanded = 0 # Count of expanded nodes
self.max_queue_size = 0 # Track max priority queue size
self.execution_time = 0 # Track execution time
self.exploration_history = [] # Track path exploration with heuristic␣
↪values
while priority_queue:
self.max_queue_size = max(self.max_queue_size, len(priority_queue))␣
↪ # Track queue size
_, g_cost, node, path = heapq.heappop(priority_queue)
self.explored_nodes.append(node)
self.nodes_expanded += 1
24
if node == goal:
self.execution_time = (time.time() - start_time) * 1000 #␣
↪Convert to milliseconds
return {
"Completeness": "Yes" if astar_path else "No",
"Optimality": "Yes (A* guarantees shortest path if h is admissible␣
↪and consistent)",
[92]: # Define the graph as an adjacency list (node: [(neighbor, cost), ...])
graph = {
'A': [('B', 6), ('C', 4)],
'B': [('D', 3), ('E', 2)],
'C': [('F', 5)],
25
'D': [('G', 1)],
'E': [],
'F': [],
'G': []
}
# Define the heuristic values for each node (estimated cost to goal)
heuristic = {
'A': 6, 'B': 4, 'C': 2,
'D': 3, 'E': 2, 'F': 5,
'G': 0 # Goal node has heuristic value 0
}
# Print results
print("A* Path from A to C:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes
Optimality: Yes (A* guarantees shortest path if h is admissible and consistent)
Time Complexity: O(13 log 7) � 0.02 ms
Space Complexity: O(3) (Max queue size = 3)
Total Cost: 10
Nodes Explored: ['A', 'C', 'B', 'E', 'D', 'G']
Total Nodes Expanded: 6
Exploration History: [('A', 0, 6, ['A']), ('C', 4, 2, ['A', 'C']), ('B', 6, 4,
['A', 'B']), ('E', 8, 2, ['A', 'B', 'E']), ('D', 9, 3, ['A', 'B', 'D']), ('G',
10, 0, ['A', 'B', 'D', 'G'])]
26
Step 2: Node C (g=4, h=2), Path Explored: ['A', 'C']
Step 3: Node B (g=6, h=4), Path Explored: ['A', 'B']
Step 4: Node E (g=8, h=2), Path Explored: ['A', 'B', 'E']
Step 5: Node D (g=9, h=3), Path Explored: ['A', 'B', 'D']
Step 6: Node G (g=10, h=0), Path Explored: ['A', 'B', 'D', 'G']
[93]: # Define the graph as an adjacency list (node: [(neighbor, cost), ...])
graph = {
'A': [('B', 6), ('C', 4)],
'B': [('D', 3), ('E', 2)],
'C': [('F', 5)],
'D': [('G', 1)],
'E': [],
'F': [],
'G': []
}
# Define the heuristic values for each node (estimated cost to goal)
heuristic = {
'A': 2, 'B': 3, 'G': 4 ,
'D': 3, 'E': 2, 'F': 5,
'C': 0, # Goal node has heuristic value 0
}
# Print results
print("A* Path from A to C:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes
Optimality: Yes (A* guarantees shortest path if h is admissible and consistent)
27
Time Complexity: O(13 log 7) � 0.01 ms
Space Complexity: O(2) (Max queue size = 2)
Total Cost: 4
Nodes Explored: ['A', 'C']
Total Nodes Expanded: 2
Exploration History: [('A', 0, 2, ['A']), ('C', 4, 0, ['A', 'C'])]
class IDAStar:
def __init__(self, graph, heuristic):
self.graph = graph
self.heuristic = heuristic
self.explored_nodes = [] # Track explored nodes
self.nodes_expanded = 0 # Count of expanded nodes
self.execution_time = 0 # Track execution time
self.exploration_history = [] # Store explored nodes with costs
self.threshold_count = 0 # Count the number of threshold updates
while True:
result, new_threshold = self.depth_limited_search(path, 0,␣
↪threshold, goal)
28
f_cost = g + self.heuristic[node] # f(n) = g(n) + h(n)
self.explored_nodes.append(node)
self.nodes_expanded += 1
path.pop() # Backtrack
return {
"Completeness": "Yes" if ida_path else "No",
"Optimality": "Yes (IDA* guarantees shortest path if h is␣
↪admissible and consistent)",
29
Performance Metrics:
Completeness: Yes
Optimality: Yes (IDA* guarantees shortest path if h is admissible and
consistent)
Time Complexity: O(13 log 7) � 0.01 ms
Space Complexity: O(d) (Memory-efficient recursive DFS)
Total Nodes Expanded: 3
Threshold Updates: 0
Nodes Explored: ['A', 'B', 'C']
Exploration History: [('A', 0, 6, 6, ['A']), ('B', 6, 4, 10, ['A', 'B']), ('C',
4, 0, 4, ['A', 'C'])]
[99]: # Define the graph as an adjacency list (node: [(neighbor, cost), ...])
graph = {
'A': [('B', 6), ('C', 4)],
'B': [('D', 3), ('E', 2)],
'C': [('F', 5)],
'D': [('G', 1)],
'E': [],
'F': [],
'G': []
}
# Define the heuristic values for each node (estimated cost to goal)
heuristic = {
'A': 6, 'B': 4, 'C': 2,
'D': 3, 'E': 2, 'F': 5,
'G': 0 # Goal node has heuristic value 0
}
# Print results
print("IDA* Path from A to G:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
30
# Print exploration history
print("\nExploration History (Node, g(n), h(n), f(n), Path Explored):")
for step, (node, g_value, h_value, f_value, visited_path) in␣
↪enumerate(metrics["Exploration History"], 1):
Performance Metrics:
Completeness: Yes
Optimality: Yes (IDA* guarantees shortest path if h is admissible and
consistent)
Time Complexity: O(13 log 7) � 0.03 ms
Space Complexity: O(d) (Memory-efficient recursive DFS)
Total Nodes Expanded: 14
Threshold Updates: 2
Nodes Explored: ['A', 'B', 'C', 'F', 'A', 'B', 'D', 'E', 'C', 'F', 'A', 'B',
'D', 'G']
Exploration History: [('A', 0, 6, 6, ['A']), ('B', 6, 4, 10, ['A', 'B']), ('C',
4, 2, 6, ['A', 'C']), ('F', 9, 5, 14, ['A', 'C', 'F']), ('A', 0, 6, 6, ['A']),
('B', 6, 4, 10, ['A', 'B']), ('D', 9, 3, 12, ['A', 'B', 'D']), ('E', 8, 2, 10,
['A', 'B', 'E']), ('C', 4, 2, 6, ['A', 'C']), ('F', 9, 5, 14, ['A', 'C', 'F']),
('A', 0, 6, 6, ['A']), ('B', 6, 4, 10, ['A', 'B']), ('D', 9, 3, 12, ['A', 'B',
'D']), ('G', 10, 0, 10, ['A', 'B', 'D', 'G'])]
[101]: # Define the graph as an adjacency list (node: [(neighbor, cost), ...])
graph = {
'A': [('B', 6), ('C', 4)],
'B': [('D', 3), ('E', 2)],
31
'C': [('F', 5)],
'D': [('G', 1)],
'E': [],
'F': [],
'G': []
}
# Define the heuristic values for each node (estimated cost to goal)
heuristic = {
'A': 2, 'B': 3, 'G': 4 ,
'D': 3, 'E': 2, 'F': 5,
'C': 0, # Goal node has heuristic value 0
}
# Print results
print("IDA* Path from A to C:", path)
print("\nPerformance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
Performance Metrics:
Completeness: Yes
Optimality: Yes (IDA* guarantees shortest path if h is admissible and
consistent)
Time Complexity: O(13 log 7) � 0.03 ms
Space Complexity: O(d) (Memory-efficient recursive DFS)
Total Nodes Expanded: 6
Threshold Updates: 1
Nodes Explored: ['A', 'B', 'C', 'A', 'B', 'C']
Exploration History: [('A', 0, 2, 2, ['A']), ('B', 6, 3, 9, ['A', 'B']), ('C',
4, 0, 4, ['A', 'C']), ('A', 0, 2, 2, ['A']), ('B', 6, 3, 9, ['A', 'B']), ('C',
4, 0, 4, ['A', 'C'])]
32
Step 1: Node A (g=0, h=2, f=2), Path Explored: ['A']
Step 2: Node B (g=6, h=3, f=9), Path Explored: ['A', 'B']
Step 3: Node C (g=4, h=0, f=4), Path Explored: ['A', 'C']
Step 4: Node A (g=0, h=2, f=2), Path Explored: ['A']
Step 5: Node B (g=6, h=3, f=9), Path Explored: ['A', 'B']
Step 6: Node C (g=4, h=0, f=4), Path Explored: ['A', 'C']
class AStarRoutePlanner:
def __init__(self, city_graph, heuristic):
self.city_graph = city_graph
self.heuristic = heuristic
self.explored_nodes = [] # Track explored locations
self.nodes_expanded = 0 # Count of expanded locations
self.execution_time = 0 # Track execution time
self.exploration_history = [] # Store explored nodes with costs
visited = {}
while priority_queue:
_, g_cost, location, path = heapq.heappop(priority_queue)
self.explored_nodes.append(location)
self.nodes_expanded += 1
33
self.exploration_history.append((location, g_cost, self.
↪heuristic[location], path))
city_graph = {
'Home': [('Mall', 10), ('Library', 6)],
'Mall': [('Office', 15), ('Restaurant', 8)],
'Library': [('Park', 5)],
'Office': [('Gym', 12)],
'Restaurant': [('Gym', 10), ('Park', 7)],
'Park': [('Gym', 4)],
'Gym': []
}
34
'Home': 14, 'Mall': 12, 'Library': 9, 'Office': 8,
'Restaurant': 7, 'Park': 4, 'Gym': 0 # Goal (Gym) has h(Gym) = 0
}
# Print results
print("\n� A* Route from Home to Gym:", route)
print("\n� Performance Metrics:")
for key, value in metrics.items():
print(f"{key}: {value}")
� Performance Metrics:
Route Found: ['Home', 'Library', 'Park', 'Gym']
Total Distance: 15
Execution Time: 0.07 ms
Nodes Explored: ['Home', 'Library', 'Park', 'Gym']
Total Nodes Expanded: 4
Exploration History: [('Home', 0, 14, ['Home']), ('Library', 6, 9, ['Home',
'Library']), ('Park', 11, 4, ['Home', 'Library', 'Park']), ('Gym', 15, 0,
['Home', 'Library', 'Park', 'Gym'])]
35
• Edges = Travel distances between places.
36