Lab Manual
Lab Manual
DHARMAPURI – 636701
AI&DS-AD3301
PAGE
S.NO NAME OF THE EXPERIMENTS MARKS SIGNATURE
NO
Dynamic programming –
(a)Coin change Problem,
5 (b) Warshall’s
(c)Floyd‘s algorithms,
(d) Knapsack Problem
Greedy Technique –
6
(a)Dijkstra’s algorithm,
(b)Huffman Trees and codes
Backtracking –
8 (a)N-Queen problem,
(b)Subset Sum Problem
AIM:
Implement recursive algorithms and study the order of growth from log2n to n!
PROCEDURE:
A function is said to be recursive if it keeps calling itself until it reaches the base case.
Any recursive function has two primary components: the base case and the recursive
step. We stop going to the recursive phase once we reach the basic case.
This is one of the most interesting Algorithms as it calls itself with a smaller value as
inputs which it gets after solving for the current inputs. In simpler words, it’s an
Algorithm that calls itself repeatedly until the problem is solved. Problems such as the
Tower of Hanoi or DFS of a Graph can be easily solved by using these Algorithms.
PROGRAM:
import math.
# Recursive function to calculate n!
def factorial_recursive(n):
if n <= 1:
return 1
else:
return n * factorial_recursive(n - 1)
# Calculate the order of growth from log2(n) to n!
def calculate_growth_order_recursive(n):
log_n = math.log2(n)
factorial_n = factorial_recursive(n)
return log_n, factorial_n
# Example usage:
n = 10 # You can change the value of n as needed
log_n, factorial_n = calculate_growth_order_recursive(n)
print(f"log2({n}) = {log_n}")
print(f"{n}! = {factorial_n}")
OUTPUT:
log2(12) = 3.584962500721156
12! = 479001600
RESULT:
EX NO: 1(b)
DATE: NON-RECURSIVE
Aim:
Implement non-recursive algorithms and study the order of growth from log2n
to n!
PROCEDURE:
A non-recursive algorithm does the sorting all at once, without calling itself.
Bubble-sort is an example of a non-recursive algorithm.
PROGRAM:
import math
# Non-recursive function to calculate n!
def factorial_non_recursive(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
# Example usage:
n = 10 # You can change the value of n as needed
log_n, factorial_n = calculate_growth_order_non_recursive(n)
print(f"log2({n}) = {log_n}")
print(f"{n}! = {factorial_n}")
OUTPUT:
log2(20) = 4.321928094887363
20! = 2432902008176640000
RESULT:
EX NO: 2
DATE: DIVIDE AND CONQUER
STRASSEN’S MATRIX MULTIPLICATION
AIM:
PROCEDURE:
Divide and Conquer: This is another effective way of solving many problems.
In Divide and Conquer algorithms, divide the algorithm into two parts; the first parts
divide the problem on hand into smaller sub problems of the same type.
Then, in the second part, these smaller problems are solved and then added together
(combined) to produce the problem’s final solution.
Problems such as Binary Search, Quick Sort, and Merge Sort can be solved using this
technique.
Strassen's algorithm is an algorithm for matrix multiplication that was devised by
Volker Strassen in 1969. It is a divide-and-conquer algorithm that can multiply two
matrices in a more efficient way compared to the standard matrix multiplication
algorithm, especially for large matrices. The standard matrix multiplication has a time
complexity of O(n^3), where n is the dimension of the matrices, while Strassen's
algorithm has a slightly better time complexity of approximately O(n^2.81).
The basic idea behind Strassen's algorithm is to break down the matrix
multiplication into a set of subproblems and then combine the results in a way that
requires fewer multiplications than the standard algorithm
PROGRAM:
import numpy as np
# Base case: if the matrices are 1x1, just multiply the elements
if n == 1:
return np.array([[A[0, 0] * B[0, 0]]])
return result
# Example usage:
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
result = strassen_multiply(A, B)
print(result)
OUTPUT:
[[19 22]
[43 50]]
RESULT:
EX NO:3
DATE: DECREASE AND CONQUER
AIM:
To implement a Topological Sorting using Decrease and Conquer -
PROCEDURE:
"Decrease and Conquer" is a problem-solving paradigm that involves breaking
down a problem into smaller subproblems, solving the subproblems, and then
combining their solutions to solve the original problem. In the context of topological
sorting, a common way to apply "Decrease and Conquer" is through the removal of
vertices with no incoming edges.
PROGRAM:
from collections import defaultdict
class Graph:
def __init__(self, vertices):
self.graph = defaultdict(list)
self.V = vertices
def addEdge(self, u, v):
self.graph[u].append(v)
def topologicalSortUtil(self, v, visited, stack):
visited[v] = True
for i in self.graph[v]:
if not visited[i]:
self.topologicalSortUtil(i, visited, stack)
stack.insert(0, v)
def topologicalSort(self):
visited = [False] * self.V
stack = []
for i in range(self.V):
if not visited[i]:
self.topologicalSortUtil(i, visited, stack)
return stack
# Example usage:
g = Graph(6)
g.addEdge(5, 2)
g.addEdge(5, 0)
g.addEdge(4, 0)
g.addEdge(4, 1)
g.addEdge(2, 3)
g.addEdge(3, 1)
topological_order = g.topologicalSort()
print("Topological Sort:")
print(topological_order)
OUTPUT:
Topological Sort:
[5, 4, 2, 3, 1, 0]
RESULT:
EX NO:4
DATE: TRANSFORM AND CONQUER - HEAP SORT
AIM:
To implement a Heap Sorting using Transform Conquer
PROCEDURE:
"Transform and Conquer" is a problem-solving paradigm that involves
transforming the input instance of a problem into a different representation and then
conquering the transformed instance. While "Transform and Conquer" is not a standard
term used specifically for Heap Sort, you can view Heap Sort through the lens of this
paradigm.
1. Transform:
Convert the unsorted array into a binary heap.
The binary heap is a data structure that satisfies the heap property (either
max-heap or min-heap).
This transformation is achieved by viewing the array as a complete binary
tree, and then heapifying the tree.
2. Conquer:
Perform the heap sort by repeatedly extracting the maximum (for max-
heap) or minimum (for min-heap) element from the heap.
The extraction involves swapping the root of the heap (which contains the
maximum or minimum element) with the last element in the heap,
reducing the heap size, and then heapifying to restore the heap property.
In this view, the transformation step involves building a max heap from the unsorted
array, and the conquering step involves repeatedly extracting the maximum element
from the heap to obtain a sorted array.
Heap Sort, in general, is an in-place sorting algorithm with a time complexity of O(n log
n) for all cases (best, average, and worst-case scenarios), making it a suitable choice for
sorting large datasets.
PROGRAM :
def heapify(arr, n, i):
largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[i] < arr[left]:
largest = left
if right < n and arr[largest] < arr[right]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
def heap_sort(arr):
n = len(arr)
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i]
heapify(arr, i, 0)
if __name__ == "__main__":
unsorted_list = [12, 11, 13, 5, 6, 7]
print("Unsorted list:", unsorted_list)
heap_sort(unsorted_list)
print("Sorted list:", unsorted_list)
OUTPUT:
RESULT:
EX NO:5
DATE: DYNAMIC PROGRAM – (a) COIN CHANGE PROBLEM
AIM:
To implement a Coin change problem using Dynamic algorithm
PROCEDURE:
Dynamic programming is a method for efficiently solving a broad range of search
and optimization problems which exhibit the property of overlapping subproblems and
optimal substructure. It is often used when a problem has a recursive structure, and the
solutions to the same subproblems are needed multiple times. Dynamic programming
avoids redundant computations by storing the results of intermediate computations
and reusing them when needed.
The coin change problem is a classic problem in dynamic programming. Given a set
of coin denominations and a target amount, the task is to find the number of ways to
make change for the target amount using any combination of the given coins
PROGRAM:
def count_ways_to_make_change(coins, amount):
# Create a table to store the number of ways to make each amount
# Initialize all values to 0
dp = [0] * (amount + 1)
# The final value in dp[amount] represents the number of ways to make change
for 'amount'
return dp[amount]
if __name__ == "__main__":
coin_denominations = [1, 2, 5]
target_amount = 5
ways = count_ways_to_make_change(coin_denominations, target_amount)
print(f"Number of ways to make change for {target_amount} is {ways}")
OUTPUT:
EX NO:5
DATE: DYNAMIC PROGRAM – (b) WARSHALL PROBLEM
AIM:
To implement a Warshall problem using Dynamic algorithm
PROCEDURE:
The Floyd-Warshall algorithm is a dynamic programming algorithm used for finding
the shortest paths between all pairs of vertices in a weighted graph, which may contain
negative weight edges. The algorithm was proposed by Robert W. Floyd and Stephen
Warshall independently.
PROGRAM:
def floyd_warshall(graph):
# Number of vertices in the graph
num_vertices = len(graph)
return dist
if __name__ == "__main__":
# Example graph represented as an adjacency matrix
graph = [
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
[1, 0, 0, 0]
]
OUTPUT:
Transitive Closure:
[0, 1, 2, 3]
[3, 0, 1, 2]
[2, 3, 0, 1]
[1, 2, 3, 0]
RESULT:
EX NO:5
DATE: DYNAMIC PROGRAM – (c) FLOYD’S PROBLEM
AIM:
To implement a Floyd’s problem using Dynamic algorithm
PROCEDURE:
The Floyd-Warshall algorithm is a dynamic programming algorithm used for
finding the shortest paths between all pairs of vertices in a weighted graph, which may
contain negative weight edges. The algorithm was proposed by Robert W. Floyd and
Stephen Warshall independently.
PROGRAM:
def floyd_warshall(graph):
num_vertices = len(graph)
# Initialize the distance matrix with the graph's adjacency matrix
dist = [row[:] for row in graph]
for k in range(num_vertices):
for i in range(num_vertices):
for j in range(num_vertices):
# If there is a shorter path from i to j through vertex k
if dist[i][j] > dist[i][k] + dist[k][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist
if __name__ == "__main__":
# Example graph represented as an adjacency matrix
graph = [
[0, 5, float('inf'), 10],
[float('inf'), 0, 3, float('inf')],
[float('inf'), float('inf'), 0, 1],
[float('inf'), float('inf'), float('inf'), 0]
]
# Find the shortest paths using Floyd's algorithm
shortest_paths = floyd_warshall(graph)
# Print the shortest paths matrix
print("Shortest Paths:")
for row in shortest_paths:
print([f if f != float('inf') else "∞" for f in row])
OUTPUT:
Shortest Paths:
[0, 5, 8, 9]
['∞', 0, 3, 4]
['∞', '∞', 0, 1]
['∞', '∞', '∞', 0]
RESULT:
EX NO:5
DATE: DYNAMIC PROGRAM – (d) KNAPSACK PROBLEM
AIM:
To implement a Knapsack problem using Dynamic algorithm
PROCEDURE:
Given a set of items, each with a weight and a value, determine the maximum value
that can be obtained by selecting a subset of the items, such that the sum of the weights
of the selected items does not exceed a given capacity.
In other words, you have a knapsack with a limited weight capacity, and you want to
maximize the total value of the items you can carry in the knapsack without exceeding
its capacity.
PROGRAM:
def knapsack(items, capacity):
n = len(items)
# Initialize a table to store the maximum values for different capacities
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
if __name__ == "__main__":
items = [(60, 10), (100, 20), (120, 30)]
capacity = 50
OUTPUT:
EX NO:6
DATE: GREEDY TECHNIQUE – (a) DIJKSTRA’S PROGRAM
AIM:
To implement a Dijkstra’s Program using Greedy Technique
PROCEDURE:
A greedy algorithm is an algorithmic paradigm that makes locally optimal choices
at each stage with the hope of finding a global optimum. In other words, it chooses the
best option at each step without worrying about the consequences in the future. The
strategy is to make the locally optimal choice at each stage, hoping that this will lead to
a globally optimal solution.
Greedy algorithms are often simple and easy to implement. They are particularly useful
for optimization problems where making a series of locally optimal choices leads to an
overall optimal solution. However, the method does not always guarantee a globally
optimal solution for every problem.
# Create a priority queue to keep track of nodes with their tentative distances
priority_queue = [(0, start)]
while priority_queue:
# Get the node with the smallest tentative distance
current_distance, current_node = heapq.heappop(priority_queue)
# If this path is shorter than the recorded distance, update the distance
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(priority_queue, (distance, neighbor))
return distances
# Example usage:
graph = {
'A': {'B': 1, 'C': 4},
'B': {'A': 1, 'C': 2, 'D': 5},
'C': {'A': 4, 'B': 2, 'D': 1},
'D': {'B': 5, 'C': 1}
}
start_node = 'A'
shortest_distances = dijkstra(graph, start_node)
RESULT:
EX NO:6
DATE: GREEDY TECHNIQUE – (b) HUFFMAN TREE AND CODE
AIM:
To implement a Huffman Tree and code using Greedy algorithm
PROGRAM :
import heapq
from collections import defaultdict
class Node:
def __init__(self, char, frequency):
self.char = char
self.frequency = frequency
self.left = None
self.right = None
def build_huffman_tree(data):
char_frequency = defaultdict(int)
for char in data:
char_frequency[char] += 1
return heap[0]
def huffman_encoding(data):
if not data:
return '', None
root = build_huffman_tree(data)
huffman_codes = {}
build_huffman_codes(root, '', huffman_codes)
current_node = root
decoded_data = ''
for bit in encoded_data:
if bit == '0':
current_node = current_node.left
else:
current_node = current_node.right
return decoded_data
if __name__ == "__main__":
data = "hello, world!"
OUTPUT:
RESULT:
EX NO:7
DATE: ITREATIVE METHOD – SIMPLEX METHOD
AIM:
To implement a Simplex method using Iterative method
PROCEDURE:
The simplex method is an iterative optimization algorithm used for solving linear
programming problems. It is a popular method for finding the optimal solution to
problems
PROGRAM:
import numpy as np
def simplex_method(c, A, b):
m, n = A.shape
c = np.array(c)
A = np.array(A)
b = np.array(b)
# Initial tableau
tableau = np.zeros((m + 1, n + 1))
tableau[:m, :n] = A
tableau[:m, -1] = b
tableau[-1, :-1] = -c
while np.any(tableau[-1, :-1] < 0):
pivot_column = np.argmin(tableau[-1, :-1])
ratios = tableau[:m, -1] / tableau[:m, pivot_column]
pivot_row = np.argmin(ratios)
pivot_element = tableau[pivot_row, pivot_column]
# Perform pivot operation
tableau[pivot_row, :] /= pivot_element
for i in range(m + 1):
if i == pivot_row:
continue
tableau[i, :] -= tableau[i, pivot_column] * tableau[pivot_row, :]
optimal_solution = tableau[-1, -1]
solution = dict()
for i in range(n):
non_zero_indices = np.nonzero(tableau[:m, i])[0]
if len(non_zero_indices) == 1:
solution[i] = tableau[non_zero_indices[0], -1]
else:
solution[i] = 0
return optimal_solution, solution
# Example usage
c = [3, 2] # Coefficients of the objective function to maximize
A = np.array([[2, 1], [1, 1]]) # Coefficients of the inequality constraints
b = [4, 3] # Right-hand side of the inequality constraints
optimal_value, solution = simplex_method(c, A, b)
print("Optimal value:", optimal_value)
print("Solution:", solution)
OUTPUT:
Optimal value: 7.0
Solution: {0: 1.0, 1: 2.0}
RESULT:
EX NO:8
DATE: BACK TRACXKING METHOD – (A) 8 QUEENS PROBLEM
AIM:
To implement an 8Queens problem using Back Tracking method
PROCEDURE:
The backtracking algorithm can be described using a recursive approach. It
systematically explores the search space, and when it finds that a solution cannot be
completed, it backtracks to the last valid configuration and continues the search.
The N-Queens problem is a classic problem in computer science and
combinatorial optimization. The problem is to place N chess queens on an �×�N×N
chessboard in such a way that no two queens threaten each other. This means that no
two queens can be in the same row, column, or diagonal. Backtracking is a common
technique used to solve the N-Queens problem.
PROGRAM:
def is_safe(board, row, col):
# Check the left side of the current row
for i in range(col):
if board[row][i] == 1:
return False
# Check upper diagonal on the left side
for i, j in zip(range(row, -1, -1), range(col, -1, -1)):
if board[i][j] == 1:
return False
# Check lower diagonal on the left side
for i, j in zip(range(row, len(board), 1), range(col, -1, -1)):
if board[i][j] == 1:
return False
return True
def solve_n_queens(board, col):
# Base case: If all queens are placed successfully
if col >= len(board):
return True
for i in range(len(board)):
if is_safe(board, i, col):
# Place a queen
board[i][col] = 1
# Recur to place the rest of the queens
if solve_n_queens(board, col + 1):
return True
# If placing a queen doesn't lead to a solution, backtrack
board[i][col] = 0
# If the queen cannot be placed in any row in this column, return False
return False
def print_board(board):
for row in board:
print(" ".join("Q" if cell == 1 else "." for cell in row))
def solve_8_queens():
n = 8 # 8x8 chessboard
board = [[0 for _ in range(n)] for _ in range(n)]
if not solve_n_queens(board, 0):
print("No solution exists.")
else:
print_board(board)
if __name__ == "__main__":
solve_8_queens()
OUTPUT:
Q.......
......Q.
....Q...
.......Q
.Q......
...Q....
.....Q..
..Q.....
RESULT:
EX NO:8
DATE: BACKTRACKING – (b) SUBSET SUM PROBLEM
AIM:
To implement a Subset Sum problem using Backtracking method
PROCEDURE:
A set of positive integers and a target sum, the task is to determine whether there
exists a subset of the given set whose elements add up to the target sum.
PROGRAM:
def is_subset_sum(arr, n, target_sum, subset=[]):
if target_sum == 0:
print("Subset with the target sum found:", subset)
return True
if n == 0 and target_sum != 0:
return False
# If the last element is greater than the target sum, exclude it
if arr[n - 1] > target_sum:
return is_subset_sum(arr, n - 1, target_sum, subset)
# Consider the last element and check if a solution can be found with it
include = is_subset_sum(arr, n - 1, target_sum - arr[n - 1], subset + [arr[n - 1]])
# Exclude the last element and check if a solution can be found without it
exclude = is_subset_sum(arr, n - 1, target_sum, subset)
# Return True if either including or excluding the last element results in a solution
return include or exclude
def subset_sum(arr, target_sum):
n = len(arr)
if not is_subset_sum(arr, n, target_sum):
print("No subset with the target sum found.")
if __name__ == "__main__":
arr = [1, 3, 5, 7, 9]
target_sum = 8
subset_sum(arr, target_sum)
OUTPUT:
Subset with the target sum found: [7, 1]
Subset with the target sum found: [5, 3]
RESULT:
EX NO:9
DATE: BRANCH AND BNOUND– (a) TRAVELLING SALESPERSON PROBLEM
AIM:
To implement a Travelling Sales person Problem using Branch and Bound
method
PROCEDURE:
Branch and Bound is an algorithmic technique for solving optimization problems,
typically combinatorial optimization problems. The main idea is to divide the problem
into smaller subproblems and solve them individually, keeping track of the best solution
found so far. The technique includes bounding the search space, pruning branches that
cannot lead to better solutions, and exploring promising regions of the solution space.
Given a set of cities and the distances between each pair of cities, the task is
to find the shortest possible tour that visits each city exactly once and returns to the
original city.
PROGRAM:
# Python3 program to solve
# Traveling Salesman Problem using
# Branch and Bound.
import math
maxsize = float('inf')
# Function to copy temporary solution
# to the final solution
def copyToFinal(curr_path):
final_path[:N + 1] = curr_path[:]
final_path[N] = curr_path[0]
# Function to find the minimum edge cost
# having an end at the vertex i
def firstMin(adj, i):
min = maxsize
for k in range(N):
if adj[i][k] < min and i != k:
min = adj[i][k]
return min
# function to find the second minimum edge
# cost having an end at the vertex i
def secondMin(adj, i):
first, second = maxsize, maxsize
for j in range(N):
if i == j:
continue
if adj[i][j] <= first:
second = first
first = adj[i][j]
# Driver code
OUTPUT:
Minimum cost : 80
Path Taken : 0 1 3 2 0
RESULT:
EX NO:9
DATE: BRANCH AND BNOUND– (b) ASSIGNMENT PROBLEM
AIM:
To implement an Assignment problem using Branch and Bound method
PROCEDURE:
The Assignment Problem is a combinatorial optimization problem that involves
finding the most cost-effective way to assign a set of tasks to a set of agents. Each task
must be assigned to exactly one agent, and each agent must be assigned exactly one
task. The goal is to minimize the total cost or maximize the total profit of the
assignment.
PROGRAM:
import sys
def assign(cost_matrix):
n = len(cost_matrix)
def subtract_min(row):
for i in range(n):
min_val = min(row)
if min_val != sys.maxsize:
for j in range(n):
if row[j] != sys.maxsize:
row[j] -= min_val
return row
def subtract_min_col(matrix):
for i in range(n):
col = [matrix[j][i] for j in range(n)]
min_val = min(val for val in col if val != sys.maxsize)
if min_val != sys.maxsize:
for j in range(n):
if matrix[j][i] != sys.maxsize:
matrix[j][i] -= min_val
return matrix
def find_zeros(matrix):
zero_positions = []
for i in range(n):
for j in range(n):
if matrix[i][j] == 0:
zero_positions.append((i, j))
return zero_positions
def assign_recursive(matrix, assigned, row_covered, col_covered, depth, total_cost):
if depth == n:
return total_cost
zero_positions = find_zeros(matrix)
if not zero_positions:
min_val = sys.maxsize
for i in range(n):
if not row_covered[i]:
for j in range(n):
if not col_covered[j]:
min_val = min(min_val, matrix[i][j])
for i in range(n):
if not row_covered[i]:
for j in range(n):
if not col_covered[j]:
matrix[i][j] -= min_val
total_cost += min_val
col_covered = [False] * n
row_covered = [False] * n
return assign_recursive(matrix, assigned, row_covered, col_covered, depth,
total_cost)
i, j = zero_positions[0]
assigned[i] = j
row_covered[i] = True
col_covered[j] = True
for k in range(n):
if k != i:
matrix[k][j] = sys.maxsize
if k != j:
matrix[i][k] = sys.maxsize
return assign_recursive(matrix, assigned, row_covered, col_covered, depth + 1,
total_cost)
# Step 1: Subtract minimum value from each row
for i in range(n):
cost_matrix[i] = subtract_min(cost_matrix[i])
# Step 2: Subtract minimum value from each column
cost_matrix = subtract_min_col(cost_matrix)
assigned = [-1] * n # Array to store assigned jobs
row_covered = [False] * n
col_covered = [False] * n
total_cost = assign_recursive(cost_matrix, assigned, row_covered, col_covered, 0, 0)
return assigned, total_cost
# Example usage:
cost_matrix = [
[9, 11, 14, 8],
[6, 15, 13, 7],
[12, 13, 6, 8],
[14, 9, 10, 12]
]
OUTPUT:
Assigned Jobs: [3, -1, -1, -1]
Total Cost: 0
RESULT: