AI_Labfile
AI_Labfile
Submitted To :- Submitted By :-
Dr. Anshul Arora Nehal Ashraf
2K22/EC/156
E4 : P4 group
Sem - VI
INDEX
S. No. Topic Signature
Overview
The 8-Puzzle problem is a classic sliding tile puzzle with a 3×3 grid
containing 8 numbered tiles and one empty space. The objective is to
rearrange the tiles from an initial configuration to a goal state by
sliding tiles into the empty space.
Generate and Test Strategy works by:
Generating possible moves (sliding tiles into the empty space)
Testing each move to see if it leads to the goal state
If not, generating further moves
This approach systematically explores the state space without using
heuristics, typically implementing a breadth-first search to ensure
optimality. The solution must handle state representation, move
generation, goal testing, and path tracking.
Code
import copy
from collections import deque
def solve_8puzzle_generate_and_test(initial_state):
# Define the goal state
goal_state = [[1, 2, 3], [4, 5, 6], [7, 8, 0]] # 0 represents empty space
def print_puzzle(state):
for row in state:
print(row)
print()
def find_blank(state):
for i in range(3):
for j in range(3):
if state[i][j] == 0:
return i, j
def get_possible_moves(state):
i, j = find_blank(state)
possible_moves = []
if i < 2: # Down
new_state = copy.deepcopy(state)
new_state[i][j], new_state[i+1][j] = new_state[i+1][j],
new_state[i][j]
possible_moves.append(new_state)
if j > 0: # Left
new_state = copy.deepcopy(state)
new_state[i][j], new_state[i][j-1] = new_state[i][j-1],
new_state[i][j]
possible_moves.append(new_state)
if j < 2: # Right
new_state = copy.deepcopy(state)
new_state[i][j], new_state[i][j+1] = new_state[i][j+1],
new_state[i][j]
possible_moves.append(new_state)
return possible_moves
iterations = 0
if iterations % 1000 == 0:
print(f"Iteration {iterations}, Queue size: {len(queue)}")
if is_goal(current_state, goal_state):
print("Goal state reached!")
print(f"Solution found in {len(path)} moves and {iterations}
iterations.")
return path
possible_moves = get_possible_moves(current_state)
iterations += 1
if solution_path:
print("Solution path:")
for i, state in enumerate(solution_path):
print(f"Move {i+1}:")
print_puzzle(state)
return solution_path
# Example usage
if __name__ == "__main__":
# Example initial state (easier to solve)
initial_state = [[1, 2, 3], [4, 0, 6], [7, 5, 8]]
solve_8puzzle_generate_and_test(initial_state)
Output
Experiment – 2
Aim
Write a program to solve the 8-Puzzle problem using DFID Strategy
Overview
Depth-First Iterative Deepening (DFID) combines the memory
efficiency of depth-first search with the completeness of breadth-
first search by:
Starting with a depth limit of 1
Performing depth-first search up to that limit
If no solution is found, incrementing the depth limit and
repeating
For the 8-Puzzle, DFID is advantageous because:
It finds the optimal solution (minimum number of moves)
It uses less memory than breadth-first search
It avoids the infinite path problems of standard depth-first
search
The implementation must include efficient state representation, loop
detection, and depth tracking mechanisms.
Code
import copy
def solve_8puzzle_dfid(initial_state):
# Define the goal state
goal_state = [[1, 2, 3], [4, 5, 6], [7, 8, 0]] # 0 represents empty space
def print_puzzle(state):
for row in state:
print(row)
print()
def find_blank(state):
for i in range(3):
for j in range(3):
if state[i][j] == 0:
return i, j
def get_possible_moves(state):
i, j = find_blank(state)
possible_moves = []
if i < 2: # Down
new_state = copy.deepcopy(state)
new_state[i][j], new_state[i+1][j] = new_state[i+1][j],
new_state[i][j]
possible_moves.append(new_state)
if j > 0: # Left
new_state = copy.deepcopy(state)
new_state[i][j], new_state[i][j-1] = new_state[i][j-1],
new_state[i][j]
possible_moves.append(new_state)
if j < 2: # Right
new_state = copy.deepcopy(state)
new_state[i][j], new_state[i][j+1] = new_state[i][j+1],
new_state[i][j]
possible_moves.append(new_state)
return possible_moves
if is_goal(state, goal):
return True, path
if depth_limit <= 0:
return False, None
possible_moves = get_possible_moves(state)
if solution_path:
print("Solution path:")
for i, state in enumerate(solution_path):
print(f"Move {i}:")
print_puzzle(state)
return solution_path
# Example usage
if __name__ == "__main__":
# Example initial state (easier to solve)
initial_state = [[1, 2, 3], [4, 0, 6], [7, 5, 8]]
solve_8puzzle_dfid(initial_state)
Output
Experiment – 3
Aim
Write a program to solve the 3-SAT Problem using Variable
Neighbourhood Descent Algorithm
Overview
The 3-SAT Problem is a variant of the boolean satisfiability problem
where each clause contains exactly three literals. The goal is to
determine if there exists an assignment of truth values to variables
that makes the entire formula true.
Variable Neighbourhood Descent (VND) works by:
Starting with an initial solution
Systematically exploring increasingly distant neighborhoods
Moving to better solutions when found
Returning to closer neighborhoods when improvements are
made
For 3-SAT, this means:
Representing boolean assignments as a solution
Defining neighborhoods (e.g., flipping 1, 2, or 3 variables)
Evaluating solutions by counting satisfied clauses
Implementing a structured search across different
neighborhood types
The algorithm must balance exploration and exploitation to find
satisfying assignments efficiently.
Code
import random
Args:
clauses: List of tuples, where each tuple contains 3 integers.
Positive integers represent variables, negative integers
represent negated variables.
variables: Number of variables in the problem.
Returns:
A solution (assignment of values to variables) if found, None
otherwise.
"""
def evaluate(assignment, clause):
"""Evaluate if a clause is satisfied by an assignment."""
for literal in clause:
var = abs(literal)
if (literal > 0 and assignment[var - 1]) or (literal < 0 and not
assignment[var - 1]):
return True
return False
def neighborhood_1(assignment):
"""Return all assignments with one bit flipped."""
neighbors = []
for i in range(variables):
neighbors.append(flip_bit(assignment, i))
return neighbors
def neighborhood_2(assignment):
"""Return all assignments with two consecutive bits flipped."""
neighbors = []
for i in range(variables - 1):
new_assignment = assignment.copy()
new_assignment[i] = not new_assignment[i]
new_assignment[i + 1] = not new_assignment[i + 1]
neighbors.append(new_assignment)
return neighbors
def neighborhood_3(assignment):
"""Return some random assignments with three bits flipped."""
neighbors = []
for _ in range(min(50, variables * (variables - 1) * (variables - 2)
// 6)):
indices = random.sample(range(variables), 3)
new_assignment = assignment.copy()
for i in indices:
new_assignment[i] = not new_assignment[i]
neighbors.append(new_assignment)
return neighbors
def vnd_search(initial_assignment):
"""Perform VND search."""
neighborhoods = [neighborhood_1, neighborhood_2, neighborhood_3]
current_assignment = initial_assignment
current_score = count_satisfied_clauses(current_assignment, clauses)
if current_score == len(clauses):
return current_assignment
improved = True
while improved:
improved = False
for get_neighbors in neighborhoods:
neighbors = get_neighbors(current_assignment)
best_neighbor = None
best_score = current_score
if current_score == len(clauses):
return current_assignment
return None
# Example usage
if __name__ == "__main__":
# Example 3-SAT problem: (x1 OR x2 OR x3) AND (NOT x1 OR x2 OR x3) AND (x1
OR NOT x2 OR NOT x3)
# Represented as [(1, 2, 3), (-1, 2, 3), (1, -2, -3)]
clauses = [(1, 2, 3), (-1, 2, 3), (1, -2, -3)]
num_variables = 3
if solution:
print("Solution found!")
for i, val in enumerate(solution):
print(f"x{i+1} = {val}")
Output
Experiment – 4
Aim
Write a program to solve the 3-SAT Problem using Stochastic Hill
Climbing Algorithm
Overview
Stochastic Hill Climbing is a variant of hill climbing that introduces
randomness to escape local optima. For the 3-SAT problem, it works
by:
Starting with a random assignment of truth values
Randomly selecting a variable to flip
Accepting the change if it improves the solution (increases
satisfied clauses)
Sometimes accepting non-improving moves with a certain
probability
The implementation should include:
Random solution initialization
Variable selection strategy
Evaluation function to count satisfied clauses
Acceptance criteria that allows occasional "bad" moves
Termination conditions (solution found or iteration limit
reached)
This approach is particularly useful for large 3-SAT instances where
complete methods are impractical.
Code
import random
Args:
clauses: List of tuples, where each tuple contains 3 integers.
Positive integers represent variables, negative integers
represent negated variables.
variables: Number of variables in the problem.
max_iterations: Maximum number of iterations to try.
Returns:
A solution (assignment of values to variables) if found, None
otherwise.
"""
def evaluate(assignment, clause):
"""Evaluate if a clause is satisfied by an assignment."""
for literal in clause:
var = abs(literal)
if (literal > 0 and assignment[var - 1]) or (literal < 0 and not
assignment[var - 1]):
return True
return False
def get_random_neighbor(assignment):
"""Get a random neighbor by flipping one bit."""
index = random.randint(0, variables - 1)
return flip_bit(assignment, index)
def stochastic_hill_climbing():
"""Perform stochastic hill climbing."""
# Generate random initial assignment
current_assignment = [random.choice([True, False]) for _ in
range(variables)]
current_score = count_satisfied_clauses(current_assignment, clauses)
if current_score == len(clauses):
return current_assignment
if current_score == len(clauses):
print(f"Solution found at iteration {iteration}")
return current_assignment
return None
# Example usage
if __name__ == "__main__":
# Example 3-SAT problem: (x1 OR x2 OR x3) AND (NOT x1 OR x2 OR x3) AND (x1
OR NOT x2 OR NOT x3)
# Represented as [(1, 2, 3), (-1, 2, 3), (1, -2, -3)]
clauses = [(1, 2, 3), (-1, 2, 3), (1, -2, -3)]
num_variables = 3
solution = solve_3sat_stochastic_hill_climbing(clauses, num_variables)
if solution:
print("Solution found!")
for i, val in enumerate(solution):
print(f"x{i+1} = {val}")
if all_satisfied:
print("All clauses are satisfied.")
else:
print("No solution found.")
Output
Experiment – 5
Aim
Write a program to solve the 8-Puzzle problem using A* algorithm
Overview
The A algorithm* is an informed search strategy that evaluates
nodes by combining:
g(n): The cost to reach the node from the start
h(n): A heuristic estimate of the cost to reach the goal from the
node
For the 8-Puzzle, common heuristics include:
Manhattan distance: Sum of the distances each tile is from its
goal position
Misplaced tiles: Number of tiles not in their goal position
The implementation must include:
Priority queue based on f(n) = g(n) + h(n)
State representation and operators
Closed set to avoid revisiting states
Path reconstruction to return the solution
A* guarantees an optimal solution if the heuristic is admissible
(never overestimates the true cost).
Code
import heapq
import copy
def solve_8puzzle_astar(initial_state):
"""
Solve the 8-puzzle problem using A* algorithm.
Args:
initial_state: 3x3 grid represented as a list of lists.
0 represents the empty space.
Returns:
A solution path if found, None otherwise.
"""
# Define the goal state
goal_state = [[1, 2, 3], [4, 5, 6], [7, 8, 0]]
def print_puzzle(state):
for row in state:
print(row)
print()
def get_possible_moves(state):
"""Get all possible moves from the current state."""
moves = []
i, j = find_position(state, 0) # Find the empty space
# Try moving the empty space in all four directions
if i > 0: # Up
new_state = copy.deepcopy(state)
new_state[i][j], new_state[i-1][j] = new_state[i-1][j],
new_state[i][j]
moves.append(new_state)
if i < 2: # Down
new_state = copy.deepcopy(state)
new_state[i][j], new_state[i+1][j] = new_state[i+1][j],
new_state[i][j]
moves.append(new_state)
if j > 0: # Left
new_state = copy.deepcopy(state)
new_state[i][j], new_state[i][j-1] = new_state[i][j-1],
new_state[i][j]
moves.append(new_state)
if j < 2: # Right
new_state = copy.deepcopy(state)
new_state[i][j], new_state[i][j+1] = new_state[i][j+1],
new_state[i][j]
moves.append(new_state)
return moves
def state_to_tuple(state):
"""Convert state to tuple for hashability."""
return tuple(tuple(row) for row in state)
def astar_search():
"""Perform A* search."""
# Priority queue of states to explore: (f-value, g-value, state, path)
# f-value = g-value + h-value (path cost + heuristic)
# g-value = path cost (number of moves)
open_set = [(manhattan_distance(initial_state, goal_state), 0,
initial_state, [initial_state])]
closed_set = set() # Set of visited states
iterations = 0
if iterations % 1000 == 0:
print(f"Iteration {iterations}, Open set size:
{len(open_set)}")
# Pop the state with the lowest f-value
f, g, current_state, path = heapq.heappop(open_set)
if next_tuple in closed_set:
continue
solution_path = astar_search()
if solution_path:
print("Solution path:")
for i, state in enumerate(solution_path):
print(f"Move {i}:")
print_puzzle(state)
return solution_path
# Example usage
if __name__ == "__main__":
# Example initial state (easier to solve)
initial_state = [[1, 2, 3], [4, 0, 6], [7, 5, 8]]
solve_8puzzle_astar(initial_state)
Output
Experiment – 6
Aim
Write a program to implement AO* search algorithm on any AND-OR
graph
Overview
AO Search* is an extension of A* for solving problems that can be
represented as AND-OR graphs, where:
OR nodes: Represent alternative ways to solve a problem (only
one needs to succeed)
AND nodes: Represent subproblems that must all be solved
The algorithm works by:
Expanding the most promising paths first
Computing the cost of nodes based on their type (AND/OR)
Propagating costs up the graph
Marking the best partial solution graph
The implementation should include:
Graph representation with AND/OR nodes
Heuristic evaluation function
Cost propagation mechanisms
Solution graph extraction
This approach is useful for problem-solving in domains like planning,
game playing, and theorem proving.
Code
class Node:
def __init__(self, name, is_and_node=False, cost=0, heuristic=0):
self.name = name
self.is_and_node = is_and_node # True for AND nodes, False for OR
nodes
self.cost = cost # Cost to reach this node
self.heuristic = heuristic # Heuristic estimate to reach goal
self.children = [] # List of child nodes
self.marked = False # Whether this node is marked (part of the
solution)
self.solved = False # Whether this node is solved
self.value = float('inf') # Value of the node (cost + heuristic)
def __repr__(self):
return f"Node({self.name}, {'AND' if self.is_and_node else 'OR'},
cost={self.cost}, h={self.heuristic}, value={self.value},
solved={self.solved})"
Args:
graph: Dictionary mapping node names to Node objects.
root: The root node of the graph.
Returns:
The solution graph (marked nodes).
"""
def calculate_node_value(node):
"""Calculate the value of a node based on its children."""
if not node.children:
# Terminal node
node.value = node.cost
node.solved = True
return node.value
if node.is_and_node:
# AND node: sum of all children's values
total_value = 0
all_solved = True
for child in node.children:
total_value += child.value
all_solved = all_solved and child.solved
node.value = node.cost + total_value
node.solved = all_solved
return node.value
else:
# OR node: minimum value among children
min_value = float('inf')
node.solved = False
for child in node.children:
if child.value < min_value:
min_value = child.value
node.solved = child.solved
node.value = node.cost + min_value
return node.value
def mark_best_path(node):
"""Mark the best path in the graph."""
node.marked = True
if not node.children:
return
if node.is_and_node:
# Mark all children of AND node
for child in node.children:
mark_best_path(child)
else:
# Mark only the best child of OR node
best_child = None
min_value = float('inf')
for child in node.children:
if child.value < min_value:
min_value = child.value
best_child = child
if best_child:
mark_best_path(best_child)
def expand_graph(node):
"""Expand the graph from the given node."""
if node.solved or not node.children:
return
return graph
# Example usage
if __name__ == "__main__":
# Create a simple AND-OR graph
# A is an OR node with children B and C
# B is an AND node with children D and E
# C is an OR node with child F
# D, E, F are terminal nodes
# Create nodes
A = Node("A", is_and_node=False, cost=0, heuristic=5)
B = Node("B", is_and_node=True, cost=2, heuristic=3)
C = Node("C", is_and_node=False, cost=3, heuristic=2)
D = Node("D", is_and_node=False, cost=4, heuristic=0)
E = Node("E", is_and_node=False, cost=5, heuristic=0)
F = Node("F", is_and_node=False, cost=7, heuristic=0)
Output
Experiment – 7
Aim
Write a program in Prolog to find maximum of two/three numbers
max_of_three(X, Y, Z, Max) :-
max_of_two(X, Y, MaxXY),
max_of_two(MaxXY, Z, Max).
Code
% Find maximum of two numbers
max_of_two(X, Y, Max) :-
X >= Y,
Max is X.
max_of_two(X, Y, Max) :-
X < Y,
Max is Y.
Output
Experiment – 8
Aim
Write a program in Prolog to find factorial of a number
Overview
This question requires implementing factorial calculation in Prolog's
logical programming paradigm. The implementation includes:
Base case: Factorial of 0 is 1
Recursive case: Factorial of N is N multiplied by factorial of (N-
1)
The solution demonstrates:
Prolog's powerful pattern matching for base case detection
Recursive rule definition
Arithmetic expressions using the is operator
This program serves as an excellent example of how mathematical
recursion translates naturally to Prolog's declarative style.
Code
% Base case: factorial of 0 is 1
factorial(0, 1).
Overview
This task involves creating a Prolog program to calculate the sum of
integers from 1 to N. The implementation includes:
Base case: Sum of first 0 numbers is 0
Recursive case: Sum of first N numbers is N plus the sum of first
(N-1) numbers
Key concepts demonstrated include:
Recursive rule definition
Arithmetic in Prolog
Base case handling to terminate recursion
The solution shows how Prolog's logical paradigm can be used to
express mathematical series calculations.
Code
% Base case: sum of first 0 numbers is 0
sum_first_n(0, 0).
Overview
This task requires implementing the Fibonacci sequence in Prolog,
where each number is the sum of the two preceding ones (starting
with 0 and 1). The implementation includes:
Base cases: Fibonacci(0) = 0, Fibonacci(1) = 1
Recursive case: Fibonacci(N) = Fibonacci(N-1) + Fibonacci(N-2)
A procedure to print all Fibonacci numbers up to the Nth term
The solution demonstrates:
Multiple base cases in Prolog
Arithmetic calculations
Recursive definitions
Potentially, memoization techniques to improve performance
This program showcases Prolog's ability to express mathematical
sequences through logical rules and recursion.
Code
% Base cases
fibonacci(0, 0).
fibonacci(1, 1).
% Recursive case
fibonacci(N, Result) :-
N > 1,
N1 is N - 1,
N2 is N - 2,
fibonacci(N1, F1),
fibonacci(N2, F2),
Result is F1 + F2.
print_fibonacci_up_to(Current, N) :-
Current =< N,
fibonacci(Current, F),
format('Fibonacci(~w) = ~w~n', [Current, F]),
Next is Current + 1,
print_fibonacci_up_to(Next, N).
print_fibonacci_up_to(Current, N) :-
Current > N.
Output