0% found this document useful (0 votes)
41 views42 pages

Lab Manual DAA

The document describes algorithms for calculating factorials recursively and non-recursively, and analyzing their order of growth. It then discusses Strassen's matrix multiplication algorithm, which uses divide and conquer to multiply matrices more efficiently. Finally, it explains topological sorting, which uses decrease and conquer by performing depth-first search on an input graph to output nodes in topologically sorted order.

Uploaded by

dharun0704
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
41 views42 pages

Lab Manual DAA

The document describes algorithms for calculating factorials recursively and non-recursively, and analyzing their order of growth. It then discusses Strassen's matrix multiplication algorithm, which uses divide and conquer to multiply matrices more efficiently. Finally, it explains topological sorting, which uses decrease and conquer by performing depth-first search on an input graph to output nodes in topologically sorted order.

Uploaded by

dharun0704
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 42

EXP.

NO:1
IMPLEMENT RECURSIVE AND NON-RECURSIVE ALGORITHMS AND STUDY THE
ORDER OF GROWTH FROM LOG2N TO N!.
Algorithm:
1. Define the function Factorial that takes an integer parameter n.
2. Check if n is 0 or 1. If so, return 1 because the factorial of 0 and 1 is 1.
3. If n is not 0 or 1, recursively call the Factorial function with the argument n - 1.
This means calculating the factorial of the previous number.
4. Multiply n by the result obtained from the recursive call and return the final result.

Program:

Recursive Algorithm:
def recursive_factorial(n):
if n == 0:
return 1
else:
return n * recursive_factorial(n - 1)
# Example usage
n=5
recursive_result = recursive_factorial(n)
print("Recursive Factorial Result:", recursive_result)

OUTPUT:

Recursive Factorial Result: 120

The recursive algorithm calculates the factorial of a number n by recursively multiplying n with the
factorial of n-1. The base case is when n reaches 0, in which case the function returns 1.

Non-Recursive Algorithm:

Algorithm:

1. Define the function NonRecursiveFactorial that takes an integer parameter n.


2. Initialize a variable result to 1. This variable will store the factorial result.
3. Use a loop to iterate from 1 to n.
4. In each iteration, multiply the current value of result by the loop variable i.
5. After the loop completes, return the final value of result.

pythonCopy code:

def non_recursive_factorial(n):
result = 1
for i in range(1, n + 1):
result *= i
return result

# Example usage
n=5
non_recursive_result = non_recursive_factorial(n)
print("Non-Recursive Factorial Result:", non_recursive_result)

OUTPUT:

Non-Recursive Factorial Result: 120

The non-recursive algorithm calculates the factorial of a number n by using a loop. It initializes the
factorial variable to 1 and then iteratively multiplies it with each number from 1 to n.

STUDY OF ORDER OF GROWTH:

The order of growth refers to how the runtime of an algorithm increases as the input size (n)
grows. Let's analyze the order of growth of the factorial algorithms:

Recursive Algorithm:

● The recursive algorithm has a time complexity of O(n) because it makes n


recursive calls, each performing constant work.
● The space complexity of the recursive algorithm is O(n) as well, because it
requires n stack frames to keep track of the recursive calls.
● Non-Recursive Algorithm:
● The non-recursive algorithm has a time complexity of O(n) because it performs n
multiplications in the loop.
● The space complexity of the non-recursive algorithm is O(1) because it only uses
a constant amount of space for the factorial variable.

Order of Growth from log2n to n!:

The order of growth from log2n to n! can be represented as follows:

log2n < n < nlog2n < n^2 < n^3 < ... < 2^n < n!

VIVA QUESTIONS:

1. What is a recursive algorithm?

A recursive algorithm is an algorithm that solves a problem by solving smaller instances of the same
problem. It involves breaking down a problem into smaller subproblems and solving them
recursively until reaching a base case, which is a simple case that can be solved directly.

2. What is a non-recursive algorithm?

A non-recursive algorithm is an algorithm that solves a problem without using recursion. It typically
uses iterative techniques such as loops and stack data structures to achieve the desired result.

3. What are the advantages of recursive algorithms?

Recursive algorithms are often simpler and more concise than their non-recursive counterparts, as
they express the problem in terms of smaller subproblems. They can be easier to understand and
implement for problems that naturally exhibit recursive structure.

4. What are the advantages of non-recursive algorithms?

Non-recursive algorithms can be more efficient in terms of time and space complexity compared to
recursive algorithms. They usually involve explicit control flow and avoid the overhead of function
calls, making them more suitable for large-scale problems.

5. How do you analyze the order of growth for algorithms?

To analyze the order of growth for algorithms, you can consider the time or space complexity. Time
complexity refers to how the algorithm's execution time increases with the input size, while space
complexity refers to how the algorithm's memory usage grows with the input size. Common
notations used to represent the order of growth include O(), Ω(), and Θ().

6. What is the order of growth from log2N to N!?

The order of growth from log2N to N! is as follows:

O(log N): logarithmic complexity

O(N): linear complexity

O(N log N): linearithmic complexity

O(N^2): quadratic complexity

O(N!): factorial complexity (the highest growth rate)

EXP.NO:2 DIVIDE AND CONQUER - STRASSEN’S MATRIX MULTIPLICATION

Algorithm:

1. The StrassenMatrixMultiply function takes two matrices A and B as input.


2. It checks if the dimensions of A and B are 1x1. If so, it directly returns the product
of A and B.
3. Otherwise, it divides the matrices A and B into four equal submatrices.
4. It recursively calls the StrassenMatrixMultiply function on the appropriate
submatrices to calculate the seven intermediate products P1 to P7.
5. It uses these intermediate products to calculate the resulting submatrices C11,
C12, C21, and C22.
6. Finally, it combines these submatrices into a single resulting matrix C and returns
it.

Program :

import numpy as np

def strassen_matrix_multiply(A, B):


# Check if matrices are compatible for multiplication
if A.shape[1] != B.shape[0]:
raise ValueError("Matrices are not compatible for multiplication")

# Base case: If matrices are 1x1, perform simple multiplication


if A.shape[0] == 1 and A.shape[1] == 1 and B.shape[0] == 1 and B.shape[1] == 1:
return np.dot(A, B)

# Split matrices into quadrants


n = A.shape[0]
m = n // 2

A11 = A[:m, :m]


A12 = A[:m, m:]
A21 = A[m:, :m]
A22 = A[m:, m:]

B11 = B[:m, :m]


B12 = B[:m, m:]
B21 = B[m:, :m]
B22 = B[m:, m:]

# Recursive steps
P1 = strassen_matrix_multiply(A11 + A22, B11 + B22)
P2 = strassen_matrix_multiply(A21 + A22, B11)
P3 = strassen_matrix_multiply(A11, B12 - B22)
P4 = strassen_matrix_multiply(A22, B21 - B11)
P5 = strassen_matrix_multiply(A11 + A12, B22)
P6 = strassen_matrix_multiply(A21 - A11, B11 + B12)
P7 = strassen_matrix_multiply(A12 - A22, B21 + B22)

# Combine the results


C11 = P1 + P4 - P5 + P7
C12 = P3 + P5
C21 = P2 + P4
C22 = P1 - P2 + P3 + P6

# Concatenate the quadrants to form the resulting matrix


result = np.vstack((np.hstack((C11, C12)), np.hstack((C21, C22))))
return result

# Example usage
A = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]])

B = np.array([[17, 18, 19, 20],


[21, 22, 23, 24],
[25, 26, 27, 28],
[29, 30, 31, 32]])

product = strassen_matrix_multiply(A, B)
print("Product of A and B:")
print(product)

OUTPUT:

Product of A and B:

[[ 250 260 270 280]

[ 618 644 670 696]

[ 986 1028 1070 1112]

[1354 1412 1470 1528]]

VIVA QUESTIONS:

1. What is Strassen's Matrix Multiplication algorithm?

Strassen's Matrix Multiplication algorithm is an efficient divide and conquer algorithm used to
multiply two matrices. It reduces the number of required multiplications by breaking down the
matrices into smaller submatrices and recursively computing partial products.
2. How does Strassen's Matrix Multiplication algorithm work?

Strassen's algorithm works by dividing each matrix into four submatrices of equal size,
recursively multiplying these submatrices using seven multiplications instead of the usual eight, and
combining the results to obtain the final product.
3. What are the advantages of Strassen's Matrix Multiplication algorithm?
Strassen's algorithm reduces the number of multiplications compared to the traditional matrix
multiplication algorithm, resulting in a lower time complexity. It is particularly efficient for large
matrices, leading to improved performance in certain scenarios.
4. What is the time complexity of Strassen's Matrix Multiplication algorithm?

The time complexity of Strassen's algorithm is O(n^log2(7)), where n is the dimension of the
matrices being multiplied. This is a more efficient time complexity compared to the traditional
matrix multiplication algorithm, which has a time complexity of O(n^3).
5. Are there any limitations or considerations when using Strassen's Matrix
Multiplication?

Strassen's algorithm is more efficient for large matrices, but it may not always be the fastest
algorithm for smaller matrices due to the overhead of recursion and additional operations involved.
Additionally, it requires matrices to have dimensions that are powers of 2.

EXP.NO :3 DECREASE AND CONQUER - TOPOLOGICAL SORTING

Algorithm :

1. Initialize an empty list sorted_nodes to store the topologically sorted nodes.


2. Initialize a set visited to keep track of the visited nodes.
3. For each node v in the graph:
4. If v is not visited, apply the following steps:
5. Call a Depth-First Search (DFS) function on v:
6. Mark v as visited by adding it to the visited set.
7. Recursively call the DFS function on each unvisited neighbor of v.
8. After visiting all neighbors, add v to the sorted_nodes list.
9. Finally, reverse the sorted_nodes list to get the topologically sorted order.
10. Return the sorted_nodes list.

Program:

from collections import defaultdict

def topological_sort(graph):
def dfs(node):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
sorted_nodes.append(node)
visited = set()
sorted_nodes = []
for node in graph:
if node not in visited:
dfs(node)
return sorted_nodes[::-1]

# Example usage
graph = defaultdict(list)
graph[1] = [2, 3]
graph[2] = [4]
graph[3] = [4, 5]
graph[4] = [6]
graph[5] = []
graph[6] = []
sorted_order = topological_sort(graph)
print("Topological Sort Order:")
print(sorted_order)

OUTPUT:

Topological Sort Order:

[1, 3, 5, 2, 4, 6]

VIVA QUESTIONS :

1. What is Topological Sorting?


Topological Sorting is a linear ordering of the vertices of a directed graph such that for every
directed edge (u, v), vertex u comes before vertex v in the ordering. It represents a valid sequence of
tasks or dependencies in a directed acyclic graph (DAG).
2. What is the goal of Topological Sorting?
The goal of Topological Sorting is to find a valid ordering of vertices in a directed graph that
respects the dependencies between the vertices. This ordering can be used to determine a feasible
order of performing tasks or to detect cycles in the graph.
3. How does Decrease and Conquer approach solve the Topological Sorting
problem?
In the Decrease and Conquer approach for Topological Sorting, we reduce the problem by removing
a vertex along with its outgoing edges from the graph and recursively performing the same process
on the remaining graph. This reduction decreases the size of the problem until a base case is
reached.
4. What is the base case for the Decrease and Conquer Topological Sorting
algorithm?
The base case for the Decrease and Conquer Topological Sorting algorithm is reached when the
graph becomes empty, indicating that all vertices have been visited and processed. At this point, the
algorithm returns the sorted vertices.
5. How does the Decrease and Conquer algorithm handle the decrease step in
Topological Sorting?
In the Decrease step, the algorithm selects a vertex with no incoming edges (or dependencies) and
removes it from the graph along with its outgoing edges. This reduces the problem size by
decreasing the number of remaining vertices and edges to be processed.
6. What is the time complexity of the Decrease and Conquer algorithm for
Topological Sorting?
The time complexity of the Decrease and Conquer algorithm for Topological Sorting depends on the
graph representation and the specific implementation. In the worst-case scenario, it can have a time
complexity of O(V + E), where V is the number of vertices and E is the number of edges in the graph.
7. What is a possible application of Topological Sorting?
Topological Sorting has various applications, including task scheduling, dependency resolution, and
job sequencing. It can be used to determine the order in which tasks or dependencies need to be
executed or to detect circular dependencies in a graph

EXP.NO:4 TRANSFORM AND CONQUER - HEAP SORT

Algorithm:

1. Build a max heap:


● Start from the last non-leaf node (i.e., n//2 - 1) to the root (i.e., 0).
● For each node, perform heapify down operation to maintain the max heap
property. Heapify down compares the node with its children and swaps them if
necessary to satisfy the property.
● After heapifying all nodes, the input array is transformed into a max heap.
2. Perform sorting:
● Initialize a loop from n-1 to 1 (exclusive) since the last element is already sorted
after building the max heap.
● Swap the root (maximum element) with the last element of the heap and
decrease the heap size by 1.
3. Heapify down the new root to restore the max heap property.
● Repeat the above steps until the heap size becomes 1.
● After each iteration, the maximum element is placed at the end of the array,
forming a sorted region from the end towards the beginning.
● The array is now sorted in ascending order.

Program:
def heapify(arr, n, i):
largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[left] > arr[largest]:
largest = left
if right < n and arr[right] > arr[largest]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
def heap_sort(arr):
n = len(arr)

# Build max heap


for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)

# Perform sorting
for i in range(n - 1, 0, -1):
arr[0], arr[i] = arr[i], arr[0]
heapify(arr, i, 0)
# Example usage
arr = [12, 11, 13, 5, 6, 7]
heap_sort(arr)
print("Sorted array:")
print(arr)

OUTPUT:

Sorted array:

[5, 6, 7, 11, 12, 13]

VIVA QUESTIONS:

1. What is Heap Sort?

Heap Sort is an efficient comparison-based sorting algorithm that uses the concept of a binary heap
data structure. It involves building a heap from the given array and repeatedly extracting the
maximum (for ascending order) or minimum (for descending order) element to sort the array.

2. How does Heap Sort work?

Heap Sort works by first building a binary heap from the given array, which can be done in O(N) time
complexity. Then, it repeatedly extracts the root element of the heap (the maximum or minimum,
depending on the desired order) and places it at the end of the array. After each extraction, the heap
is restored to maintain its properties, and the process continues until the entire array is sorted.

3. What are the steps involved in Heap Sort?

The steps involved in Heap Sort are as follows:

● Build a binary heap from the input array.


● Repeatedly extract the root element from the heap and place it at the end of the
array.
● Restore the heap properties after each extraction.
● Continue this process until the entire array is sorted.
4. What is the time complexity of Heap Sort?

The time complexity of Heap Sort is O(N log N), where N is the number of elements in the array. Both
the building of the heap and the extraction/restoration steps take O(log N) time complexity, and
these operations are performed N times

5. What are the advantages of Heap Sort?

Heap Sort has several advantages, including:

● It has a guaranteed worst-case time complexity of O(N log N), making it efficient
for large datasets.
● It is an in-place sorting algorithm, meaning it does not require additional memory
beyond the input array.
● It is stable, preserving the relative order of equal elements.
6. What are the limitations of Heap Sort?

Heap Sort has a few limitations, including:

● It is not a stable sorting algorithm, meaning it may change the order of equal
elements.
● It has a relatively high constant factor and requires more comparisons than
some other sorting algorithms, which can impact performance for small datasets.
7. Can you explain the process of building a binary heap in Heap Sort?

Building a binary heap involves starting from the middle index of the array and repeatedly percolating
down each element to its proper position in the heap. The percolation process compares the
element with its children and swaps it with the larger (for max heap) or smaller (for min heap) child
until the heap property is satisfied.
EXP.NO:5(a) COIN CHANGE PROBLEM USING DYNAMIC PROGRAMMING

Algorithm:

1. Define the problem:


● Given a target amount and a set of coin denominations, determine the
minimum number of coins needed to make up the target amount.
2. Initialize an array dp of size target + 1 with a large value, except for dp[0] which is
set to 0. This array will store the minimum number of coins needed to make up
each amount from 0 to the target.
3. Iterate over each coin denomination coin in the set of coins:
● For each coin, iterate from coin to target:
● Update dp[i] with the minimum value between dp[i] and dp[i - coin] + 1.
● dp[i - coin] represents the minimum number of coins needed to make up the
remaining amount after subtracting the current coin.
● Adding 1 to dp[i - coin] accounts for the current coin being used.
4. The minimum number of coins needed to make up the target amount will be
stored in dp[target]. If dp[target] is still the initial large value, it means it is not
possible to make up the target amount using the given coin denominations.
5. Optionally, to retrieve the actual combination of coins that make up the target
amount:
● Initialize an empty list coins_used.
● Starting from target, backtrack by subtracting the selected coins until reaching 0:
● If dp[i] - dp[i - coin] == 1, it means the current coin was used.
● Append the coin denomination to coins_used and update i to i - coin.
● Reverse coins_used to get the combination in the correct order.
6. Return dp[target] as the minimum number of coins needed to make up the target
amount. Optionally, return coins_used as well.

Program :
def coin_change(coins, target):
dp = [float('inf')] * (target + 1)
dp[0] = 0

for coin in coins:


for i in range(coin, target + 1):
dp[i] = min(dp[i], dp[i - coin] + 1)

if dp[target] == float('inf'):
return -1

# Retrieve coin combination (optional)


coins_used = []
i = target
while i > 0:
for coin in coins:
if i - coin >= 0 and dp[i] - dp[i - coin] == 1:
coins_used.append(coin)
i -= coin
break

coins_used.reverse()

return dp[target], coins_used

# Example usage
coins = [1, 2, 5]
target = 11

min_coins, coin_combination = coin_change(coins, target)

print("Minimum number of coins needed:", min_coins)

print("Coin combination:", coin_combination)

OUTPUT:

Minimum number of coins needed: 3

Coin combination: [5, 5, 1]

VIVA QUESTIONS:

1. What is the Coin Change Problem?

The Coin Change Problem is a classic dynamic programming problem that involves finding the
minimum number of coins needed to make a given amount of money. Given a set of coin
denominations and a target amount, the goal is to determine the minimum number of coins required
to make up that amount.

2. How does Dynamic Programming solve the Coin Change Problem?


Dynamic Programming solves the Coin Change Problem by breaking it down into smaller
subproblems and solving them iteratively. It builds an optimal solution for each subproblem based
on previously computed solutions, gradually solving larger subproblems until reaching the target
amount.

3. What is the approach used in Dynamic Programming for the Coin Change
Problem?

The approach used in Dynamic Programming for the Coin Change Problem is known as the
"bottom-up" approach. It involves building a table or an array to store the optimal solutions for
subproblems, starting from the smallest possible subproblem and progressively filling the table until
reaching the target amount.

4. How does the Dynamic Programming algorithm initialize the table for the Coin
Change Problem?

The table for the Coin Change Problem is initialized with values representing an invalid or
unreachable state, such as infinity or a large value. This ensures that the algorithm can properly
track the minimum number of coins required to make each amount.

5. What is the recurrence relation used in the Dynamic Programming algorithm for
the Coin Change Problem?

The recurrence relation for the Coin Change Problem states that the minimum number of coins
required to make a certain amount is the minimum between taking the current coin and considering
the remaining amount or not taking the current coin and considering the remaining amount with
previously used coins. This relation is used to fill the table iteratively.

6. What is the time complexity of the Dynamic Programming algorithm for the
Coin Change Problem?

The time complexity of the Dynamic Programming algorithm for the Coin Change Problem is O(A *
N), where A is the target amount and N is the number of coin denominations. It iterates over each
amount and each coin denomination once to fill the table.


EXP.NO:5(b) WARSHALL’S AND FLOYD‘S ALGORITHMS USING DYNAMIC
PROGRAMMING
Algorithm:

Algorithm for Warshall's Algorithm using Dynamic Programming:

1. Define the problem:


● Given a directed graph represented by an adjacency matrix, compute the
transitive closure of the graph. The transitive closure matrix indicates whether
there is a path from each vertex to every other vertex.
2. Initialize the transitive closure matrix tc as a copy of the adjacency matrix.
3. Perform dynamic programming iterations:
● For each vertex k from 0 to the number of vertices - 1:
● For each vertex i from 0 to the number of vertices - 1:
● For each vertex j from 0 to the number of vertices - 1:
● Update tc[i][j] to tc[i][j] OR (tc[i][k] AND tc[k][j]).
● If there is a direct path from i to j (i.e., tc[i][j] = 1), or if there is a path from i to k
and from k to j (i.e., tc[i][k] = 1 and tc[k][j] = 1), then update tc[i][j] to 1.
4. The resulting tc matrix represents the transitive closure of the graph.
5. The time complexity of Warshall's Algorithm is O(V^3), where V is the number of
vertices in the graph. The space complexity is O(V^2) to store the transitive
closure matrix.

Algorithm for Floyd's Algorithm using Dynamic Programming:

1. Define the problem:


● Given a directed graph represented by an adjacency matrix, compute the shortest
distances between all pairs of vertices in the graph.
2. Initialize the distance matrix dist as a copy of the adjacency matrix.
● If there is no direct edge between vertices i and j, set dist[i][j] to a large value
representing infinity.
3. Perform dynamic programming iterations:
● For each vertex k from 0 to the number of vertices - 1:
● For each vertex i from 0 to the number of vertices - 1:
● For each vertex j from 0 to the number of vertices - 1:
● Update dist[i][j] to the minimum between dist[i][j] and dist[i][k] + dist[k][j].
● If the distance from i to j is larger than the distance from i to k plus the distance
from k to j, update the distance.
4. The resulting dist matrix represents the shortest distances between all pairs of
vertices in the graph.

Program :

Code for Warshall's Algorithm:

def warshall(adj_matrix):
num_vertices = len(adj_matrix)
tc = [row[:] for row in adj_matrix] # Create a copy of the adjacency matrix

for k in range(num_vertices):
for i in range(num_vertices):
for j in range(num_vertices):
tc[i][j] = tc[i][j] or (tc[i][k] and tc[k][j])

return tc

# Example usage
adj_matrix = [[0, 1, 0, 0],
[0, 0, 0, 1],
[0, 0, 0, 0],
[1, 0, 1, 0]]

transitive_closure = warshall(adj_matrix)
print("Transitive Closure Matrix:")
for row in transitive_closure:
print(row)

OUTPUT:

Transitive Closure Matrix:

[1, 1, 1, 1]

[1, 1, 1, 1]

[0, 0, 0, 0]

[1, 1, 1, 1]

VIVA QUESTIONS:
1. What is Warshall's Algorithm?

Warshall's Algorithm is a dynamic programming algorithm used to compute the transitive closure
of a directed graph. It determines the reachability between all pairs of vertices in a graph by
iteratively considering intermediate vertices.

2. How does Warshall's Algorithm work?

Warshall's Algorithm works by initializing a matrix to represent the reachability between pairs of
vertices. It then iteratively updates the matrix by considering an intermediate vertex and updating the
reachability based on whether a path exists between two vertices that goes through the intermediate
vertex.

3. What is the time complexity of Warshall's Algorithm?

The time complexity of Warshall's Algorithm is O(N^3), where N is the number of vertices in the
graph. It involves three nested loops that iterate over all vertices, making it cubic in the worst case.

4. What is Floyd's Algorithm?

Floyd's Algorithm is a dynamic programming algorithm used to compute the shortest paths between
all pairs of vertices in a weighted directed graph. It determines the shortest distance between all
pairs of vertices by iteratively considering intermediate vertices.

5. How does Floyd's Algorithm work?

Floyd's Algorithm works by initializing a matrix to represent the shortest distances between pairs of
vertices. It then iteratively updates the matrix by considering an intermediate vertex and updating the
shortest distances based on whether a shorter path exists between two vertices that goes through
the intermediate vertex.

6. What is the time complexity of Floyd's Algorithm?

The time complexity of Floyd's Algorithm is O(N^3), where N is the number of vertices in the graph. It
involves three nested loops that iterate over all vertices, making it cubic in the worst case.

7. What are the differences between Warshall's and Floyd's Algorithms?

Warshall's Algorithm computes the transitive closure of a directed graph, determining reachability
between all pairs of vertices. Floyd's Algorithm computes the shortest paths between all pairs of
vertices in a weighted directed graph.

Code for Floyd's Algorithm:

def floyd(adj_matrix):
num_vertices = len(adj_matrix)
dist = [row[:] for row in adj_matrix] # Create a copy of the adjacency matrix

for k in range(num_vertices):
for i in range(num_vertices):
for j in range(num_vertices):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])

return dist

# Example usage
adj_matrix = [[0, 1, float('inf'), 3],
[2, 0, 4, float('inf')],
[float('inf'), float('inf'), 0, 2],
[float('inf'), float('inf'), 1, 0]]

shortest_distances = floyd(adj_matrix)
print("Shortest Distance Matrix:")
for row in shortest_distances:
print(row)

OUTPUT:

Shortest Distance Matrix:

[0, 1, 5, 3]

[2, 0, 4, 6]

[4, 5, 0, 2]

[3, 4, 1, 0]

VIVVA QUESTIONS :

1. What is Floyd's Algorithm?

Floyd's Algorithm is a dynamic programming algorithm used to compute the shortest paths
between all pairs of vertices in a weighted directed graph. It determines the shortest distance
between all pairs of vertices by iteratively considering intermediate vertices.

2. How does Floyd's Algorithm work?

Floyd's Algorithm works by initializing a matrix to represent the shortest distances between pairs
of vertices. It then iteratively updates the matrix by considering an intermediate vertex and updating
the shortest distances based on whether a shorter path exists between two vertices that goes
through the intermediate vertex.

3. What is the main idea behind Floyd's Algorithm?

The main idea behind Floyd's Algorithm is to consider all possible intermediate vertices and
check if using a particular intermediate vertex results in a shorter path between two vertices. By
iteratively updating the matrix, the algorithm gradually finds the shortest distances between all pairs
of vertices.

4. What is the significance of the intermediate vertices in Floyd's Algorithm?

The intermediate vertices in Floyd's Algorithm play a crucial role in determining the shortest paths.
By considering all possible intermediate vertices, the algorithm explores different paths and updates
the matrix to find the shortest distances between all pairs of vertices.

5. What is the Floyd-Warshall Algorithm? Is it the same as Floyd's Algorithm?

The Floyd-Warshall Algorithm is a related algorithm used to compute the shortest paths between
all pairs of vertices in a weighted directed graph, similar to Floyd's Algorithm. However, the Floyd-
Warshall Algorithm is specifically designed for graphs with negative edge weights, whereas Floyd's
Algorithm assumes non-negative edge weights.

6. What is the time complexity of Floyd's Algorithm?

The time complexity of Floyd's Algorithm is O(N^3), where N is the number of vertices in the
graph. It involves three nested loops that iterate over all vertices, making it cubic in the worst case.

7. What are some applications of Floyd's Algorithm?

Floyd's Algorithm has various applications, including finding the shortest paths in transportation
networks, computing distances between locations, network routing, and optimizing communication
networks.

EXP.NO:5(c) KNAPSACK PROBLEM USING DYNAMIC PROGRAMMING

Algorithm:

1. Define the problem:


● Given a set of items with their weights and values, and a maximum weight
capacity for the knapsack, determine the maximum total value that can be
obtained by selecting a subset of items to fit within the capacity of the
knapsack.
2. Initialize a 2D array dp of size (n+1) x (W+1), where n is the number of items and
W is the maximum weight capacity.
● dp[i][j] represents the maximum value that can be obtained using items
from 1 to i with a maximum weight capacity of j.
3. Set the base cases:
●For i = 0, set dp[i][j] = 0 for all j.
● When there are no items, the maximum value is 0.
● For j = 0, set dp[i][j] = 0 for all i.
● When the knapsack capacity is 0, the maximum value is 0.
4. Perform dynamic programming iterations:
● For i from 1 to n (items):
● For j from 1 to W (weights):
● If the weight of item i is less than or equal to j:
● Update dp[i][j] to the maximum value between:
● dp[i-1][j] (excluding item i) and
● value[i] + dp[i-1][j-weight[i]] (including item i)
● The maximum value that can be obtained is stored in dp[n][W].
● Optionally, to retrieve the selected items:
● Initialize an empty list selected_items.
● Starting from i = n and j = W, backtrack by checking if dp[i][j] is different from
dp[i-1][j].
● If they are different, it means item i was selected:
● Append item i to selected_items.
● Update j to j - weight[i].
● Reverse selected_items to get the selected items in the correct order.
5. Return the maximum value and selected_items (optional).

Program :

def knapsack(items, weights, values, capacity):


n = len(items)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]

for i in range(1, n + 1):


for j in range(1, capacity + 1):
if weights[i - 1] <= j:
dp[i][j] = max(dp[i - 1][j], values[i - 1] + dp[i - 1][j - weights[i - 1]])
else:
dp[i][j] = dp[i - 1][j]

max_value = dp[n][capacity]
# Retrieve selected items (optional)
selected_items = []
i=n
j = capacity
while i > 0 and j > 0:
if dp[i][j] != dp[i - 1][j]:
selected_items.append(items[i - 1])
j -= weights[i - 1]
i -= 1

selected_items.reverse()

return max_value, selected_items

# Example usage
items = ['Item1', 'Item2', 'Item3', 'Item4', 'Item5']
weights = [2, 3, 4, 5, 6]
values = [5, 7, 9, 11, 13]
capacity = 10

max_value, selected_items = knapsack(items, weights, values, capacity)

print("Maximum value:", max_value)


print("Selected items:", selected_items)

OUTPUT:

Maximum value: 24
Selected items: ['Item3', 'Item5']

VIVA QUESTIONS:

1. What is the Knapsack Problem?

The Knapsack Problem is a classic optimization problem that involves selecting items with certain
values and weights to maximize the total value while ensuring that the total weight does not exceed
a given capacity.

2. How does Dynamic Programming solve the Knapsack Problem?

Dynamic Programming solves the Knapsack Problem by breaking it down into smaller subproblems
and solving them iteratively. It builds an optimal solution for each subproblem based on previously
computed solutions, gradually solving larger subproblems until reaching the desired capacity.

3. What is the approach used in Dynamic Programming for the Knapsack Problem?
The approach used in Dynamic Programming for the Knapsack Problem is known as the "bottom-up"
approach. It involves building a table or an array to store the optimal solutions for subproblems,
starting from the smallest possible subproblem and progressively filling the table until reaching the
desired capacity.

4. What is the time complexity of the Dynamic Programming algorithm for the
Knapsack Problem?

The time complexity of the Dynamic Programming algorithm for the Knapsack Problem is O(NW),
where N is the number of items and W is the capacity of the knapsack. It iterates over each item and
each possible capacity once to fill the table.

5. How does the Dynamic Programming algorithm initialize the table for the
Knapsack Problem?

The table for the Knapsack Problem is initialized with appropriate values based on the problem
requirements. Typically, it is initialized with zeros or negative infinity to indicate the absence of any
items or a non-feasible solution.

6. What is the recurrence relation used in the Dynamic Programming algorithm for
the Knapsack Problem?

The recurrence relation for the Knapsack Problem states that the optimal value for a given capacity
is the maximum between taking the current item and considering the remaining capacity or not
taking the current item and considering the remaining items.

EXP.NO:6(a) GREEDY TECHNIQUE – DIJKSTRA’S ALGORITHM

Algorithm:

1. Define the problem:


● Given a weighted directed graph and a source vertex, find the shortest path
from the source vertex to all other vertices in the graph.
2. Initialize the following data structures:
● Create an empty set visited to keep track of the visited vertices.
● Create a list distances to store the shortest distances from the source vertex
to each vertex. Initialize all distances to infinity except the source vertex,
which is set to 0.
● Create a priority queue (min-heap) pq to store vertices based on their
tentative distance from the source vertex.
3. Set the distance of the source vertex as 0 and add it to the priority queue pq.
4. Perform the following steps until the priority queue pq is empty:
● Extract the vertex u with the minimum distance from pq.
● If u is already in the visited set, continue to the next iteration.
● Add u to the visited set.
● For each neighboring vertex v of u:
● If the distance from u to v plus the distance of u is less than the current distance
of v, update the distance of v to the new shorter distance.
● Add v to the pq with its updated distance.
5. After the algorithm finishes, the distances list will contain the shortest distances
from the source vertex to all other vertices.
6. Optionally, to retrieve the shortest path to a specific vertex v:
● Start from v and backtrack through the distances list, selecting the previous
vertex with the minimum distance until reaching the source vertex.
● Reverse the path to obtain the shortest path from the source vertex to v

Program:

import heapq

def dijkstra(graph, source):


distances = [float('inf')] * len(graph)
distances[source] = 0

pq = [(0, source)] # (distance, vertex)

while pq:
dist, u = heapq.heappop(pq)

if dist > distances[u]:


continue
for v, weight in graph[u]:
new_dist = dist + weight
if new_dist < distances[v]:
distances[v] = new_dist
heapq.heappush(pq, (new_dist, v))
return distances
# Example usage
graph = [[(1, 4), (2, 2)],
[(2, 5), (3, 2)],
[(1, 1), (3, 1)],
[(4, 3)], []]
source_vertex = 0

shortest_distances = dijkstra(graph, source_vertex)


print("Shortest distances from vertex", source_vertex)
print(shortest_distances)

OUTPUT:

Shortest distances from vertex 0

[0, 3, 2, 3, inf]

VIVA QUESTIONS:

1. What is Dijkstra's Algorithm?

Dijkstra's Algorithm is a greedy algorithm used to find the shortest path from a starting vertex to
all other vertices in a weighted graph with non-negative edge weights. It constructs a shortest path
tree by iteratively selecting the vertex with the minimum distance and relaxing its adjacent vertices.

2. How does Dijkstra's Algorithm work?

Dijkstra's Algorithm works by maintaining a set of vertices whose shortest path distances from
the starting vertex have been determined. It iteratively selects the vertex with the minimum distance
and updates the distances of its adjacent vertices if a shorter path is found. This process continues
until all vertices have been processed.

3. What is the main idea behind Dijkstra's Algorithm?

The main idea behind Dijkstra's Algorithm is to iteratively find the vertex with the minimum
distance and add it to the set of processed vertices. By doing so, the algorithm gradually builds the
shortest path tree, ensuring that the selected vertices have the shortest distances from the starting
vertex.

4. What data structures are typically used in Dijkstra's Algorithm?

Dijkstra's Algorithm typically uses a priority queue (such as a min-heap) to efficiently select the
vertex with the minimum distance. Additionally, it uses an array or a data structure to store the
shortest distances from the starting vertex to each vertex in the graph.

5. What is the time complexity of Dijkstra's Algorithm?

The time complexity of Dijkstra's Algorithm depends on the implementation and the data
structures used. With a binary heap as the priority queue, the time complexity is O((V + E) log V),
where V is the number of vertices and E is the number of edges in the graph.

6. What are the limitations of Dijkstra's Algorithm?

Dijkstra's Algorithm assumes non-negative edge weights. If the graph contains negative edge
weights, it may produce incorrect results. Additionally, the algorithm does not handle graphs with
cycles, as it is designed for finding shortest paths in acyclic graphs.

EXP.NO:6(b) GREEDY TECHNIQUE –HUFFMAN TREES AND CODES

Algorithm:

1. Define the problem:


● Given a set of characters and their frequencies, construct a Huffman tree,
which is a binary tree used for data compression, and generate the
corresponding Huffman codes for each character.
2. Create a node structure to represent a character and its frequency.
3. Initialize a priority queue pq (min-heap) to store the nodes based on their
frequencies.
● Each node represents a character and its frequency.
● Sort the nodes in the priority queue by their frequencies in ascending order.
4. For each character and its frequency:
● Create a node and enqueue it into the priority queue pq.
5. Perform the following steps until there is only one node left in the priority queue:
● Dequeue the two nodes with the minimum frequencies from the priority
queue pq.
● Create a new node with a null character and a frequency equal to the sum
of the frequencies of the dequeued nodes.
● Set the left child of the new node as the first dequeued node, and the right
child as the second dequeued node.
● Enqueue the new node back into the priority queue.
6. The remaining node in the priority queue is the root of the Huffman tree.
7. Optionally, to generate the Huffman codes for each character:
● Traverse the Huffman tree in a depth-first manner.
● Assign a '0' to the left edge and a '1' to the right edge.
● When reaching a leaf node representing a character, record the path from the
root to that node as the Huffman code for that character.
8. Return the Huffman tree and the Huffman codes for each character.

Program:

from queue import PriorityQueue

class Node:
def __init__(self, char, frequency):
self.char = char
self.frequency = frequency
self.left = None
self.right = None
def build_huffman_tree(characters, frequencies):
pq = PriorityQueue()

for i in range(len(characters)):
node = Node(characters[i], frequencies[i])
pq.put((node.frequency, node))
while pq.qsize() > 1:
freq1, node1 = pq.get()
freq2, node2 = pq.get()
new_node = Node(None, freq1 + freq2)
new_node.left = node1
new_node.right = node2

pq.put((new_node.frequency, new_node))

root = pq.get()[1]
return root

def generate_huffman_codes(root):
codes = {}

def dfs(node, code):


if node.char:
codes[node.char] = code
else:
dfs(node.left, code + '0')
dfs(node.right, code + '1')
dfs(root, '')
return codes
# Example usage
characters = ['A', 'B', 'C', 'D', 'E']
frequencies = [10, 7, 15, 4, 12]

root = build_huffman_tree(characters, frequencies)


huffman_codes = generate_huffman_codes(root)

print("Huffman Codes:")
for char, code in huffman_codes.items():
print(char, ":", code)

OUTPUT:

Huffman Codes:

C : 00

E : 01

A : 10

B : 110

D : 111

VIVA QUESTIONS:

1. What are Huffman Trees and Codes?

Huffman Trees and Codes are a technique used for lossless data compression. They involve
constructing a binary tree known as a Huffman tree and assigning variable-length codes to different
characters or symbols based on their frequencies in the input data.

2. How does Huffman Coding work?

Huffman Coding works by assigning shorter codes to more frequently occurring symbols and
longer codes to less frequently occurring symbols. It achieves data compression by representing
frequently occurring symbols with fewer bits and less frequently occurring symbols with more bits.

3. What is the main idea behind Huffman Trees and Codes?

The main idea behind Huffman Trees and Codes is to create an efficient binary encoding scheme
by constructing a tree where the most frequent symbols are closer to the root. This ensures that the
most common symbols have shorter codes, reducing the overall number of bits required to
represent the input data.

4. How is a Huffman Tree constructed?

A Huffman Tree is constructed by iteratively merging the two nodes with the lowest frequencies to
create a new parent node. This process continues until all symbols are combined into a single root
node, resulting in a binary tree.

5. How are the variable-length codes assigned in Huffman Coding?


Variable-length codes in Huffman Coding are assigned by traversing the Huffman Tree. A left
branch represents a "0" bit, while a right branch represents a "1" bit. The codes are assigned by
traversing from the root to each leaf node, recording the path taken to reach each symbol.

6. What is the advantage of Huffman Coding?

The advantage of Huffman Coding is that it produces an optimal prefix-free code, meaning that no
code is a prefix of any other code. This property ensures that the encoded data can be uniquely
decoded, allowing for lossless compression.

7. Can you explain the process of encoding and decoding using Huffman Trees
and Codes?

Encoding using Huffman Trees and Codes involves traversing the tree to find the corresponding
code for each symbol and concatenating these codes to form the encoded data. Decoding involves
traversing the tree based on the encoded bits to reconstruct the original symbols.

EXP.NO:7 ITERATIVE IMPROVEMENT - SIMPLEX METHOD

Algorithm:

1. Define the problem:


● Given a linear programming problem in standard form, maximize (or minimize) a
linear objective function subject to linear constraints.
2. Convert the linear programming problem into standard form:
● Ensure that the objective function is in a maximization form.
● Express all inequality constraints as equality constraints by introducing slack or
surplus variables.
● Add non-negativity constraints for all variables.
3. Initialize the simplex tableau:
● Create a tableau that represents the current state of the linear programming
problem.
● Include the objective function row, the constraint rows, and the right-hand side
(RHS) column.
4. Determine the pivot element:
● Choose a pivot column by selecting the most negative coefficient in the objective
function row (if maximizing) or the most positive coefficient (if minimizing).
● Compute the ratios of the RHS values to the corresponding coefficients in the
pivot column.
● Choose the pivot row by selecting the smallest positive ratio, considering the
minimum ratio test.
● The element at the intersection of the pivot row and pivot column is the pivot
element.
5. Perform row operations to update the tableau:
● Divide the pivot row by the pivot element to make the pivot element equal to 1.
● Perform elementary row operations to make all other elements in the pivot
column equal to 0.
● Update the objective function row and constraint rows accordingly.
6. Repeat steps 4 and 5 until an optimal solution is reached:
● If there are negative coefficients in the objective function row, go back to step 4.
● If all coefficients in the objective function row are non-negative (for maximization)
or non-positive (for minimization), the current tableau represents an optimal
solution.
7. Extract the optimal solution:
● Read the values of the variables from the tableau.
● The variables corresponding to the basic variables (columns with only one non-
zero element) are non-zero, while the non-basic variables are zero.
● The objective function value is given by the value in the RHS column
corresponding to the objective function row.

Program:

import numpy as np
from scipy.optimize import linprog

# Define the objective function coefficients


c = [3, 2]

# Define the constraint coefficients and RHS values


A = [[-1, 1],
[3, 2],
[1, 0]]
b = [1, 12, 3]

# Define the bounds for the variables


x_bounds = [(0, None), (0, None)]

# Solve the linear programming problem using the simplex method


result = linprog(c, A_ub=A, b_ub=b, bounds=x_bounds, method='simplex')

# Print the optimal solution and objective function value


print("Optimal Solution:")
print(result.x)
print("Objective Function Value:")
print(result.fun)

OUTPUT:

Optimal Solution:

[0. 0.]

Objective Function Value:

0.0

VIVA QUESTIONS:

1. What is the Simplex Method?

The Simplex Method is an iterative algorithm used to solve linear programming problems. It
optimizes a linear objective function while satisfying a set of linear constraints by iteratively moving
from one feasible solution to another until an optimal solution is reached.

2. How does the Simplex Method work?

The Simplex Method starts with an initial feasible solution and iteratively improves it by moving
along the edges of the feasible region. It selects an entering variable (corresponding to a non-basic
variable with a positive coefficient in the objective function) and a departing variable (corresponding
to a basic variable to be replaced). It then performs pivot operations to obtain a new feasible
solution with a better objective function value.

3. What is the main idea behind the Iterative Improvement in the Simplex Method?

The main idea behind the Iterative Improvement in the Simplex Method is to iteratively improve the
current feasible solution by iteratively moving from one basic feasible solution to another, eventually
reaching the optimal solution. This improvement is achieved through pivot operations that change
the basic and non-basic variables.

4. What are the basic components of the Simplex Method algorithm?

The basic components of the Simplex Method algorithm include the initialization of the initial
feasible solution, selecting the entering and departing variables, performing pivot operations to
update the basic and non-basic variables, and checking for termination conditions to determine if an
optimal solution has been reached.

5. How does the Simplex Method handle unbounded solutions or infeasible


problems?

The Simplex Method detects unbounded solutions by checking if the objective function can be
improved indefinitely. If an unbounded solution is detected, it means that no optimal solution exists.
In the case of infeasible problems, the Simplex Method can detect infeasibility by examining the
constraints and determining that they cannot be satisfied simultaneously.

6. What is the time complexity of the Simplex Method?

The time complexity of the Simplex Method varies depending on the specific problem and the size of
the constraints and variables. In the worst-case scenario, the Simplex Method has an exponential
time complexity. However, in practice, it often performs efficiently for problems with a moderate
number of constraints and variables.

EXP.NO:8(a) BACKTRACKING – N-QUEEN PROBLEM

Algorithm:

1. Define the problem:


● Given an integer N, find all possible placements of N queens on an N x N
chessboard such that no two queens threaten each other.
2. Initialize an empty solution board of size N x N.
3. Define a recursive function place_queens(row, board, N):
● If row is equal to N, it means all queens have been successfully placed in each
row. Add the current configuration to the list of solutions.
● Iterate through each column in the current row.
● If placing a queen at the current position (row, col) is safe (i.e., it does not
conflict with any previously placed queens), mark the position as occupied in the
board.
● Recursively call place_queens(row + 1, board, N) to place the queens in the next
row.
● After the recursive call, remove the queen from the current position by marking it
as unoccupied in the board.
4. Start the backtracking process by calling place_queens(0, board, N) with the
initial row set to 0.
5. Return the list of solutions.

Program:
def is_safe(row, col, board, N):
# Check if a queen can be placed at the current position without conflicts
# Check column
for i in range(row):
if board[i][col] == 1:
return False
# Check upper diagonal
i = row - 1
j = col - 1
while i >= 0 and j >= 0:
if board[i][j] == 1:
return False
i -= 1
j -= 1
# Check lower diagonal
i = row - 1
j = col + 1
while i >= 0 and j < N:
if board[i][j] == 1:
return False
i -= 1
j += 1
return True
def place_queens(row, board, N, solutions):
if row == N:
# All queens have been placed successfully
solutions.append(board.copy())
return
for col in range(N):
if is_safe(row, col, board, N):
# Place the queen at the current position
board[row][col] = 1
# Recursively place queens in the next row
place_queens(row + 1, board, N, solutions)

# Remove the queen from the current position


board[row][col] = 0

def solve_n_queen(N):
board = [[0] * N for _ in range(N)]
solutions = []
place_queens(0, board, N, solutions)
return solutions

# Example usage
N=4
solutions = solve_n_queen(N)

print("Number of solutions:", len(solutions))


for i, solution in enumerate(solutions):
print("Solution", i + 1)
for row in solution:
print(row)
print()
OUTPUT:

Number of solutions: 2
Solution 1
[0, 1, 0, 0]
[0, 0, 0, 1]
[1, 0, 0, 0]
[0, 0, 1, 0]

Solution 2
[0, 0, 1, 0]
[1, 0, 0, 0]
[0, 0, 0, 1]
[0, 1, 0, 0]

VIVA QUESTIONS:

1. What is the N-Queen Problem?

The N-Queen Problem is a classic problem in computer science and mathematics that involves
placing N queens on an N x N chessboard such that no two queens threaten each other. The goal is
to find all possible configurations or solutions for placing the queens.

2. How does the Backtracking algorithm solve the N-Queen Problem?

The Backtracking algorithm solves the N-Queen Problem by systematically exploring all possible
placements of queens on the chessboard, backtracking whenever a conflict is detected. It tries
different possibilities and recursively explores the solution space until a valid solution is found or all
possibilities are exhausted.

3. What is the approach used in the Backtracking algorithm for the N-Queen
Problem?

The Backtracking algorithm for the N-Queen Problem uses a recursive approach. It starts with an
empty board and places queens row by row, making sure each placement is safe and does not
conflict with previously placed queens. If a conflict is detected, the algorithm backtracks and tries a
different possibility.

4. How do you check if a queen can be safely placed in a specific position on the
chessboard?

To check if a queen can be safely placed at a specific position (row, column) on the chessboard, you
need to ensure it does not conflict with any previously placed queens. This involves checking if there
are no other queens in the same column, the same diagonal, or the same row.

5. How do you handle the backtracking process in the N-Queen Problem?

During the backtracking process, if a placement of a queen leads to a conflict, the algorithm
removes the queen from that position and continues exploring other possibilities. It goes back to the
previous row and tries a different column to place the queen. This process continues until a valid
solution is found or all possibilities have been exhausted.

6. What is the time complexity of the Backtracking algorithm for the N-Queen
Problem?

The time complexity of the Backtracking algorithm for the N-Queen Problem is exponential,
specifically O(N!) in the worst-case scenario. This is because the algorithm explores all possible
permutations of queen placements. However, by using various optimizations and heuristics, the
actual number of backtracks can be significantly reduced in practice.

EXP.NO:7(b) BACKTRACKING – SUBSET SUM PROBLEM

Algorithm:

1. Define the problem:


● Given a set of positive integers and a target sum, find all subsets of the set
whose elements add up to the target sum.
2. Initialize an empty list subset to store the current subset.
● This list will be used to keep track of the elements that are included in the
current subset.
3. Define a recursive function find_subsets(nums, target_sum, index, subset,
subsets):
● If the target sum is reached (i.e., target_sum is 0), add the current subset to
the list of subsets.
● Iterate through the remaining elements starting from index:
● Include the current element in the subset by appending it.
● Reduce the target_sum by the value of the current element.
● Recursively call find_subsets with the updated target_sum, index + 1, and the
updated subset.
● Backtrack by removing the last element from the subset to explore other
possibilities.
4. Start the backtracking process by calling find_subsets(nums, target_sum, 0, [],
subsets) with the initial index set to 0 and an empty subset.
5. Return the list of subsets

Program:

def find_subsets(nums, target_sum, index, subset, subsets):


if target_sum == 0:

subsets.append(subset[:]) # Add a copy of the subset to the subsets list

return

for i in range(index, len(nums)):

if nums[i] <= target_sum:

subset.append(nums[i])

find_subsets(nums, target_sum - nums[i], i + 1, subset, subsets)

subset.pop()

def subset_sum(nums, target_sum):

subsets = []

find_subsets(nums, target_sum, 0, [], subsets)

return subsets

# Example usage

nums = [2, 4, 6, 8]

target_sum = 8

result = subset_sum(nums, target_sum)

print("Subsets with a sum of", target_sum)

for subset in result:

print(subset)

OUTPUT:

Subsets with a sum of 8

[2, 6]
[4, 4]

[8]

VIVA QUESTIONS:

1. What is the Subset Sum Problem?

The Subset Sum Problem is a classic problem in computer science that involves finding all
possible subsets of a given set whose elements sum up to a target value.

2. How does the Backtracking algorithm solve the Subset Sum Problem?

The Backtracking algorithm solves the Subset Sum Problem by systematically exploring all possible
subsets of the given set and checking if their elements sum up to the target value. It tries different
possibilities and recursively explores the solution space until a valid subset sum is found or all
possibilities are exhausted.

3. What is the approach used in the Backtracking algorithm for the Subset Sum
Problem?

The Backtracking algorithm for the Subset Sum Problem uses a recursive approach. It starts with an
empty subset and incrementally adds elements to it, checking if the current subset sum equals the
target sum. If the current subset sum exceeds the target sum, the algorithm backtracks and tries a
different possibility.

4. How does the Backtracking algorithm handle the backtracking process in the
Subset Sum Problem?

During the backtracking process, if the current subset sum exceeds the target sum, the algorithm
removes the most recently added element and continues exploring other possibilities. It goes back
to the previous level and tries a different element to include in the subset. This process continues
until a valid subset sum is found or all possibilities have been exhausted.

5. What is the time complexity of the Backtracking algorithm for the Subset Sum
Problem?

The time complexity of the Backtracking algorithm for the Subset Sum Problem can vary based on
the problem instance. In the worst-case scenario, the algorithm has an exponential time complexity
of O(2^N), where N is the number of elements in the set. However, using various optimizations and
heuristics, the actual number of backtracks can be significantly reduced in practice.

6. Can the Backtracking algorithm find all possible subsets with the desired sum
in the Subset Sum Problem?

Yes, the Backtracking algorithm can find all possible subsets of the given set that sum up to the
target value. By systematically exploring the solution space and trying different possibilities, it
exhaustively searches for all valid subsets with the desired sum.

Exp.NO:9(a) BRANCH AND BOUND - ASSIGNMENT PROBLEM

Algorithm

1. Define the problem:


● Given a set of tasks and a set of agents, each with an associated cost matrix
representing the cost of assigning each task to each agent, find the
assignment that minimizes the total cost while ensuring that each task is
assigned to exactly one agent and each agent is assigned to at most one
task.
2. Initialize the variables:
● Create an empty assignment matrix of size N x N, where N is the number of
tasks or agents.
● Create a list selected_tasks to keep track of the selected tasks.
● Initialize the minimum cost min_cost as infinity.
3. Define a recursive function branch_and_bound(cost_matrix, assignment_matrix,
selected_tasks, current_cost):
● If current_cost exceeds the min_cost, prune the branch and return.
● If all tasks are assigned, update min_cost with current_cost and update the
assignment_matrix as the current assignment.

Iterate through each unassigned task:

● Select the task and mark it as assigned in the selected_tasks.


● Find the minimum cost agent for the selected task.
● Update the assignment_matrix with the assignment of the task to the agent.
● Recursively call branch_and_bound with the updated parameters (cost_matrix,
assignment_matrix, selected_tasks, current_cost + cost_matrix[task][agent]).
● Backtrack by unmarking the task as assigned and removing the assignment from
the assignment_matrix.
4. Start the branch and bound process by calling branch_and_bound(cost_matrix,
assignment_matrix, selected_tasks, current_cost) with the initial parameters.
5. Return the assignment_matrix and min_cost.
Program:

import numpy as np
def branch_and_bound(cost_matrix):
N = cost_matrix.shape[0]
assignment_matrix = np.zeros((N, N), dtype=int)
selected_tasks = []
min_cost = float('inf')
def backtrack(selected_tasks, current_cost):
nonlocal min_cost
if current_cost > min_cost:
return

if len(selected_tasks) == N:
min_cost = current_cost
assignment_matrix.fill(0)
for i, task in enumerate(selected_tasks):
assignment_matrix[task][i] = 1
return

for task in range(N):


if task not in selected_tasks:
agent = np.argmin([cost_matrix[task][i] for i in range(N) if i not in
selected_tasks])
selected_tasks.append(task)
backtrack(selected_tasks, current_cost + cost_matrix[task][agent])
selected_tasks.remove(task)

backtrack(selected_tasks, 0)
return assignment_matrix, min_cost

# Example usage
cost_matrix = np.array([[5, 7, 3, 8],
[9, 2, 6, 4],
[1, 3, 8, 6],
[7, 6, 4, 2]])
assignment, min_cost = branch_and_bound(cost_matrix)
print("Assignment Matrix:")
print(assignment)
print("Minimum Cost:", min_cost)

OUTPUT:

Assignment Matrix:
[[0 0 1 0]
[1 0 0 0]
[0 1 0 0]
[0 0 0 1]]
Minimum Cost: 14

VIVA QUESTIONS:
1. What is the Assignment Problem?

The Assignment Problem is a classic problem in operations research that involves finding the
optimal assignment of tasks to agents, given a cost matrix representing the cost of each
assignment. The objective is to minimize the total cost while ensuring that each task is assigned to
exactly one agent and each agent is assigned to at most one task.

2. How does the Branch and Bound algorithm solve the Assignment Problem?

The Branch and Bound algorithm solves the Assignment Problem by systematically exploring the
solution space and using a bounding mechanism to prune unpromising branches. It starts with an
initial solution and iteratively branches out, considering different possible assignments and
bounding the search based on the lower bounds of the current solutions.

3. What is the approach used in the Branch and Bound algorithm for the
Assignment Problem?

The Branch and Bound algorithm for the Assignment Problem uses a combination of depth-first
search and bounding techniques. It explores the solution space by considering different task-agent
assignments and uses lower bounds to determine which branches to prune, reducing the number of
solutions that need to be examined.

4. How does the Branch and Bound algorithm perform branching in the
Assignment Problem?

In the Assignment Problem, branching in the Branch and Bound algorithm involves considering
different task-agent assignments at each level of the search. It branches out by selecting an
unassigned task and trying different agents for the assignment, creating child nodes corresponding
to each possibility.

5. How does the Branch and Bound algorithm use bounding in the Assignment
Problem?

The Branch and Bound algorithm uses bounding in the Assignment Problem by evaluating the
current partial assignment and using lower bounds to estimate the potential minimum cost of
completing the assignment. If the lower bound exceeds the current best solution cost, the algorithm
prunes that branch of the search, avoiding unnecessary exploration.
6. What is the time complexity of the Branch and Bound algorithm for the
Assignment Problem?

The time complexity of the Branch and Bound algorithm for the Assignment Problem can vary
depending on the problem instance and the bounding techniques used. In the worst-case scenario,
the algorithm has an exponential time complexity of O(2^N * N^2)

EXP.NO:9(b) BRANCH AND BOUND - TRAVELING SALESMAN PROBLEM

Algorithm:

1. Define the problem:


● Given a complete graph with N nodes representing cities and the distances
between them, find the shortest possible route that visits each city exactly once
and returns to the starting city.
2. Initialize the variables:
● Create a matrix distances to represent the distances between each pair of cities.
● Create a list path to store the current path being explored.
● Create a variable min_distance to keep track of the minimum distance found so
far, initially set to infinity.
3. Define a recursive function branch_and_bound(current_city, visited_cities, path,
distance):
● If all cities have been visited and the distance from the current city to the starting
city is less than min_distance, update min_distance and store the current path as
the best path.
● For each unvisited city:
● Mark the current city as visited.
● Add the current city to the path.
● Update the distance by adding the distance from the previous city to the current
city.
● If the distance is less than min_distance, recursively call branch_and_bound with
the current city as the new current_city, the updated visited_cities, the updated
path, and the updated distance.
● Mark the current city as unvisited and remove it from the path.
● Subtract the distance from the previous city to the current city from the distance.
4. Start the branch and bound process by calling branch_and_bound(starting_city,
visited_cities, path, distance) with the initial parameters.
5. Return the best path and the minimum distance.
Program:

import numpy as np

def branch_and_bound(current_city, visited_cities, path, distance):


global min_distance, best_path

if len(visited_cities) == N and distances[current_city][0] < min_distance:


min_distance = distance + distances[current_city][0]
best_path = path[:]
return

for city in range(N):


if city not in visited_cities:
visited_cities.add(city)
path.append(city)

new_distance = distance + distances[current_city][city]


if new_distance < min_distance:
branch_and_bound(city, visited_cities, path, new_distance)

visited_cities.remove(city)
path.pop()

# Example usage
distances = np.array([[0, 2, 9, 10],
[1, 0, 6, 4],
[15, 7, 0, 8],
[6, 3, 12, 0]])

N = distances.shape[0]
min_distance = float('inf')
best_path = []

branch_and_bound(0, {0}, [0], 0)

print("Best Path:", best_path)


print("Minimum Distance:", min_distance)
OUTPUT:

Best Path: [0, 1, 3, 2, 0]

Minimum Distance: 19

VIVA QUESTIONS :

1. What is the Traveling Salesman Problem (TSP)?

The Traveling Salesman Problem is a well-known optimization problem in computer science that
involves finding the shortest possible route that visits a set of cities exactly once and returns to the
starting city, given the distances between each pair of cities.

2. How does the Branch and Bound algorithm solve the Traveling Salesman
Problem?

The Branch and Bound algorithm solves the Traveling Salesman Problem by systematically exploring
the solution space of possible routes and using lower bounds to prune unpromising branches. It
starts with an initial route and iteratively branches out, considering different possible paths and
bounding the search based on the lower bounds of the current solutions.

3. What is the approach used in the Branch and Bound algorithm for the Traveling
Salesman Problem?

The Branch and Bound algorithm for the Traveling Salesman Problem uses a combination of depth-
first search and bounding techniques. It explores the solution space by considering different city
permutations and uses lower bounds to determine which branches to prune, reducing the number of
solutions that need to be examined.

4. How does the Branch and Bound algorithm perform branching in the Traveling
Salesman Problem?

In the Traveling Salesman Problem, branching in the Branch and Bound algorithm involves
considering different city permutations at each level of the search. It branches out by selecting an
unvisited city and trying different positions for it in the current path, creating child nodes
corresponding to each possibility.

5. How does the Branch and Bound algorithm use bounding in the Traveling
Salesman Problem?

The Branch and Bound algorithm uses bounding in the Traveling Salesman Problem by evaluating
the current partial path and using lower bounds to estimate the potential minimum distance of
completing the route. If the lower bound exceeds the current best distance, the algorithm prunes
that branch of the search, avoiding unnecessary exploration.
6. What is the time complexity of the Branch and Bound algorithm for the
Traveling Salesman Problem?

The time complexity of the Branch and Bound algorithm for the Traveling Salesman Problem can
vary depending on the problem instance and the bounding techniques used. In the worst-case
scenario, the algorithm has an exponential time complexity of O(N^2 * 2^N)

You might also like