AI lab manual Updated
AI lab manual Updated
, CSE [AI&ML]
Evaluation Plan
11 Week 11: Design and Development of expert system for the specified 18
domain
12 Week 12: Design and Development of expert system for the specified 18
domain
Lab 1 – Python Basics-1 (Data Structures)
Tuples
Tuples are a built-in data structure in Python that are similar to lists, but with some key differences. Tuples are
immutable, meaning their values cannot be changed once they are created. They are also usually used to store
related values, as they allow you to group data together in a single object.
# Creating a tuple
my_tuple = (1, 2, 3, 4)
# Slicing a tuple
print(my_tuple[1:3]) # Output: (2, 3)
# Tuple concatenation
new_tuple = my_tuple + (5, 6)
print(new_tuple) # Output: (1, 2, 3, 4, 5, 6)
# Tuple repetition
print(my_tuple * 3) # Output: (1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4)
List
Lists are a built-in data structure in Python that are used to store an ordered collection of items. Lists are
mutable, meaning their values can be changed after they are created. They can contain elements of different
types, including other lists.
# Creating a list
my_list = [1, 2, 3, 4]
# Slicing a list
print(my_list[1:3]) # Output: [2, 3]
# List concatenation
new_list = my_list + [5, 6]
print(new_list) # Output: [1, 2, 3, 4, 5, 6]
# List repetition
print(my_list * 3) # Output: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]
Data Dictionary
A data dictionary is a collection of descriptions of the variables in a dataset, including their names,
types, and other characteristics. In Python, you can use a dictionary to store this information.
data_dictionary = {
"variable_1": {"type": "string", "description": "Name of the person"},
"variable_2": {"type": "integer", "description": "Age of the person"},
"variable_3": {"type": "float", "description": "Height of the person in meters"}
}
def delete_variable(variable_name):
if variable_name in data_dictionary:
del data_dictionary[variable_name]
else:
print("Error: Variable not found in data dictionary.")
def get_variable_info(variable_name):
if variable_name in data_dictionary:
return data_dictionary[variable_name]
else:
print("Error: Variable not found in data dictionary.")
return None
Python code for stack implentation.
1. The Stack class initializes an empty list self.items to store the stack items.
2. The push method adds an item to the end of the list self.items, which represents the top of the stack.
3. The pop method removes and returns the last item from the list self.items. If the list is empty, it returns
None.
4. The peek method returns the last item from the list self.items without removing it. If the list is empty, it
returns None.
5. The is_empty method returns True if the list self.items is empty, and False otherwise.
class Stack:
def __init__(self):
self.items = []
def pop(self):
return self.items.pop() if self.items else None
def peek(self):
return self.items[-1] if self.items else None
def is_empty(self):
return not self.items
Implementation of a queue in Python using a list
class Queue:
def __init__(self):
self.queue = []
def dequeue(self):
if not self.is_empty():
return self.queue.pop(0)
def is_empty(self):
return len(self.queue) == 0
def size(self):
return len(self.queue)
Lab Questions:
Q1. Implementation of a queue in Python using two stacks.
Description: A queue can be implemented using two stacks in Python by following the below steps:
1. Use two stacks, stack1 and stack2, to implement the enqueue and dequeue operations.
2. In the enqueue operation, push the new element onto stack1.
3. In the dequeue operation, if stack2 is empty, transfer all elements from stack1 to stack2. The element
at the top of stack2 is the first element that was pushed onto stack1 and thus represents the front of the
queue. Pop this element from stack2 to return it as the dequeued element.
Q2. Implement the following graph using python. Print the adjacency list and adjacency matrix.
[A graph is a data structure that consists of vertices that are connected via edges.]
Q3. Create two list X and Y with some set of numerical values. Compute Euclidean distance for corresponding
values in X and Y. Store the distance values in a separate list and sort them using Bubble sort algorithm.
Q4. Implement the given binary search tree using Python and print the pre-order, in-order, and post-order tree
traversal.
Expected Output:
Lab 2 – Python Basics2 (Object Oriented Programming Concepts)
Class
The class can be defined as a collection of objects. It is a logical entity that has some specific attributes and
methods. For example: if you have an employee class, then it should contain an attribute and method, i.e. an
email id, name, age, salary, etc.
Syntax
class ClassName:
<statement-1>
.
.
<statement-N>
Object
The object is an entity that has state and behavior. It may be any real-world object like the mouse, keyboard,
chair, table, pen, etc.
Everything in Python is an object, and almost everything has attributes and methods. All functions have a built-
in attribute __doc__, which returns the docstring defined in the function source code.
When we define a class, it needs to create an object to allocate the memory. Consider the following example.
Example:
class car:
self.modelname = modelname
self.year = year
def display(self):
print(self.modelname,self.year)
c1 = car("Toyota", 2016)
c1.display()
Instantiation is nothing but creating a new object/instance of a class. Let’s create the object of the above class
we defined-
obj1 = Car()
And it’s done! Note that you can change the object name according to your choice.
print(obj1)
Since our class was empty, it returns the address where the object is stored i.e 0x7fc5e677b6d8
You also need to understand the class constructor before moving forward.
Class constructor
Until now we have an empty class Car, time to fill up our class with the properties of the car. The job of the
class constructor is to assign the values to the data members of the class when an object of the class is created.
There can be various properties of a car such as its name, color, model, brand name, engine power, weight,
price, etc. We’ll choose only a few for understanding purposes.
class Car:
def __init__(self, name, color):
self.name = name
self.color = color
So, the properties of the car or any other object must be inside a method that we call __init__( ). This __init__()
method is also known as the constructor method. We call a constructor method whenever an object of the class
is constructed.
Now let’s talk about the parameter of the __init__() method. So, the first parameter of this method has to be
self. Then only will the rest of the parameters come.
This will create new attributes namely name and color and then assign the value of the respective parameters to
them. The “self” keyword represents the instance of the class. By using the “self” keyword we can access the
attributes and methods of the class. It is useful in method definitions and in variable initialization. The “self” is
explicitly used every time we define a method.
Note: You can create attributes outside of this __init__() method also. But those attributes will be universal to
the whole class and you will have to assign the value to them.
Suppose all the cars in your showroom are Sedan and instead of specifying it again and again you can fix the
value of car_type as Sedan by creating an attribute outside the __init__().
class Car:
car_type = "Sedan" #class attribute
def __init__(self, name, color):
self.name = name #instance attribute
self.color = color #instance attribute
Here, Instance attributes refer to the attributes inside the constructor method i.e self.name and self.color. And,
Class attributes refer to the attributes outside the constructor method i.e car_type.
Class methods
Methods are the functions that we use to describe the behavior of the objects. They are also defined inside a
class.
The methods defined inside a class other than the constructor method are known as the instance methods.
Furthermore, we have two instance methods here- description() and max_speed(). Let’s talk about them
individually-
description()- This method is returning a string with the description of the car such as the name and its mileage.
This method has no additional parameter. This method is using the instance attributes.
max_speed()- This method has one additional parameter and returning a string displaying the car name and its
speed.
Notice that the additional parameter speed is not using the “self” keyword. Since speed is not an instance
variable, we don’t use the self keyword as its prefix. Let’s create an object for the class described above.
class Car:
def __init__(self, name, mileage):
self.name = name
self.mileage = mileage
class Car:
Since we did not provide the second argument, we got this error.
class Car:
The interesting thing is, along with the inherited properties and methods, a child class can have its own
properties and methods.
class parent_class:
body of parent class
def description(self):
return f"The {self.name} car gives the mileage of {self.mileage}km/l"
class BMW(Car): #child class
pass
We have created two child classes namely “BMW” and “Audi” who have inherited the methods and properties
of the parent class “Car”. We have provided no additional features and methods in the class BMW. Whereas
one additional method inside the class Audi.
Notice how the instance method description() of the parent class is accessible by the objects of child classes
with the help of obj1.description() and obj2.description(). And also the separate method of class Audi is also
accessible using obj2.audi_desc().
Encapsulation
Encapsulation, as I mentioned in the initial part of the article, is a way to ensure security. Basically, it hides the
data from the access of outsiders. Such as if an organization wants to protect an object/information from
unwanted access by clients or any unauthorized person then encapsulation is the way to ensure this.
You can declare the methods or the attributes protected by using a single underscore ( _ ) before their names.
Such as- self._name or def _method( ); Both of these lines tell that the attribute and method are protected and
should not be used outside the access of the class and sub-classes but can be accessed by class methods and
objects.
Though Python uses ‘ _ ‘ just as a coding convention, it tells that you should use these attributes/methods
within the scope of the class. But you can still access the variables and methods which are defined as protected,
as usual.
Now for actually preventing the access of attributes/methods from outside the scope of a class, you can use
“private members“. In order to declare the attributes/method as private members, use double underscore ( __ )
in the prefix. Such as – self.__name or def __method(); Both of these lines tell that the attribute and method are
private and access is not possible from outside the class.
class car:
def description(self):
return f"The {self._name} car gives the mileage of {self.mileage}km/l"
Notice how we accessed the protected variable without any error. It is clear that access to the variable is still
public. Let us see how encapsulation works-
class Car:
def description(self):
return f"The {self.__name} car gives the mileage of {self.mileage}km/l"
Polymorphism
This is a Greek word. If we break the term Polymorphism, we get “poly”-many and “morph”-forms. So
Polymorphism means having many forms. In OOP it refers to the functions having the same names but carrying
different functionalities.
class Audi:
def description(self):
print("This the description function of class AUDI.")
class BMW:
def description(self):
print("This the description function of class BMW.")
audi = Audi()
bmw = BMW()
for car in (audi,bmw):
car.description()
When the function is called using the object audi then the function of class Audi is called and when it is called
using the object bmw then the function of class BMW is called.
Data abstraction
We use Abstraction for hiding the internal details or implementations of a function and showing its
functionalities only. This is similar to the way you know how to drive a car without knowing the background
mechanism. Or you know how to turn on or off a light using a switch but you don’t know what is happening
behind the socket.
Any class with at least one abstract function is an abstract class. In order to create an abstraction class first, you
need to import ABC class from abc module. This lets you create abstract methods inside it. ABC stands for
Abstract Base Class.
class abs_class(ABC):
Body of the class
Important thing is– you cannot create an object for the abstract class with the abstract method. For example-
@abstractmethod
def price(self,x):
pass
obj = Car("Honda City")
class Car(ABC):
def __init__(self,name):
self.name = name
def description(self):
print("This the description function of class car.")
@abstractmethod
def price(self,x):
pass
class new(Car):
def price(self,x):
print(f"The {self.name}'s price is {x} lakhs.")
obj = new("Honda City")
obj.description()
obj.price(25)
Car is the abstract class that inherits from the ABC class from the abc module. Notice how I have an abstract
method (price()) and a concrete method (description()) in the abstract class. This is because the abstract class
can include both of these kinds of functions but a normal class cannot. The other class inheriting from this
abstract class is new(). This method is giving definition to the abstract method (price()) which is how we use
abstract functions.
After the user creates objects from new() class and invokes the price() method, the definitions for the price
method inside the new() class comes into play. These definitions are hidden from the user. The Abstract
method is just providing a declaration. The child classes need to provide the definition.
But when the description() method is called for the object of new() class i.e obj, the Car’s description() method
is invoked since it is not an abstract method.
Collections In Python :
A list is declared in square brackets, it is mutable, stores duplicate values and elements can be accessed using
indexes.
A tuple is ordered and immutable in nature, although duplicate entries can be there inside a tuple.
A set is unordered and declared in square brackets. It is not indexed and does not have duplicate entries as well.
A dictionary has key value pairs and is mutable in nature. We use square brackets to declare a dictionary.
These are the python’s general purpose built-in container data types. But as we all know, python always has a
little something extra to offer. It comes with a python module named collections which has specialized data
structures.
Lab Questions:
Q1. Implement the following directed unweighted graph using class, methods, and data structures of Python.
Expected output:
(0 —> 1)
(1 —> 2)
(2 —> 0) (2 —> 1)
(3 —> 2)
(4 —> 5)
(5 —> 4)
Q2. Implement the following directed weighted graph using class, methods, and data structures of Python.
Expected Output:
(0 —> 1, 6)
(1 —> 2, 7)
(2 —> 0, 5) (2 —> 1, 4)
(3 —> 2, 10)
(4 —> 5, 1)
(5 —> 4, 3)
Q3. Implement the following undirected weighted graph using class, methods, and data structures of Python.
Print the adjacency list and adjacency matrix.
Expected Output:
Adjacency List:
["A:['B',
'C', 'E']",
"C:['A',
'B', 'D',
'E']",
"B:['A',
'C', 'D']",
"E:['A',
'C']",
"D:['B',
'C']"]
Adjacency Matrix
[[ 0. 1. 1. 0. 1.]
[ 1. 0. 1. 1. 0.]
[ 1. 1. 0. 1. 1.]
[ 0. 1. 1. 0. 0.]
[ 1. 0. 1. 0. 0.]]
Additional Questions:
Consider a situation where there is a single teller in a bank who can assist customers with their transactions.
When a customer arrives at the bank, they join the end of a queue to wait for the teller. When the teller is
available, they assist the first customer in the queue and remove them from the queue.
Description:
Customer class is defined to store information about each customer, such as their name and transaction. The
Bank class is defined with a queue attribute to store instances of the Customer class, and methods to add
customers to the queue (add_customer), serve the next customer in the queue (serve_customer), and check if
the queue is empty (is_queue_empty). The code simulates customers arriving at the bank and being served by
the teller. The teller serves customers in the order they arrive and removes them from the queue using the
pop(0) method.
Lab 3 - Implementation of Depth First Search
When a dead-end occurs in any iteration, the Depth First Search (DFS) method traverses a network in a deathward
motion and uses a stack data structure to remember to acquire the next vertex to start a search.
Following the definition of the dfs algorithm, you will look at an example of a depth-first search method for a
better understanding.
Pseudocode
1. DFS(G,v) ( v is the vertex where the search starts )
2. Stack S := {}; ( start with an empty stack )
3. for each vertex u, set visited[u] := false;
4. push S, v;
5. while (S is not empty) do
6. u := pop S;
7. if (not visited[u]) then
8. visited[u] := true;
9. for each unvisited neighbour w of uu
10. push S, w;
11. end if
12. end while
13. END DFS()
Code:
graph1 = {
'A' : ['B','S'],
'B' : ['A'],
'C' : ['D','E','F','S'],
'D' : ['C'],
'E' : ['C','H'],
'F' : ['C','G'],
'G' : ['F','S'],
'H' : ['E','G'],
'S' : ['A','C','G']
}
Lab exercises:
Q.1) Implement topological sorting using DFS algorithm for the following graph.
Note: Topological sorting for Directed Acyclic Graph (DAG) is a linear ordering of vertices such that for every
directed edge u v, vertex u comes before v in the ordering.
For example, a topological sorting of the following graph is “5 4 2 3 1 0”. There can be more than one
topological sorting for a graph. Another topological sorting of the following graph is “4 5 2 3 1 0”. The first
vertex in topological sorting is always a vertex with an in-degree of 0 (a vertex with no incoming edges).
Q.2) Consider the following directed graph for detecting cycles in the graph using DFS algorithm using Python.
Q.3) Write a Python program to solve the maze problem using DFS algorithm. The following is the problem
statement and algorithm for the maze problem. Con
1. Enter the maze
2. If you have multiple ways, choose anyone and move forward
3. Keep choosing a way which was not seen so far till you exit the maze or reach dead end
4. If you exit maze, you are done.
5. If you reach dead end, this is wrong path, so take one step back, choose different path. If all paths are
seen in this, take one step back and repeat
Additional questions:
Q.1) Write a Python program to solve 3x3 sudoku with Depth First Search algorithm.
Q.2) Write a Python code to check if a given graph is Bipartite using DFS.
Note: A bipartite graph is possible if the graph coloring is possible using two colors such that vertices in a set are
colored with the same color.
Lab 4 – Implementation of Breadth First Search
The breadth-first search (BFS) algorithm is used to search a tree or graph data structure for a node that meets a
set of criteria. It starts at the tree’s root or graph and searches/visits all nodes at the current depth level before
moving on to the nodes at the next depth level.
Algorithm:
1. Create a variable called NODE-LIST and set it to initial state
2. Until a goal state is found or NODE-LIST is empty do
a. Remove the first element from NODE-LIST and call it E. If NODE-LIST
was empty, quit
b. For each way that each rule can match the state described in E do:
i. Apply the rule to generate a new state
ii. If the new state is a goal state, quit and return this state
iii. Otherwise, add the new state to the end of NODE-LIST
BFS illustrated:
Step 1: Initially fringe contains only one node corresponding to the source state A.
Figure 1
FRINGE: A
Step 2: A is removed from fringe. The node is expanded, and its children B and C are generated.
They are placed at the back of fringe.
Figure 2
FRINGE: B C
Step 3: Node B is removed from fringe and is expanded. Its children D, E are generated and put
at the back of fringe.
Figure 3
FRINGE: C D E
Step 4: Node C is removed from fringe and is expanded. Its children D and G are added to the
back of fringe.
Figure 4
FRINGE: D E D G
Step 5: Node D is removed from fringe. Its children C and F are generated and
added to the backof fringe.
Figure 5
FRINGE: E D G C F
Step 6: Node E is removed from fringe. It has no children.
Figure 6
FRINGE: D G C F
34
Step 7: D is expanded; B and F are put in OPEN.
Figure 7
FRINGE: G C F B F
Lab Exercise 1:
Q.1) Implement topological sorting using BFS algorithm for the following graph.
Note: Topological sorting for Directed Acyclic Graph (DAG) is a linear ordering of vertices
such that for every directed edge u v, vertex u comes before v in the ordering.
For example, a topological sorting of the following graph is “5 4 2 3 1 0”. There can be
more than one topological sorting for a graph. Another topological sorting of the following
graph is “4 5 2 3 1 0”. The first vertex in topological sorting is always a vertex with an in-
degree of 0 (a vertex with no incoming edges).
Q.2) Consider the following directed graph for detecting cycles in the graph using BFS
algorithm using Python.
35
Q3. Write python code for Traveling Salesman Problem (TSP) using Breadth First Search
(BFS). Graph Given Below.
graph = {
'A': {'B': 2, 'C': 3, 'D': 1},
'B': {'A': 2, 'C': 4, 'D': 2},
'C': {'A': 3, 'B': 4, 'D': 3},
'D': {'A': 1, 'B': 2, 'C': 3}
}
Additional Exercise: Write a Python program to solve 3x3 sudoku with Depth First Search
algorithm
36
Lab 05 – Implementation of Uniform cost search
Uniform Cost Search is an algorithm used to move around a directed weighted search space to
go from a start node to one of the ending nodes with a minimum cumulative cost. This search
is an uninformed search algorithm since it operates in a brute-force manner, i.e. it does not take
the state of the node or search space into consideration. It is used to find the path with the
lowest cumulative cost in a weighted graph where nodes are expanded according to their cost
of traversal from the root node. This is implemented using a priority queue where lower the
cost higher is its priority.
Pseudocode:
37
Lab Exercises:
Q.1) Write a python program to find the best path between node s and g from the given graph
using Uniform Cost Search algorithm.
Output :
Minimum cost from S to G is =3
Q.2) Implement Uniform Cost Search algorithm for the following graph to find the goal (G1,
G2 or G3) with the least cumulative cost from the source (S).
38
Q. 3) Find the shortest path between “Maldon” and “Dunwich” from the following graph
using Uniform Cost search algorithm.
Additional Questions:
Q.1) Write a python program to find the best route between any 2 cites of the given road map
using Uniform Cost Search algorithm.
39
Lab 06 – Implementation of Hill climbing search
We will assume we are trying to maximize a function. That is, we are trying to
find a point in the search space that is better than all the others. And by "better"
we mean that the evaluation is higher. We might also say that the solution is of
better quality than all the others.
40
Algorithm:
Function HILL-CLIMBING(Problem)
returns a solution stateInputs:
Problem, problem
End
Write a single python program to solve the Hill climbing search problem.
a. Let A, B to M represent a state in solution space.
State space moves are given below. For Example A5 means A is node and 5 is
its heuristics values.
A5 to T11 , B13 and C21
B13 to D27 and E3
C21 to F25 and G4
D27 to H101 and I99
F25 to J67
G4 to K99 and L3
H101 ,I99,J67 to M17
Closed = list()
SUCCESS=True
FAILURE=False
def MOVEGEN(N):
New_list=list()
if N in SuccList.keys():
New_list=SuccList[N]
return New_list
def SORT(L):
L.sort(key = lambda x: x[1])
return L
42
def APPEND(L1,L2):
New_list=list(L1)+list(L2)
return New_list
def Hill_Climbing(Start):
global Closed
N=Start
CHILD = MOVEGEN(N)
SORT(CHILD)
N=[Start,5]
print("\nStart=",N)
print("Sorted Child List=",CHILD)
newNode=CHILD[0]
CLOSED=[N]
Closed=CLOSED
#Driver Code
Hill_Climbing(Start) #call search algorithm
Lab Exercise 1: Write python code to find maximum value of f(x) where -10 <= x <= 10)
using Hill Climbing method.
Lab Exercise 2: Write a Python program to Hill Climbing Search to solve the 8-Queens
problem:
43
Lab 07 – Implementation of A* Algorithm
What is an A* Algorithm?
It is a searching algorithm that is used to find the shortest path between an initial and a final
point.
It is a handy algorithm that is often used for map traversal to find the shortest path to be
taken. A* was initially designed as a graph traversal problem, to help build a robot that can
find its own course. It still remains a widely popular algorithm for graph traversal.
It searches for shorter paths first, thus making it an optimal and complete algorithm. An
optimal algorithm will find the least cost outcome for a problem, while a complete algorithm
finds all the possible outcomes of a problem.
Another aspect that makes A* so powerful is the use of weighted graphs in its
implementation. A weighted graph uses numbers to represent the cost of taking each path or
course of action. This means that the algorithms can take the path with the least cost, and find
the best route in terms of distance and time.
Why A* Search Algorithm?
A* Search Algorithm is a simple and efficient search algorithm that can be used to find the
optimal path between two nodes in a graph. It will be used for the shortest path finding. It is
an extension of Dijkstra’s shortest path algorithm (Dijkstra’s Algorithm). The extension here
is that, instead of using a priority queue to store all the elements, we use heaps (binary trees)
to store them. The A* Search Algorithm also uses a heuristic function that provides
additional information regarding how far away from the goal node we are. This function is
used in conjunction with the f-heap data structure in order to make searching more efficient.
44
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.
Sample Input:
def heuristic(n):
H_dist = {
'A': 11,
'B': 6,
'C': 99,
'D': 1,
'E': 7,
'G': 0,
}
return H_dist[n]
45
#Describe your graph here
Graph_nodes = {
'A': [('B', 2), ('E', 3)],
'B': [('A', 2), ('C', 1), ('G', 9)],
'C': [('B', 1)],
'D': [('E', 6), ('G', 1)],
'E': [('A', 3), ('D', 6)],
'G': [('B', 9), ('D', 1)]
}
aStarAlgo('A', 'G')
Output:
Lab Exercises:
Q.1) Write a Python program to implement A* algorithm. Consider the following graph to find
the path between A and J.
46
Q. 2) Implement A* algorithm using Python to solve the given 8 puzzle problem.
Sample Solution:
47
g(n) – number of nodes traversed from start node to get to the
current node.
h(n) – number of misplaced tiles
Additional Questions:
Q.1) Implement Bellman Ford algorithm using Python to solve the given problem.
Note: Bellman Ford algorithm helps us find the shortest path from a vertex to all other
vertices of a weighted graph. Bellman Ford algorithm works by overestimating the length of
the path from the starting vertex to all other vertices. Then it iteratively relaxes those
estimates by finding new paths that are shorter than the previously overestimated paths.
Pseudocode:
function bellmanFord(G, S)
for each vertex V in G
distance[V] <- infinite
previous[V] <- NULL
48
distance[S] <- 0
49
Lab 08 – Implementation of Crypt Arithmetic
50
# Stores if a number
# is assigned to any
# character or not
used = [0]*(10)
# Stores if a character
# is at index 0 of any
# string
CharAtfront = [0]*(26)
# Update Hash[ch-'A]
Hash[ord(ch) - ord('A')] += pow(10, len(words[word]) - i - 1
# If mp[ch-'A'] is -1
if mp[ord(ch) - ord('A')] == -1:
mp[ord(ch) - ord('A')] = 0
51
uniq += str(ch)
# If i is 0 and word
# length is greater
# than 1
if i == 0 and len(words[word]) > 1:
CharAtfront[ord(ch) - ord('A')] = 1
# If mp[ch-'A] is -1
if mp[ord(ch) - ord('A')] == -1:
mp[ord(ch) - ord('A')] = 0
uniq += str(ch)
# If i is 0 and length of
# result is greater than 1
if i == 0 and len(result) > 1:
CharAtfront[ord(ch) - ord('A')] = 1
mp = [-1]*(26)
52
# Stores the character at
# index i
ch = words[i]
# If val is -1
if val != -1:
# Recursion
return solve(words, i + 1, S + val * Hash[ord(ch) - ord('A')], mp, used,
Hash, CharAtfront)
# If used[l] is true
if used[l] == 1:
continue
# Assign l to ch
mp[ord(ch) - ord('A')] = l
# Marked l as used
used[l] = 1
53
x |= solve(words, i + 1, S + l * Hash[ord(ch) - ord('A')], mp, used,
Hash, CharAtfront)
# Backtrack
mp[ord(ch) - ord('A')] = -1
# Unset used[l]
used[l] = 0
# Function Call
if isSolvable(arr, S):
print("Yes")
else:
print("No")
Lab Exercise 1:
Write python code for arithmetic problem CROSS + ROADS = DANGER
Lab Exercise 2:
Lab Exercise 3:
Write python code for arithmetic problem MIT + MANIPAL = MITMAHE
54
Lab 9 – Implementation of Water jug problem
Problem: You are given two jugs, a 4-gallon one and a 3-gallon one.Neither has any
measuring mark on it.There is a pump that can be used to fill the jugs with water.How can
you get exactly 2 gallons of water into the 4-gallon jug.
Solution:
The state space for this problem can be described as the set of ordered pairs of integers (x,y)
Where,
X represents the quantity of water in the 4-gallon jug X= 0,1,2,3,4
Y represents the quantity of water in 3-gallon jug Y=0,1,2,3
Start State: (0,0)
Goal State: (2,0)
Generate production rules for the water jug problem
Production Rules:
Rule State Process
1 (X,Y | X<4) (4,Y)
{Fill 4-gallon jug}
2 (X,Y |Y<3) (X,3)
{Fill 3-gallon jug}
3 (X,Y |X>0) (0,Y)
{Empty 4-gallon jug}
4 (X,Y | Y>0) (X,0)
{Empty 3-gallon jug}
5 (X,Y | X+Y>=4 ^ (4,Y-(4-X))
Y>0) {Pour water from 3-gallon jug into 4-gallon jug
until 4-gallon jug is full}
6 (X,Y | X+Y>=3 (X-(3-Y),3)
^X>0) {Pour water from 4-gallon jug into 3-gallon jug
until 3-gallon jug is full}
55
7 (X,Y | X+Y<=4 (X+Y,0)
^Y>0) {Pour all water from 3-gallon jug into 4-gallon jug}
8 (X,Y | X+Y <=3^ (0,X+Y)
X>0) {Pour all water from 4-gallon jug into 3-gallon jug}
9 (0,2) (2,0)
{Pour 2 gallon water from 3 gallon jug into 4 gallon
jug}
Initialization:
Start State: (0,0)
Apply Rule 2:
(X,Y | Y<3) ->
(X,3)
{Fill 3-gallon jug}
Now the state is (X,3)
Iteration 1:
Current State: (X,3)
Apply Rule 7:
(X,Y | X+Y<=4 ^Y>0)
(X+Y,0)
{Pour all water from 3-gallon jug into 4-gallon jug}
Now the state is (3,0)
Iteration 2:
Current State : (3,0)
Apply Rule 2:
(X,Y | Y<3) ->
(3,3)
{Fill 3-gallon jug}
Now the state is (3,3)
Iteration 3:
Current State:(3,3)
Apply Rule 5:
(X,Y | X+Y>=4 ^ Y>0)
(4,Y-(4-X))
{Pour water from 3-gallon jug into 4-gallon jug until 4-gallon jug is full}
56
Now the state is (4,2)
Iteration 4:
Current State : (4,2)
Apply Rule 3:
(X,Y | X>0)
(0,Y)
{Empty 4-gallon jug}
Now state is (0,2)
Iteration 5:
Current State : (0,2)
Apply Rule 9:
(0,2)
(2,0)
{Pour 2 gallon water from 3 gallon jug into 4 gallon jug}
Now the state is (2,0)
Goal Achieved.
Lab Exercises:
Q.1) Write a Python program to solve the water jug problem using Breadth First Search
algorithm.
Q.2) Write a Python program to solve the water jug problem using Depth First Search
algorithm.
Additional Questions:
Q.1) Write a Python program to solve the water jug problem using Memoization algorithm.
Note: Approach: Using Recursion, visit all the six possible moves one by one until one of
them returns True. Since there can be repetitions of same recursive calls, hence every return
value is stored using memoization to avoid calling the recursive function again and returning
the stored value.
57
Lab 10 – Implementation of Missionaries and Cannibals problem
3. The boat cannot cross the river by itself with no people on board.
Problem Formation:
State space: triple (x,y,z) with 0 x,y,z 3, wherex,y, and
z represent the number of missionaries, cannibals and boats
currently on the original bank.
Initial State: (3,3,1)
Successor function: From each state, either bring one
missionary, one cannibal, two missionaries, two cannibals, or
one of each type to the other bank.
Note: Not all states are attainable (e.g., (0,0,1)), andsome are
illegal.
Goal State: (0,0,0)
Path Costs: 1 unit per crossing
58
#Python8 program to illustrate Missionaries & cannibals Problem
print("\n")
print("\tGame Start\nNow the task is to move all of them to right side of the river")
print("rules:\n1. The boat can carry at most two people\n2. If cannibals num greater
than missionaries then the cannibals would eat the missionaries\n3. The boat cannot
cross the river by itself with no people on board")
lM = 3 #lM = Left side Missionaries number
lC = 3 #lC = Laft side Cannibals number
rM=0 #rM = Right side Missionaries number
rC=0 #rC = Right side cannibals number
userM = 0 #userM = User input for number of missionaries for right to left side
travel
userC = 0 #userC = User input for number of cannibals for right to left travel
k=0
print("\nM M M C C C | --- | \n")
try:
while(True):
while(True):
print("Left side -> right side river travel")
#uM = user input for number of missionaries for left to right
59
travel
#uC = user input for number of cannibals for left to right travel
uM = int(input("Enter number of Missionaries travel => "))
uC = int(input("Enter number of Cannibals travel => "))
if((uM==0)and(uC==0)):
print("Empty travel not possible")
print("Re-enter : ")
elif(((uM+uC) <= 2)and((lM-uM)>=0)and((lC-uC)>=0)):
break
else:
print("Wrong input re-enter : ")
lM = (lM-uM)
lC = (lC-uC)
rM += uM
rC += uC
print("\n")
for i in range(0,lM):
print("M ",end="")
for i in range(0,lC):
print("C ",end="")
print("| --> | ",end="")
for i in range(0,rM):
print("M ",end="")
for i in range(0,rC):
print("C ",end="")
print("\n")
k +=1
if(((lC==3)and (lM ==
1))or((lC==3)and(lM==2))or((lC==2)and(lM==1))or((rC==3)and (rM ==
1))or((rC==3)and(rM==2))or((rC==2)and(rM==1))):
print("Cannibals eat missionaries:\nYou lost the game")
break
60
if((rM+rC) == 6):
print("You won the game : \n\tCongrats")
print("Total attempt")
print(k)
break
while(True):
print("Right side -> Left side river travel")
userM = int(input("Enter number of Missionaries travel => "))
userC = int(input("Enter number of Cannibals travel => "))
if((userM==0)and(userC==0)):
print("Empty travel not possible")
print("Re-enter : ")
elif(((userM+userC) <= 2)and((rM-userM)>=0)and((rC-
userC)>=0)):
break
else:
print("Wrong input re-enter : ")
lM += userM
lC += userC
rM -= userM
rC -= userC
k +=1
print("\n")
for i in range(0,lM):
print("M ",end="")
for i in range(0,lC):
print("C ",end="")
print("| <-- | ",end="")
for i in range(0,rM):
print("M ",end="")
for i in range(0,rC):
print("C ",end="")
print("\n")
61
if(((lC==3)and (lM ==
1))or((lC==3)and(lM==2))or((lC==2)and(lM==1))or((rC==3)and (rM ==
1))or((rC==3)and(rM==2))or((rC==2)and(rM==1))):
print("Cannibals eat missionaries:\nYou lost the game")
break
except EOFError as e:
print("\nInvalid input please retry !!")
Lab Exercise 1: Write python code for Python code for a Wumpus World game.
62
Lab 11 – Implementation of 8 queen’s problem
The eight queens problem is the problem of placing eight queens on an 8×8 chessboard such
that none of them attack one another (no two are in the same row, column, or diagonal). More
generally, the n queens problem places n queens on an n×n chessboard.
def printSolution(board):
for i in range(N):
for j in range(N):
print (board[i][j],end=' ')
print()
return True
64
# Consider this column and try placing
# this queen in all rows one by one
for i in range(N):
if isSafe(board, i, col):
# Place this queen in board[i][col]
board[i][col] = 1
65
# placement of queens in the form of 1s.
# note that there may be more than one
# solutions, this function prints one of the
# feasible solutions.
def solveNQ():
board = [ [0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
if solveNQUtil(board, 0) == False:
print ("Solution does not exist")
return False
printSolution(board)
return True
Lab Exercises:
Q.1) Write a python program to solve 8 queens problem using Hill Climbing algorithm.
Sample Input: N = 8
Output:
66
00000010
01000000
00000100
00100000
10000000
00010000
00000001
00001000
Q. 2) Write a Python program to find all possible solution for 8 queens problem using Breadth
First Search algorithm.
Expected output:
solution: (0, 4, 7, 5, 2, 6, 1, 3)
solution: (0, 5, 7, 2, 6, 3, 1, 4)
solution: (0, 6, 3, 5, 7, 1, 4, 2)
…
…
solution: (7, 3, 0, 2, 5, 1, 6, 4)
Additional Questions:
Q.1) Write a Python program to solve the N queens problem using genetic algorithm.
A genetic algorithm is a search heuristic that is inspired by Charles Darwin’s theory of natural
evolution. This algorithm reflects the process of natural selection where the fittest individuals
are selected for reproduction in order to produce offspring of the next generation.
Pseudocode:
START
Generate the initial population
Compute fitness
REPEAT
Selection
Crossover
Mutation
Compute fitness
UNTIL population has converged
STOP
67
Lab 12 – Implementation of Best First Search
CLOSED are nodes that have already been generated and these nodes must be
stored because a graph is being used in preference to a tree.
Algorithm:
68
• For each successor Do
• If it has not been generated before ,evaluate it ,add it to
OPEN and record its parent.If it has been generated
before change the parent if this new path is better and
in that case update the cost of getting to any successor
nodes.
Write a single python program to solve the Best First Search Problem
a. State space moves are given below.
A to [B,3],[C,2]
B to [[A,5],[C,2],[D,2],[E,3]]
C to [[A,5],[B,3],[F,2],[G,4]]
D to [[H,1],[I,99]]
F to [J,99]
G to [K,99],[L,3]
#Python10 program
def MOVEGEN(N):
New_list=list()
if N in SuccList.keys():
New_list=SuccList[N]
return New_list
def GOALTEST(N):
if N == Goal:
return True
else:
return False
def APPEND(L1,L2):
New_list=list(L1)+list(L2)
return New_list
def SORT(L):
L.sort(key = lambda x: x[1])
return L
def BestFirstSearch():
70
OPEN=[[Start,5]]
CLOSED=list()
global State
global Closed
while (len(OPEN) != 0) and (State != SUCCESS):
print("------------")
N= OPEN[0]
print("N=",N)
del OPEN[0] #delete the node we picked
if GOALTEST(N[0])==True:
State = SUCCESS
CLOSED = APPEND(CLOSED,[N])
print("CLOSED=",CLOSED)
else:
CLOSED = APPEND(CLOSED,[N])
print("CLOSED=",CLOSED)
CHILD = MOVEGEN(N[0])
print("CHILD=",CHILD)
for val in CLOSED:
if val in CHILD:
CHILD.remove(val)
for val in OPEN:
if val in CHILD:
CHILD.remove(val)
OPEN = APPEND(CHILD,OPEN) #append movegen
elements to OPEN
print("Unsorted OPEN=",OPEN)
SORT(OPEN)
print("Sorted OPEN=",OPEN)
Closed=CLOSED
return State
#Driver Code
result=BestFirstSearch() #call search algorithm
print(Closed,result)
71
Lab Exercises: AI game called "Connect Four" with two players take turns dropping discs
into a vertical grid with the goal of getting four discs in a row vertically, horizontally, or
diagonally.
REFERENCES:
1. Stuart Russell and Peter Norvig -‚Artificial Intelligence A Modern Approach", Pearson
Education, Third Edition, 2016.   
2. Elaine Rich, Kevin Knight, Shivashankar B. Nair, Artificial Intelligence, Third‚
Edition, Tata McGraw Hill Edition, 2010.  
3. Saroj Kaushik-‚Artificial Intelligence, Cengage Learning Publications, First Edition,
2011.‚ÄØ
4. Don W. Patterson -‚Introduction to‚ Artificial Intelligence and Expert Systems, PHI
Publication,2006.‚ÄØ
72