Vijay Mama
Vijay Mama
Basic Information:
o Breadth-first search is the most common search strategy for traversing a tree or graph.
This algorithm searches breadthwise in a tree or graph, so it is called breadth-first
search.
o BFS algorithm starts searching from the root node of the tree and expands all successor
node at the current level before moving to nodes of next level.
o The breadth-first search algorithm is an example of a general-graph search algorithm.
o Breadth-first search implemented using FIFO queue data structure.
Advantages:
Disadvantages:
o It requires lots of memory since each level of the tree must be saved into memory to
expand the next level.
o BFS needs lots of time if the solution is far away from the root node.
Algorithm
Breadth-first search is the process of traversing each node of the graph, a standard BFS
algorithm traverses each vertex of the graph into two parts: 1) Visited 2) Not Visited. So,
the purpose of the algorithm is to visit all the vertex while avoiding cycles.
BFS starts from a node, then it checks all the nodes at distance one from the beginning
node, then it checks all the nodes at distance two, and so on. So as to recollect the nodes
to be visited, BFS uses a queue.
The steps of the algorithm work as follow:
1. Start by putting any one of the graph’s vertices at the back of the queue.
2. Now take the front item of the queue and add it to the visited list.
3. Create a list of that vertex's adjacent nodes. Add those which are not within the
visited list to the rear of the queue.
4. Keep continuing steps two and three till the queue is empty.
create a queue Q
while Q is non-empty
Implementation
graph = {
'5’: ['3','7'],
'3’: ['2', '4'],
'7’: ['8'],
'2’: [],
'4’: ['8'],
'8’: []
}
# Driver Code
print ("Following is the Breadth-First Search")
bfs (visited, graph, '5’) # function calling
Output
Following is the Breadth-First Search
537248
Complexity Analysis
The time complexity of the Breadth first Search algorithm is in the form of O(V+E), where V
is the representation of the number of nodes and E is the number of edges. Also, the space
complexity of the BFS algorithm is O(V).
Completeness: BFS is complete, which means if the shallowest goal node is at some finite
depth, then BFS will find a solution.
Optimality: BFS is optimal if path cost is a non-decreasing function of the depth of the node.
1. In GPS navigation, it helps in finding the shortest path available from one point to
another.
2. In pathfinding algorithms
3. Cycle detection in an undirected graph
4. In minimum spanning tree
5. To build index by search index
Lab Program 2 : Implementation of Depth First Search Algorithm
Basic Information:
Depth-first search isa recursive algorithm for traversing a tree or graph data structure.
It is called the depth-first search because it starts from the root node and follows each
path to its greatest depth node before moving to the next path.
Advantage:
o DFS requires very less memory as it only needs to store a stack of the nodes on the path
from root node to the current node.
o It takes less time to reach to the goal node than BFS algorithm (if it traverses in the
right path).
Disadvantage:
o There is the possibility that many states keep re-occurring, and there is no guarantee of
finding the solution.
o DFS algorithm goes for deep down searching and sometime it may go to the infinite
loop.
Algorithm
The recursive method of the Depth-First Search algorithm is implemented using stack. A
standard Depth-First Search implementation puts every vertex of the graph into one in all 2
categories: 1) Visited 2) Not Visited. The only purpose of this algorithm is to visit all the
vertex of the graph avoiding cycles.
1. We will start by putting any one of the graph's vertex on top of the stack.
2. After that take the top item of the stack and add it to the visited list of the vertex.
3. Next, create a list of that adjacent node of the vertex. Add the ones which aren't in the
visited list of vertexes to the top of the stack.
4. Lastly, keep repeating steps 2 and 3 until the stack is empty.
DFS pseudocode
The pseudocode for Depth-First Search in python goes as below: In the init() function, notice
that we run the DFS function on every node because many times, a graph may contain two
different disconnected part and therefore to make sure that we have visited every vertex, we
can also run the DFS algorithm at every node.
DFS (G, u)
u.visited = true
if v.visited == false
DFS(G,v)
init() {
For each u ∈ G
u.visited = false
For each u ∈ G
DFS(G, u)
Implementation
# Using a Python dictionary to act as an adjacency list
graph = {
'5' : ['3','7'],
'3' : ['2', '4'],
'7' : ['8'],
'2' : [],
'4' : ['8'],
'8' : []
}
visited = set () # Set to keep track of visited nodes of graph.
# Driver Code
print ("Following is the Depth-First Search")
dfs(visited, graph, '5')
Output
The output of the above code is as follow:
Time Complexity
The time complexity of the Depth-First Search algorithm is represented within the sort
of O(V + E), where V is that the number of nodes and E is that the number of edges.
The space complexity of the algorithm is O(V).
Optimal: Depth-limited search can be viewed as a special case of DFS, and it is also not
optimal even if ℓ>d.
Applications
Depth-First Search Algorithm has a wide range of applications for practical purposes. Some
of them are as discussed below:
Basic Information
o Standard failure value: It indicates that problem does not have any solution.
o Cutoff failure value: It defines no solution for the problem within a given depth limit.
Advantages:
Disadvantages:
Algorithm
Args:
graph: A dictionary representing the graph, where keys are nodes and values are
lists of adjacent nodes.
Returns:
The path from start to goal if found within the depth limit, otherwise None.
"""
if depth == depth_limit:
return None
if new_path:
return new_path
return None
Implementation
# Example usage
graph = {
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
start_node = 'A'
goal_node = 'F'
depth_limit = 2
else:
Explanation:
1. dls(graph, start, goal, depth_limit):
• This is the main function that initiates the DLS algorithm.
• It calls the recursive_dls function to perform the actual search.
• It returns the path if found, otherwise None.
2. recursive_dls(node, path, depth):
• This function recursively explores the graph.
• Base Case 1: If the current node is the goal, it returns the path to the goal.
• Base Case 2: If the current depth reaches the depth_limit, it returns None to
indicate a cutoff.
• Recursive Case: It iterates through each neighbor of the current node.
• It calls itself recursively with the neighbor, updated path, and
incremented depth.
• If the recursive call returns a non-None value (i.e., a path), it means a
path to the goal is found, so it returns that path.
• If no path is found from any neighbor, it returns None.
Example Usage:
• The provided example defines a simple graph, a starting node, a goal node, and a
depth limit.
• It calls the dls function and prints the result.
Key Points:
• DLS is a variation of Depth-First Search (DFS) that limits the exploration to a
specified depth.
• It is useful for searching in graphs with large branching factors or infinite depth, as it
prevents the search from getting stuck in endless paths.
• However, it may not find the optimal solution if the goal is deeper than the specified
depth limit.
Lab Program 4: Implementation of Uniform cost search
Basic Information
Uniform-cost search is a searching algorithm used for traversing a weighted tree or graph. This
algorithm comes into play when a different cost is available for each edge. The primary goal
of the uniform-cost search is to find a path to the goal node which has the lowest cumulative
cost. Uniform-cost search expands nodes according to their path costs form the root node. It
can be used to solve any graph/tree where the optimal cost is in demand. A uniform-cost search
algorithm is implemented by the priority queue. It gives maximum priority to the lowest
cumulative cost. Uniform cost search is equivalent to BFS algorithm if the path cost of all edges
is the same.
Advantages:
o Uniform cost search is optimal because at every state the path with the least cost is
chosen.
Disadvantages:
o It does not care about the number of steps involve in searching and only concerned
about path cost. Due to which this algorithm may be stuck in an infinite loop.
Completeness: Uniform-cost search is complete, such as if there is a solution, UCS will find
it.
Optimal: Uniform-cost search is always optimal as it only selects a path with the lowest path
cost.
Explanation:
1. Initialize:
• Create a priority queue frontier and push the starting node with cost 0 and an
empty path.
•Create a set explored to keep track of visited nodes.
2. Explore:
• While the frontier is not empty, pop the node with the lowest cost from the
priority queue.
• If the current node is the goal, return the path and its cost.
• If the current node is already explored, skip it.
• Mark the current node as explored.
• For each neighbor of the current node, calculate the new cost (current cost + edge
cost).
• Push the neighbor, new cost, and updated path to the priority queue.
3. Return:
• If the goal is reached, return the path and cost.
• If the frontier is empty and the goal is not found, return None, indicating no path
exists.
Key points:
• Priority queue:
Uses a heap to efficiently get the node with the lowest cost.
• Explored set:
Prevents revisiting already explored nodes, ensuring optimality and avoiding infinite
loops.
• Graph representation:
Uses a dictionary where keys are nodes, and values are dictionaries of neighbors and
their corresponding edge costs.
• Path reconstruction:
Stores the path taken to reach each node, allowing easy backtracking to find the
complete path from start to goal.
This implementation assumes:
• All edge costs are non-negative.
• The graph is represented as an adjacency list.
• The goal node is reachable from the start node.
Algorithm
import heapq
Args:
graph: A dictionary representing the graph, where keys are nodes and values are
dictionaries of neighbors and their costs.
Returns:
A tuple containing the shortest path (list of nodes) and the total cost.
Implementation
while frontier:
if current == goal:
if current in explored:
continue
explored.add(current)
Implementation
# Example usage:
graph = {
start_node = 'A'
goal_node = 'G'
if path:
else:
Basic Information
The iterative deepening algorithm is a combination of DFS and BFS algorithms. This search
algorithm finds out the best depth limit and does it by gradually increasing the limit until a goal
is found.
This algorithm performs depth-first search up to a certain "depth limit", and it keeps increasing
the depth limit after each iteration until the goal node is found.
This Search algorithm combines the benefits of Breadth-first search's fast search and depth-
first search's memory efficiency.
The iterative search algorithm is useful uninformed search when search space is large, and
depth of goal node is unknown.
Advantages:
o It combines the benefits of BFS and DFS search algorithm in terms of fast search and
memory efficiency.
Disadvantages:
o The main drawback of IDDFS is that it repeats all the work of the previous phase.
Explanation:
1. dfs(graph, start, goal, depth_limit):
• Performs Depth-First Search with a given depth limit.
• Uses a stack to maintain the search frontier.
• If the goal is found, returns the path to it.
• If the depth limit is reached, backtracks and explores other branches.
2. iterative_deepening_search(graph, start, goal, max_depth):
• Iteratively increases the depth limit for DFS.
• Starts with a depth limit of 0 and increases it until the goal is found or the
maximum depth is reached.
• Calls the dfs function for each depth limit.
Key points:
• Space efficiency:
IDDFS combines the space efficiency of DFS with the completeness of BFS.
• Optimality:
IDDFS guarantees finding the shortest path in a tree or an unweighted graph, like BFS.
Implementation:
It uses a simple loop to increase the depth limit and calls the DFS function repeatedly.
• Suitable for:
Problems where the solution is not too deep in the search tree and memory usage is a
concern.
Algorithm
from collections import deque
while stack:
if current == goal:
return path
if result:
return result
return None
Implementation
graph = {
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
start_node = 'A'
goal_node = 'F'
max_depth_limit = 3
if path:
else:
1. For the given tree apply the BFS algorithm to calculate the time and space
complexity required in order to reach node K. Compare the same with DFS.
2. For the given tree apply the DFS algorithm to calculate the time and space
complexity required in order to reach node G. Compare the same with BFS.
3. For the given tree apply the DLS algorithm to calculate the time and space
complexity required in order to reach node J.
4. For the given tree apply the UCS algorithm to calculate the time and space
complexity required in order to reach node G.
5. For the given tree apply the Iterative Deepening algorithm to calculate the
time and space complexity required in order to reach node G.
You have been given a Tree consisting of N nodes. A tree is a fully-connected graph
consisting of N nodes and 𝑁−1 edges. The nodes in this tree are indexed from 1 to N.
Consider node indexed 1 to be the root node of this tree. The root node lies at level one in the
tree. You shall be given the tree and a single integer x . You need to find out the number of
nodes lying on level x.
Input Format
The first line consists of a single integer N denoting the number of nodes
in the tree. Each of the next 𝑛−1 lines consists of 2 integers a and b denoting an
undirected edge between node a and node b. The next line consists of a single
integer x.
Output Format
You need to print a single integer denoting the number of nodes on level x.
A heuristic function helps the search algorithm choose a branch from the ones that are
available. It helps with the decision process by using some extra knowledge about the search
space.
Let’s use a simple analogy. If you went to a supermarket with many check-out counters, you
would try to go to the one with the least number of people waiting. This is a heuristic that
reduces your wait time.
Example
While playing tic tac toe, there are many placements from which one player can start, and
each placement has its own chances of winning. However, if the first player starts from the
centremost area, they have the most chances of winning. Hence, chances of winning can be a
heuristic.
Heuristics function: Heuristic is a function which is used in Informed Search, and it finds the
most promising path. It takes the current state of the agent as its input and produces the
estimation of how close agent is from the goal. The heuristic method, however, might not
always give the best solution, but it guaranteed to find a good solution in reasonable time.
Heuristic function estimates how close a state is to the goal. It is represented by h(n), and it
calculates the cost of an optimal path between the pair of states. The value of the heuristic
function is always positive.
Here h(n) is heuristic cost, and h*(n) is the estimated cost. Hence heuristic cost should be less
than or equal to the estimated cost.
Basic Information
Hill Climbing is a technique to solve certain optimization problems. In this technique, we start
with a sub-optimal solution and the solution is improved repeatedly until some condition is
maximized.
Hill Climbing technique is mainly used for solving computationally hard problems. It looks
only at the current state and immediate future state. Hence, this technique is memory efficient
as it does not maintain a search tree.
Algorithm
Hill Climbing technique can be used to solve many problems, where the current state allows
for an accurate evaluation function, such as Network-Flow, Travelling Salesman problem, 8-
Queens problem, Integrated Circuit design, etc.
Hill Climbing is used in inductive learning methods too. This technique is used in robotics for
coordination among multiple robots in a team. There are many other problems where this
technique is used.
Implementation
This technique can be applied to solve the travelling salesman problem. First an initial solution
is determined that visits all the cities exactly once. Hence, this initial solution is not optimal in
most of the cases. Even this solution can be very poor. The Hill Climbing algorithm starts with
such an initial solution and makes improvements to it in an iterative way. Eventually, a much
shorter route is likely to be obtained.
import random
# Distance matrix representing distances between cities
# Replace this with the actual distance matrix for your problem
distance_matrix = [
[0, 10, 15, 20],
[10, 0, 35, 25],
[15, 35, 0, 30],
[20, 25, 30, 0]
]
def total_distance(path):
# Calculate the total distance traveled in the given path
total = 0
for i in range(len(path) - 1):
total += distance_matrix[path[i]][path[i+1]]
total += distance_matrix[path[-1]][path[0]] # Return to starting city
return total
def hill_climbing_tsp(num_cities, max_iterations=10000):
current_path = list(range(num_cities)) # Initial solution, visiting cities in order
current_distance = total_distance(current_path)
for _ in range(max_iterations):
# Generate a neighboring solution by swapping two random cities
neighbor_path = current_path.copy()
i, j = random.sample(range(num_cities), 2)
neighbor_path[i], neighbor_path[j] = neighbor_path[j], neighbor_path[i]
neighbor_distance = total_distance(neighbor_path)
# If the neighbor solution is better, move to it
if neighbor_distance < current_distance:
current_path = neighbor_path
current_distance = neighbor_distance
return current_path
def main():
num_cities = 4 # Number of cities in the TSP
solution = hill_climbing_tsp(num_cities)
print("Optimal path:", solution)
print("Total distance:", total_distance(solution))
if __name__ == "__main__":
main()
Output:
Optimal path: [1, 0, 2, 3]
Total distance: 80
Basic Information
The best first search algorithm is a version of the depth first search using heuristics. Each
node is evaluated with respect to its distance from the goal. Whichever node is closest to the
final state is explored first. If the path fails to reach the goal, the algorithm backtracks and
chooses some other node that didn’t seem to be the best before.
Algorithm
Implementation
currentNode = openList[0]
currentIndex = 0
# Finding the node with the least 'h' value
for i in range(len(openList)):
if(h[openList[i].node] < h[currentNode.node]):
currentNode = openList[i]
currentIndex = i
for x in closeList:
if(x.node == node.v):
continue
openList.append(pathNode(node.v, currentNode))
return []
# Driver Code
""" Making the following graph
src = 0
/ | \
/ | \
1 2 3
/\ | /\
/ \ | / \
4 5 6 7 8
/
/
dest = 9
"""
# The total number of vertices.
V = 10
## Initializing the adjacency list
for i in range(V):
adj.append([])
addEdge(0, 1, 2)
addEdge(0, 2, 1)
addEdge(0, 3, 10)
addEdge(1, 4, 3)
addEdge(1, 5, 2)
addEdge(2, 6, 9)
addEdge(3, 7, 5)
addEdge(3, 8, 2)
addEdge(7, 9, 5)
print(path[(len(path)-1)])
Output
0 --> 3 --> 7 --> 9
Complexity Analysis
• Time Complexity - In the worst situation, we may require to traverse through all the
edges to reach the destination. Hence in the worst case, the time complexity will
be 𝑂(𝐸log𝑉)O(ElogV).
• Space Complexity - In the worst scenario, we may have all the vertices (nodes)
in openList which will make the worst case space complexity 𝑂(𝑉)O(V).
Lab Program 3: Implementation of A* Algorithm
Basic Information
The previous algorithm we discussed only considered the distance of the nodes from the goal.
A* uses the path of reaching to the current node from the starting node, and the path of
reaching the goal from the current node. So, the heuristic function becomes:
f(n) = g(n) + h(n)
where:
f(n): cost of the optimal path from start to goal
g(n): shortest path of the current node from the start
h(n): shortest path of the goal from the current node
Note: The actual distance from any node to the goal may be greater than h(n), since h(n) is
the shortest distance. This distance can be the straight line distance from the current node to
the goal. A path with the shortest distance may or may not exist.
Algorithm
Complexity Analysis
• Time Complexity - The time it takes for the A* search algorithm to find a solution
depends on how many options each step has (the branching factor) and how deep the
solution is. We express this time complexity as 𝑂(𝑏𝑑)O(bd), where b is the
branching factor and d is the depth of the solution.
• Space Complexity - In the worst scenario, we may have all the vertices (nodes)
in openList which will make the worst-case space complexity be 𝑂(𝑏𝑑)O(bd).
Practice Problems
1) You are given a partially filled 9x9 Sudoku board. Your task is to implement a
Sudoku solver using the hill climbing algorithm.
Sudoku Rules
• Each of the numbers from 1 to 9 must appear exactly once in each row.
• Each of the numbers from 1 to 9 must appear exactly once in each
column.
• Each of the numbers from 1 to 9 must appear exactly once in each 3x3
sub-grid.
Input
The input will be a 9x9 grid of integers where 0 represents an empty cell.
Output
Example
Input:
[
[5, 3, 0, 0, 7, 0, 0, 0, 0],
[6, 0, 0, 1, 9, 5, 0, 0, 0],
[0, 9, 8, 0, 0, 0, 0, 6, 0],
[8, 0, 0, 0, 6, 0, 0, 0, 3],
[4, 0, 0, 8, 0, 3, 0, 0, 1],
[7, 0, 0, 0, 2, 0, 0, 0, 6],
[0, 6, 0, 0, 0, 0, 2, 8, 0],
[0, 0, 0, 4, 1, 9, 0, 0, 5],
[0, 0, 0, 0, 8, 0, 0, 7, 9]
]
Output:
[
[5, 3, 4, 6, 7, 8, 9, 1, 2],
[6, 7, 2, 1, 9, 5, 3, 4, 8],
[1, 9, 8, 3, 4, 2, 5, 6, 7],
[8, 5, 9, 7, 6, 1, 4, 2, 3],
[4, 2, 6, 8, 5, 3, 7, 9, 1],
[7, 1, 3, 9, 2, 4, 8, 5, 6],
[9, 6, 1, 5, 3, 7, 2, 8, 4],
[2, 8, 7, 4, 1, 9, 6, 3, 5],
[3, 4, 5, 2, 8, 6, 1, 7, 9]
]
2) Write a program and execute to find the shortest path in an unweighted grid
using the Best-first search algorithm. You are given a 2D grid where each cell
is either an open cell (0) or a blocked cell (1). The task is to find the shortest
path from the top-left cell (0,0) to the bottom-right cell (n-1, m-1). The path can
only move in four possible directions: up, down, left, and right. If there is no
path, return -1.
Input:
The first line contains two integers n and m representing the number of rows
and columns in the grid.
The next n lines each contain m integers representing the grid cells (either 0 for
open or 1 for blocked).
Output:
Print the length of the shortest path from the top-left to the bottom-right cell. If
no such path exists, print -1.
Constraints:
▪ 1 <= n, m <= 100
▪ The grid contains only 0 and 1
Example:
Input:
0 0 0
1 1 0
0 0 0
Output:
4
3) Write and execute a program to find the shortest path in a maze using the A*
algorithm. You are given a maze represented as a 2D grid where each cell is either
passable (0 for open cell) or impassable (1 for wall). The task is to find the shortest
path from the top-left cell (0,0) to the bottom-right cell (n-1, m-1) through open cells
only. The path can move in four possible directions: up, down, left, and right. If there
is no path, return -1.
Input:
1. The first line contains two integers n and m representing the number of rows
and columns in the maze.
2. The next n lines each contain m integers representing the cells of the maze (0
for open cell, 1 for wall).
Output:
• Print the total cost of the shortest path from the top-left to the bottom-right
cell. If no such path exists, print -1.
Constraints:
Example:
Input:
0 1 0
0 0 0
1 1 0
Output:
4
with a total cost of 4.