Practical No1 3
Practical No1 3
Aim:- Implement Depth First Search algorithm and Breadth First Search algorithm. Use an
undirected graph and develop a recursive algorithm for searching all the vertices of a graph or tree
data structure.
Theory:-
Depth First Search
Depth first search (DFS) algorithm starts with the initial node of the graph G, and then goes to
deeper and deeper until we find the goal node or the node which has no children. The algorithm, then
backtracks from the dead end towards the most recent node that is yet to be completely unexplored.
The data structure which is being used in DFS is stack. In DFS, the edges that leads to an unvisited
node are called discovery edges while the edges that leads to an already visited node are called block
edges.
Depth first Search or Depth first traversal is a recursive algorithm for searching all the vertices of a
graph or tree data structure. Traversal means visiting all the nodes of a graph.
Next, we visit the element at the top of stack i.e. 1 and go to its adjacent nodes. Since 0 has already
been visited, we visit 2 instead.
Vertex 2 has an unvisited adjacent vertex in 4, so we add that to the top of the stack and visit it.
Vertex 2 has an unvisited adjacent vertex in 4, so we add that to the top of the stack and visit it.
Vertex 2 has an unvisited adjacent vertex in 4, so we add that to the top of the stack and visit it.
After we visit the last element 3, it doesn’t have any unvisited adjacent nodes, so we have completed
the Depth First Traversal of the graph.
init() {
for each u ∈ G
u.visited = false
for each u ∈ G
DFS(G, u)
}
BFS Algorithm
A standard BFS implementation puts each vertex of the graph into one of two categories:
1. Visited
2. Not Visited
The purpose of the algorithm is to mark each vertex as visited while avoiding cycles.
The algorithm works as follows:
1. Start by putting any one of the graph’s vertices at the back of a queue.
2. 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 the ones which aren’t in the visited list to the
back of the queue.
4. Keep repeating steps 2 and 3 until the queue is empty.
BFS Example
Let’s see how the Breadth First Search algorithm works with an example. We use an undirected
graph with 5 vertices.
BFS pseudocode
Create a queue Q
mark v as visited and put v into Q
while Q is non empty
remove the head u of Q
mark and enqueue all (unvisited) neighbours of u
BFS Algorithm Complexity
The time complexity of the BFS algorithm is represented in the form of O(V + E), where V is the
number of nodes and E is the number of edges.
The space complexity of the algorithm is O(V).
Conclusion:-
Thus, we have implemented Depth First Search algorithm and Breadth First Search algorithm. Using
undirected graph and developed a recursive algorithm for searching all the vertices of a graph.
Implement depth first search algorithm and Breadth First Search algorithm. Use an
undirected graph and develop a recursive algorithm for searching all the vertices of a graph
or tree data structure.
Program:
from collections import defaultdict, deque
class Graph:
def __init__(self):
# Default dictionary to store graph
self.graph = defaultdict(list)
while queue:
vertex = queue.popleft() # Pop the front of the queue
print(vertex, end=' ')
Output:
Depth First Search (starting from vertex 0):
0 1 3 7 4 8 2 5 9 6 10
Breadth First Search (starting from vertex 0):
0 1 2 3 4 5 6 7 8 9 10
Experiment No: 2
Theory:-
A* Search
➢ A* Search is the most commonly know form of best-first search. It uses heuristic function
h(n), and cost to reach the node n from the start state g(n). It has combined features of UCS
and greedy best first search, by which it solve the problem efficiently.
➢ A* search algorithm finds the shortest path through the search space using the heuristic
function. This search algorithm expands less search tree and provides optimal result
faster.
➢ All graphs have different nodes or points which the algorithm has to take, to reach the final
node. The paths between these nodes all have a numerical value, which is considered as the
weight of the path. The total of all path’s transverse gives you the cost of that route.
➢ Initially, the Algorithm calculates the cost to all its immediate neighboring nodes,n, and
chooses the one incurring the least cost. This process repeats until no new nodes can be
chosen and all paths have been traversed. Then, you should consider the best path among
them. If f(n) represents the final cost, then it can be denoted as:
f(n) = g(n) + h(n), where:
g(n) = cost of traversing from one to another. Thus will vary from node to node.
h(n) = heuristic approximation of the node’s value. This is not a real value but an
approximation cost.
Consider the weighted graph depicted above, which contains nodes and the distance between them.
Let's say you start from A and have to go to D.
Now, since the start is at the source A, which will have some initial heuristic value. Hence, the results
are f(A) = g(A) + h(A)
f(A) = 0 + 6 = 6
Next, take the path to other neighbouring vertices :
f(A-B) = 1 + 4
f(A-C) = 5 + 2
Now take the path to the destination from these nodes, and calculate the weights:
f(A-B-D) = (1+ 7) + 0
f(A-C-D) = (5 + 10) + 0
It is clear that node B gives you the best path, so that is the node you need to take to reach the
destination.
Algorithm of A* Search:
Step1: Place the starting node in the OPEN list.
Step 2: Check if the OPEN list is empty or not, if the list is empty then return failure and stops.
Step 3: Select the node from the OPEN list which has the smallest value of evaluation function
(g+h), if node n is goal node then return success and stop, otherwise
Step 4: Expand node n and generate all of its successors, and put n into the closed list. For each
successor n', check whether n' is already in the OPEN or CLOSED list, if not then compute
evaluation function for n' and place into Open list.
Step 5: Else if node n' is already in OPEN and CLOSED, then it should be attached to the back
pointer which reflects the lowest g(n') value.
Step 6: Return to Step 2
Advantages:
1. A* Search algorithm is the best algorithm than other search algorithms.
2. A* search algorithm is optimal and complete.
3. This algorithm can solve very complex problems.
Disadvantages:
1. It does not always produce the shortest path as it mostly based on heuristics and
approximation.
2. A* search algorithm has some complexity issues.
3. The main drawback of A* is memory requirement as it keeps all generated nodes in the
memory, so it is not practical for various large-scale problems.
Conclusion:-
In this Experiment, an introduction to the powerful search algorithm, we learned about everything
about the algorithm and saw the basic concept behind it. Also implement the algorithm in
python/Java.
Practical No. 2
Program:
import heapq
class AStar:
def __init__(self, grid, start, goal):
self.grid = grid # 2D grid where 0 = walkable, 1 = blocked
self.start = start # Start position (x, y)
self.goal = goal # Goal position (x, y)
self.rows = len(grid)
self.cols = len(grid[0])
def a_star_search(self):
# Priority queue to store (f_score, node)
open_list = []
heapq.heappush(open_list, (0, self.start))
while open_list:
current = heapq.heappop(open_list)[1]
Output:
Path from start to goal: [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (4, 1), (4,
0)]
Experiment No: 3
Aim:- Implement Greedy Search Algorithm for Prim’s Minimal Spanning Tree Algorithm.
Theory:-
Minimum Spanning Tree
A minimum spanning tree can be defined as the spanning tree in which the sum of the weights of the
edge is minimum. The weight of the spanning tree is the sum of the weights given to the edges of the
spanning tree. In the real world, this weight can be considered as the distance, traffic load,
congestion, or any random value.
Example of minimum spanning tree
Let's understand the minimum spanning tree with the help of an example.
The sum of the edges of the above graph is 16. Now, some of the possible spanning trees created
from the above graph are –
So, the minimum spanning tree that is selected from the above spanning trees for the given weighted
graph is –
Applications of Minimum Spanning Tree
The applications of the minimum spanning tree are given as follows -
o Minimum spanning tree can be used to design water-supply networks, telecommunication
networks, and electrical grids.
o Kruskal's Algorithm
Prim’s Algorithm
It is a greedy algorithm that starts with an empty spanning tree. It is used to find the minimum
spanning tree from the graph. This algorithm finds the subset of edges that includes every vertex of
the graph such that the sum of the weights of the edges can be minimized.
Prim's algorithm starts with the single node and explores all the adjacent nodes with all the
connecting edges at every step. The edges with the minimal weights causing no cycles in the graph
got selected.
o Now, we have to find all the edges that connect the tree in the above step with the new
vertices. From the edges found, select the minimum edge and add it to the tree.
Step 1 – First, we have to choose a vertex from the above graph. Let’s choose B.
Step 2 - Now, we have to choose and add the shortest edge from vertex B. There are two edges from
vertex B that are B to C with weight 10 and edge B to D with weight 4. Among the edges, the edge
BD has the minimum weight. So, add it to the MST.
Step 3 - Now, again, choose the edge with the minimum weight among all the other edges. In this
case, the edges DE and CD are such edges. Add them to MST and explore the adjacent of C, i.e., E
and A. So, select the edge DE and add it to the MST.
Step 4 - Now, select the edge CD, and add it to the MST.
Step 5 - Now, choose the edge CA. Here, we cannot select the edge CE as it would create a cycle to
the graph. So, choose the edge CA and add it to the MST.
So, the graph produced in step 5 is the minimum spanning tree of the given graph. The cost of the
MST is given below -
Cost of MST = 4 + 2 + 1 + 3 = 10 units.
Algorithm
Step 1: Select a starting vertex
Step 2: Repeat Steps 3 and 4 until there are fringe vertices
Step 3: Select an edge 'e' connecting the tree vertex and fringe vertex that has minimum weight
Step 4: Add the selected edge and the vertex to the minimum spanning tree T
[END OF LOOP]
Step 5: EXIT
Data structure used for the minimum edge weight Time Complexity
Conclusion:-
Prim’s algorithm can be simply implemented by using the adjacency matrix or adjacency list graph
representation, and to add the edge with the minimum weight requires the linearly searching of an
array of weights. It requires O(|V|2) running time. It can be improved further by using the
implementation of heap to find the minimum weight edges in the inner loop of the algorithm.
Practical No. 3
Program:
import math
MAX_PLAYER = 'X'
MIN_PLAYER = 'O'
EMPTY = '_'
class TicTacToe:
def __init__(self):
self.board = [
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY]
]
return 0
if not self.is_moves_left(board):
return 0
if is_maximizing:
best = -math.inf
for i in range(3):
for j in range(3):
if board[i][j] == EMPTY:
board[i][j] = MAX_PLAYER
best = max(best, self.minimax(board, depth + 1,
False, alpha, beta))
board[i][j] = EMPTY
alpha = max(alpha, best)
if beta <= alpha:
break
return best
else:
best = math.inf
for i in range(3):
for j in range(3):
if board[i][j] == EMPTY:
board[i][j] = MIN_PLAYER
best = min(best, self.minimax(board, depth + 1,
True, alpha, beta))
board[i][j] = EMPTY
beta = min(beta, best)
if beta <= alpha:
break
return best
def find_best_move(self):
best_val = -math.inf
best_move = (-1, -1)
for i in range(3):
for j in range(3):
if self.board[i][j] == EMPTY:
self.board[i][j] = MAX_PLAYER
move_val = self.minimax(self.board, 0, False, -math.inf,
math.inf)
self.board[i][j] = EMPTY
def print_board(self):
for row in self.board:
print(" | ".join(row))
print("-" * 9)
game = TicTacToe()
game.board = [
['X', '_', 'O'],
['O', 'X', '_'],
['_', 'O', '_']
]
print("Current board:")
game.print_board()
best_move = game.find_best_move()
print(f"\nThe best move for 'X' is: {best_move}")
Output:
Current board:
X | _ | O
---------
O | X | _
---------
_ | O | _
---------