AILAB
Krish Shah
SY CS
231070061
Batch D
Aim: To perform A* search and Best First Search
Theory:
1] A* Search Algorithm
1.1. Description
The A* (A-star) algorithm is a widely used path finding and
graph traversal algorithm. It is designed to find the shortest
path from a starting node to a goal node in a weighted graph.
A* combines the advantages of Dijkstra's algorithm (which
guarantees the shortest path) and Greedy Best-First Search
(which is efficient but does not guarantee the shortest path). It
uses a heuristic function to estimate the cost from the current
node to the goal, making it more efficient than Dijkstra's in
many cases.
1.2. How does it work?
A* finds the optimal path by evaluating nodes using the
function:
f(n)=g(n)+h(n)
where:
● f(n) → Estimated total cost from start to goal passing
through node n.
● g(n) → Cost from the start node to node n (known cost).
● h(n) → Heuristic function that estimates the cost from
node n to the goal.
AILAB
Heuristic Function h(n): The choice of heuristic function is
critical to A*'s performance. A heuristic is:
● Admissible → it never overestimates the cost to reach
the goal (ensures optimality).
● Consistent (Monotonic) → It satisfies the condition:
h(n) ≤ c(n,m)
+h(m) where c(n,m) is the actual cost
from n to m.
Common Heuristics Used
● Manhattan Distance (for grid-based paths): h(n)=|x1−x2|
+|y1−y2|
● Euclidean Distance (for continuous space
paths): h(n)=(x1−x2)^2+(y1−y2)^2
● Chebyshev Distance (for diagonal moves in
grids): h(n)=max(|x1−x2|,|y1−y2|)
1.3. Algorithm
1. Initialization:
o Create an open list (priority queue) and a closed list
(visited nodes).
o Add the start node to the open list with f(n) = g(n) +
h(n).
o Set g(n) for the start node to 0.
2. Main Loop:
o While the open list is not empty:
▪ Select the node with the lowest f(n) from the
open list.
▪ If this node is the goal, reconstruct the path
and return it.
▪ Otherwise, move the node to the closed list.
3. Expand Neighbours:
o For each neighbour of the current node:
▪ Calculate g(n) for the neighbour as
g(current) + cost(current, neighbour).
▪ If the neighbour is already in the closed list
and the new g(n) is higher, skip it.
AILAB
▪ If the neighbour is not in the open list or the
new g(n) is lower:
▪ Update the neighbour's g(n) and f(n).
AILAB
▪ Add the neighbour to the open list if
it's not already there.
4. Termination:
o If the open list is empty and the goal has not been
reached, no path exists.
1.4. Time Complexity
● The time complexity of A* depends on the heuristic
function and the structure of the graph.
● In the worst case, A* explores all nodes, leading
to a time complexity of O(b^d), where:
o b is the branching factor (average number of
neighbours per node).
o d is the depth of the goal node (number of steps to
reach the goal).
● With a good heuristic, the number of explored
nodes can be significantly reduced.
1.5. Space Complexity
● The space complexity is O(b^d) because A* stores all
explored nodes in memory (open and closed lists).
Best First Search is a heuristic search algorithm that explores a
graph by expanding the most promising node first, according to
a specified evaluation function. We use a priority queue or heap
to store the costs of nodes that have the lowest evaluation
function value. So the implementation is a variation of BFS, we
just need to change Queue to PriorityQueue. Heuristic function
is nothing but a shortcut to solve a given problem when either
the exact solution is non-existing or it takes huge time to find
the same.
Key Features:
Uses a heuristic function to evaluate and prioritize nodes. This
heuristic is used to estimate the "cost" or "distance" from a
given node to the goal.
Follows a greedy strategy, focusing on expanding the most
promising node based on the heuristic
AILAB
Since BFS only uses the heuristic and doesn’t account for the
total path cost, it doesn’t guarantee finding the optimal solution
unless the heuristic is specifically designed to be admissible
and consistent
It prevents the system from becoming entangled in loops of
previously tested paths or nodes and helps avoid errors.
Branching Factor and Depth:
In BFS, the branching factor b refers to the average number of
child nodes
that each node has in a tree. The depth d refers to how many
levels deep the
traversal goes, starting from the root.
Nodes Traversed in a Tree with Branching Factor b and Depth d:
1. At depth 0 (the root), there is 1 node.
2. At depth 1, there are b nodes.
3. At depth 2, there are b^2 nodes.
4. And so on, up to depth d, where there are b^d nodes.
Time Complexity:
The time complexity of BFS depends on the total number of
nodes and
edges it must explore:
● Node Exploration:
AILAB
○ Each node is visited exactly once, contributing O(V), where V
is the number of nodes.
● Edge Exploration:
○ Each edge is traversed exactly once while exploring the
nodes, contributing O(E), where E is the number of edges. In
a tree, the number of edges is E = V - 1, meaning there are
fewer edges compared to general graphs.
● Worst Case for a Tree:
The number of nodes explored at each level grows
exponentially. For a tree with branching factor b and depth d,
the total number of nodes explored up to depth d is
approximately O(b^d).
Therefore, the time complexity of BFS in a tree-like structure is
O(b^d) where:
■ b is the branching factor (average number of children per
node)
■ d is the depth of the tree.
Space Complexity:
The space complexity of BFS is dominated by the space
required to store
the queue and the visited nodes. This results in the following:
● The number of nodes stored in the queue is at mostO(b^d)
where b is the branching factor and d is the depth of the tree or
graph.
● The number of nodes stored in the visited list is also O(b^d),
in the worst case.
Therefore, the space complexity of BFS is: O(b^d)
AILAB
Algorithm:
Best-First-Search(Tree root)
PriorityQueue pq;
[Link](root)
Until PriorityQueue is empty
u = [Link]
If u is the goal
Exit
Else
[Link](u->right)
[Link](u->left)
Code:
#include <iostream>
#include <queue>
#include <sstream>
using namespace std;
// TreeNode structure
struct TreeNode {
AILAB
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// Comparator to assign priority
struct Compare {
bool operator()(TreeNode* a, TreeNode* b) {
return a->val > b->val;
};
void bestFirstSearch(TreeNode* root) {
if (!root) return;
// Create a priority queue
priority_queue<TreeNode*, vector<TreeNode*>, Compare>
pq;
[Link](root);
while (![Link]()) {
TreeNode* current = [Link]();
[Link]();
AILAB
cout << current->val << " ";
// Push children of node into queue
if (current->left) [Link](current->left);
if (current->right) [Link](current->right);
TreeNode* createTree() {
cout << "Enter the tree nodes in level order (use -1 for null
nodes): ";
string input;
getline(cin, input);
stringstream ss(input);
int val;
ss >> val;
if (val == -1) return nullptr;
TreeNode* root = new TreeNode(val);
queue<TreeNode*> q;
[Link](root);
while (![Link]()) {
TreeNode* node = [Link]();
AILAB
[Link]();
if (ss >> val) {
if (val != -1) {
node->left = new TreeNode(val);
[Link](node->left);
if (ss >> val) {
if (val != -1) {
node->right = new TreeNode(val);
[Link](node->right);
return root;
int main() {
TreeNode* root = createTree();
cout << "Best First Search: ";
bestFirstSearch(root);
cout << endl;
AILAB
return 0;
2]A* Search vs Best First Search
Feature A* Algorithm Best-First Search
Finds a path (not
Objective Finds the shortest
necessarily the
path
shortest)
Cost Function Uses f(n) = g(n) + Uses only h(n)
h(n)
Optimality Guarantees shortest Not optimal
path
Completeness Complete Not complete
Heuristic Requires
Works with any
Requirement admissible
heuristic
heuristic
Combines Dijkstra
Algorithm Greedy search
and heuristic search
Type
Time O(b^d) O(b^m)
Complexity
Space O(b^d) O(b^m)
Complexity
Ideal for shortest Suitable for quick
Use Case
path applications solutions
Guarantees Guarantees optimal No guarantees
path
Code:
import heapq
AILAB
class Graph:
def init (self):
[Link] =
{} self.h =
{}
def add_edge(self, node, neighbour,
cost): if node not in [Link]:
[Link][node] = []
[Link][node].append((neighbour,
cost))
AILAB
def set_heuristic(self,
heuristics): self.h =
heuristics
# A* Search Algorithm
def a_star_search(graph, start, goal):
open_list = []
[Link](open_list, (0 + graph.h[start], 0,
start, [])) closed_list = set()
while open_list:
_, cost, node, path =
[Link](open_list) if node in
closed_list:
continue
path = path +
[node] if node ==
goal:
return path
closed_list.add(node)
for neighbour, move_cost in
[Link](node, []): if neighbour not in
closed_list:
[Link](open_list, (cost + move_cost +
graph.h[neighbour], cost + move_cost, neighbour, path))
return None
# Best First Search Algorithm
AILAB
def best_first_search(graph, start, goal):
open_list = []
AILAB
[Link](open_list, (graph.h[start], start, []))
closed_list = set()
while open_list:
_, node, path =
[Link](open_list) if node in
closed_list:
continue
path = path +
[node] if node ==
goal:
return path
closed_list.add(node)
for neighbour, _ in
[Link](node, []): if
neighbour not in closed_list:
[Link](open_list, (graph.h[neighbour],
neighbour,
path))
return None
graph = Graph()
graph.add_edge('A', 'B', 1)
graph.add_edge('A', 'C', 4)
graph.add_edge('B', 'D', 2)
graph.add_edge('C', 'D', 1)
graph.add_edge('D', 'E', 5)
graph.add_edge('B', 'F', 3)
graph.add_edge('D', 'G', 2)
AILAB
graph.add_edge('E', 'H', 3)
graph.add_edge('F', 'H', 4)
AILAB
graph.add_edge('G', 'H', 1)
graph.set_heuristic({'A': 10, 'B': 8, 'C': 7, 'D': 6, 'E': 4, 'F': 5, 'G':
3, 'H': 0})
print("A* Search Path:", a_star_search(graph, 'A', 'H'))
print("Best First Search Path:", best_first_search(graph,
'A', 'H'))
AILAB
Output:
Conclusion:
The implementation of A* Search and Best-First Search
highlights their differences in finding paths in a graph. Both
algorithms were tested on a graph with heuristic values, and
the paths from node 'A' to node 'H' were derived.
1. A Search:*
o Path: ['A', 'B', 'D', 'G', 'H']
o A* Search considers both the actual cost (g(n)) and
the heuristic (h(n)), which ensures that the shortest
path is found. In this case, the algorithm traversed
through the optimal nodes while maintaining a
balance between cost and heuristic evaluation. A*
guarantees the shortest path due to its combination
of Dijkstra's algorithm and heuristic-based search.
2. Best-First Search:
o Path: ['A', 'C', 'D', 'G', 'H']
o Best-First Search relies solely on the heuristic value
(h(n)), which leads it to choose nodes that appear
closest to the goal. Although it reached the goal, it
does not guarantee the shortest path since the
actual cost (g(n)) is not considered. This makes
Best-First Search faster but less reliable for optimal
paths.
Comparison:
● A* Search is optimal and guarantees the shortest path by
using the cost function f(n)=g(n)+h(n)
● Best-First Search is a greedy algorithm that
prioritizes nodes based on h(n), making it efficient
for quick solutions but not necessarily accurate in
finding the shortest path.
AILAB
In this example, the A* algorithm provided a shorter path
compared to the Best-First Search, demonstrating its
advantage when optimality is required. Best-First Search,
however, may still be suitable for scenarios where speed is
prioritized over precision.