ADA - Assignments 2
ADA - Assignments 2
Properties of an algorithm:
Input: An algorithm must have zero or more inputs, which are the initial quantities that it is given to
work with. These inputs are taken from a specified set of objects.
Output: An algorithm must produce at least one output. The output is the result of the computation
and is the solution to the problem the algorithm is designed to solve.
Definiteness: Every step of the algorithm must be precisely defined and unambiguous. The
instructions must be clear, simple, and lead to only one interpretation. For example, "add 5 to x" is a
definite instruction, while "add a few numbers to x" is not.
Finiteness: An algorithm must terminate after a finite number of steps for all possible inputs. It
cannot go on forever. This property ensures that the algorithm will eventually produce a result.
Effectiveness: Each step of the algorithm must be sufficiently basic that it can be carried out, in
principle, by a person using only pencil and paper. It should be feasible and not require infinite
precision or an impossible task.
Example (Relation): Let Set A = {Ram, Sita} and Set B = {Apple, Orange, Banana}. A relation R can
be "likes" where Ram likes Apple and Orange, and Sita likes Banana. So, R = {(Ram, Apple), (Ram,
Orange), (Sita, Banana)}. Notice that Ram is related to two different fruits.
A function is a more constrained and specialized type of relation. A function from set A to set B is a
relation where every element in set A (the domain) is paired with exactly one element in set B (the
codomain). This "one-to-one or many-to-one" mapping is the key differentiator.
Example (Function): Let Set A = {2, 3, 4} and Set B = {4, 6, 8}. A function F can be defined as
f(x)=2x. Here, F = {(2, 4), (3, 6), (4, 8)}. Each element in Set A is mapped to a unique element in Set
B.
Key Difference: In a relation, an element in the domain can be paired with multiple elements in the
codomain. In a function, each element in the domain is paired with only one element in the codomain.
Predicts Performance: It allows us to predict how an algorithm will perform with larger inputs
without actually running it.
Comparison: It enables a fair comparison between different algorithms that solve the same
problem, helping us choose the most efficient one.
Resource Management: It helps in understanding the resource requirements, which is vital for
designing scalable and efficient systems.
Complexity Cases:
Worst-Case Complexity: This is the maximum running time an algorithm can take for any input of a
given size n. It provides an upper bound on the time required, guaranteeing that the algorithm will
never take longer than this. This is often the most important measure because it gives a
performance guarantee.
Example (Linear Search): To find an element 'x' in an array of size n, the worst case is when
'x' is the last element or not present at all. The algorithm must check all n elements, so the
worst-case complexity is O(n).
Best-Case Complexity: This is the minimum running time an algorithm can take for any input of
size n. While it's an interesting metric, it's often not very useful in practice because it represents an
ideal, and often unlikely, scenario.
Example (Linear Search): The best case is when the element 'x' is the very first one in the
array. The algorithm finds it in just one step, so the best-case complexity is O(1).
Average-Case Complexity: This is the expected running time of an algorithm over all possible
inputs of size n, assuming a certain probability distribution of inputs. It gives a more realistic
measure of performance than the best or worst case, but it can be difficult to calculate because it
requires knowledge of the input distribution.
Example (Linear Search): On average, we can expect to find the element somewhere in the
middle of the array. The algorithm would, on average, check about n/2 elements. Therefore, the
average-case complexity is O(n).
Big Oh (O): Upper Bound. f(n)=O(g(n)) if there exist positive constants c and n0such that
f(n)≤c⋅g(n) for all n≥n0. This notation provides an upper limit on the growth rate of a function. We
use Big Oh to describe the worst-case running time.
Example: If an algorithm has a running time of 3n2+5n+10, we can say its worst-case
complexity is O(n2) because for large n, the n2 term dominates. The algorithm's performance is
bounded from above by some constant multiple of n2.
Omega (Ω): Lower Bound. f(n)=Ω(g(n)) if there exist positive constants c and n0such that
f(n)≥c⋅g(n) for all n≥n0. This notation provides a lower limit on the growth rate. We use Omega to
describe the best-case running time.
Example: A sorting algorithm must, at minimum, examine all n elements. Therefore, its best-
case running time is Ω(n).
Theta (Θ): Tight Bound. f(n)=Θ(g(n)) if there exist positive constants c1,c2and n0such that c1
⋅g(n)≤f(n)≤c2⋅ g(n) for all n≥n0. Theta notation provides a tight bound, meaning the function is
bounded both from above and below by the same function g(n). This is a very precise description of
an algorithm's running time.
Example: If an algorithm has a running time of 3n2+5n+10, we can say its running time is
Θ(n2) because for large n, the running time is both an upper bound (O(n2)) and a lower bound
(Ω(n2)).
First Pass:
Compare (5, 1): 5 > 1, so swap. Array: {1, 5, 4, 2, 8}
Compare (5, 4): 5 > 4, so swap. Array: {1, 4, 5, 2, 8}
Compare (5, 2): 5 > 2, so swap. Array: {1, 4, 2, 5, 8}
Compare (5, 8): 5 < 8, no swap. Array: {1, 4, 2, 5, 8} . After the first pass, the largest
element (8) is at its correct position.
Second Pass:
Compare (1, 4): no swap.
Compare (4, 2): 4 > 2, so swap. Array: {1, 2, 4, 5, 8}
Compare (4, 5): no swap.
... (the last two elements are already sorted, so we don't need to check them)
Third Pass:
Compare (1, 2): no swap.
Compare (2, 4): no swap.
... No swaps were made in this pass, so the array is sorted.
Complexity of Bubble Sort: The worst-case and average-case complexity is O(n2) because in the
worst case (a reverse-sorted array), the inner loop must perform n−1 comparisons and swaps, and this
is repeated n times. The best-case complexity is O(n) if the array is already sorted, as the algorithm can
detect that no swaps were made in the first pass and terminate early.
Algorithm Steps:
1. Pass 1: Find the minimum element in A[0...4] . It's 11 . Swap 11 with A[0] (which is 64 ). Array
becomes: {11, 25, 12, 22, 64} . Sorted part: {11} . Unsorted part: {25, 12, 22, 64} .
2. Pass 2: Find the minimum element in A[1...4] . It's 12 . Swap 12 with A[1] (which is 25 ). Array
becomes: {11, 12, 25, 22, 64} . Sorted part: {11, 12} . Unsorted part: {25, 22, 64} .
3. Pass 3: Find the minimum element in A[2...4] . It's 22 . Swap 22 with A[2] (which is 25 ). Array
becomes: {11, 12, 22, 25, 64} . Sorted part: {11, 12, 22} . Unsorted part: {25, 64} .
4. ...and so on, until the entire array is sorted.
Best Case, Worst Case, and Average Case: The complexity is always O(n2). This is because the
algorithm always performs n passes, and in each pass, it must scan the entire unsorted subarray to
find the minimum element. The number of comparisons is fixed regardless of the initial order of the
elements. For an array of size n, the number of comparisons is (n−1)+(n−2)+...+1, which is
approximately n2/2, leading to a complexity of O(n2).
7) Sort given array A = {27, 46, 11, 95, 67, 32, 78}
using insertion sort algorithm. Also perform best
case and worst case analysis of insertion sort
algorithm.
Insertion Sort Algorithm:
Insertion sort works by iterating through the array, taking each element and "inserting" it into its correct
position within the sorted portion of the array to its left.
Sorting the Array A = {27, 46, 11, 95, 67, 32, 78} :
Step 1: Consider the first element {27} . It is already a sorted sub-array of size 1.
Step 2: Take 46 . Compare it with 27 . 46 > 27 , so it stays. Sub-array: {27, 46} .
Step 3: Take 11 . Compare with 46 ( 11 < 46 ). Shift 46 to the right. Compare with 27 ( 11 <
27 ). Shift 27 to the right. Insert 11 . Sub-array: {11, 27, 46} .
Step 4: Take 95 . Compare with 46 , 27 , 11 . 95 is the largest. Sub-array: {11, 27, 46, 95} .
Step 5: Take 67 . Compare with 95 ( 67 < 95 ). Shift 95 . Compare with 46 . 67 > 46 , so insert
67 . Sub-array: {11, 27, 46, 67, 95} .
Step 6: Take 32 . Compare with 95, 67, 46 . Insert it between 27 and 46 . Sub-array: {11, 27,
32, 46, 67, 95} .
Step 7: Take 78 . Compare with 95, 67 . Insert it between 67 and 95 . Final sorted array: {11,
27, 32, 46, 67, 78, 95} .
Best Case: O(n). This occurs when the array is already sorted. The inner loop, which handles the
shifting of elements, will not execute at all because each element is already in its correct position.
The algorithm only performs a single comparison for each element.
Worst Case: O(n2). This occurs when the array is sorted in reverse order. For each element, the
inner loop must compare it with and shift all the elements in the sorted sub-array. This leads to a
total number of comparisons and shifts proportional to n(n−1)/2, which is O(n2).
8) Give the properties of Heap Tree. Sort the following data using Heap Sort Method: 20, 50, 30,
75, 90, 60, 80, 25, 10, 40.
A heap is a specialized tree-based data structure that satisfies the following properties:
1. Complete Binary Tree: A heap is a complete binary tree. This means that all levels of the tree are
fully filled, except possibly for the last level. The last level is filled from left to right without any gaps.
This property allows a heap to be efficiently stored in an array.
2. Heap Property: The value of a node is related to the value of its children in a specific way:
Max-Heap: For every node i other than the root, the value of the parent is greater than or
equal to the value of its children. This means the largest element is always at the root.
Min-Heap: For every node i other than the root, the value of the parent is less than or equal to
the value of its children. The smallest element is always at the root.
Heap sort uses the heap data structure to sort an array. The process consists of two main stages:
1. Build a Max-Heap: First, we transform the input array into a max-heap. This is typically done using
a "bottom-up" approach, starting from the last non-leaf node and working up to the root, ensuring
the heap property is satisfied at each node.
Initial array: {20, 50, 30, 75, 90, 60, 80, 25, 10, 40} .
After building a max-heap, the array representation would be transformed (the root would be
90).
2. Sort the Heap: After the max-heap is built, the largest element (the root) is at A[0] .
Swap the root A[0] with the last element of the heap A[n-1] .
The largest element is now at its final sorted position.
Reduce the size of the heap by one.
"Heapify" the new root (the element that was just swapped into A[0] ) to restore the max-heap
property.
Repeat this process until the heap size is 1. The array will be sorted in ascending order.
1. Aggregate Method:
This is the simplest method. We determine the total cost of a sequence of n operations and then
divide by n to get the amortized cost per operation.
Example (Dynamic Array): Consider a dynamic array that can grow in size. Let's analyze a
sequence of n insertions. Most insertions take O(1) time. However, when the array is full, we
must allocate a new, larger array (e.g., twice the size), copy all elements, and then insert the
new element. This expensive resizing operation takes O(k) time for an array of size k.
Let's analyze a sequence of n insertions starting with an array of size 1. The resizes will happen
at insertions 2, 4, 8, 16, etc. The costs are: 1+2+4+...+2log2(n)−1. The total cost of these resizes
is approximately 2n. The total cost of the n insertions is therefore n+2n=3n. The total cost for n
insertions is O(n).
The amortized cost per operation is Total Cost / Number of Operations = O(n)/n=O(1).
2. Accounting Method:
This method is like an accounting system. We assign a different "amortized cost" to each
operation. Cheap operations are "overcharged," and the excess "credit" is stored. Expensive
operations can then be "paid for" using the accumulated credit. The total credit must never be
negative.
Example (Stack with Multi-pop): Let's analyze a stack with PUSH , POP , and MULTI-POP(k)
operations.
PUSH : We assign an amortized cost of 2. We use 1 unit to pay for the actual push and
store 1 unit of credit on the element we just pushed.
POP : We assign an amortized cost of 0. We use the 1 unit of credit from the element being
popped to pay for this operation.
MULTI-POP(k) : This operation pops the top k elements. The cost is min(k, s) , where s
is the stack size. We assign an amortized cost of 0. The credit stored on each of the k
elements being popped is used to pay for the operation.
Since we can always pay for a POP or MULTI-POP with the credit stored on the elements, the
total credit never becomes negative. Therefore, the amortized cost of each operation is constant.
Assignment 2: Algorithms and Dynamic Programming
Document
1) What do you mean by recurrence? Explain different
methods to solve recurrence in brief.
A recurrence relation is a mathematical equation or inequality that describes a function in terms of its
value on smaller inputs. In the context of algorithms, a recurrence is used to describe the running time
of a recursive algorithm. It expresses the time complexity of a problem of size n as a function of the time
complexity of smaller subproblems. For example, the recurrence for Merge Sort is T(n)=2T(n/2)+O(n),
where 2T(n/2) is the time for the two recursive calls and O(n) is the time for the merge step.
1. Substitution Method:
Brief explanation: This is a powerful "guess and check" method. We first guess the form of the
solution (e.g., O(nlogn)), and then we use mathematical induction to prove that the guess is
correct. It's effective but requires a good initial guess, which can be challenging.
2. Recursion-Tree Method:
Brief explanation: This method visualizes the recurrence as a tree, where each node
represents the cost of a single subproblem. The root is the total cost of the problem, and its
children are the costs of the subproblems it calls. We then sum the costs at each level of the tree
to find the total cost. This method is intuitive and helps in making an educated guess for the
substitution method.
3. Master Theorem:
Brief explanation: This is a "cookbook" method for solving a specific type of recurrence relation
of the form T(n)=aT(n/b)+f(n), where a≥1,b>1 are constants and f(n) is an asymptotically positive
function. The theorem provides three cases to quickly determine the solution based on the
comparison of f(n) with nlogba. It's the easiest method for solving recurrences that fit its form.
1. Divide: The list is recursively split into sublists until each sublist contains only one element.
<25,15,23,16,5,1,34,11,22,12,23>
<25,15,23,16,5,1> and <34,11,22,12,23>
...and so on, until each element is in its own list.
2. Conquer (Merge): The sorted sublists are merged back together.
Merge <25> and <15> to get <15, 25> .
Merge <23> and <16> to get <16, 23> .
...
Merge <15, 25> and <16, 23> to get <15, 16, 23, 25> .
...
This process continues, merging the sorted sublists at each level.
3. Final Merge: The two largest sorted sublists are merged to produce the final sorted list:
<1, 5, 15, 16, 23, 25> and <11, 12, 22, 23, 34> .
Merging these gives: <1, 5, 11, 12, 15, 16, 22, 23, 23, 25, 34> .
Divide: The algorithm first divides the search space into two halves by finding the middle element of
the array.
Conquer: It then compares the middle element with the target value.
If the middle element is the target, the search is complete.
If the target is smaller than the middle element, the algorithm discards the right half and
recursively searches in the left half.
If the target is larger, it discards the left half and recursively searches in the right half.
Combine: The results of the subproblems are trivial to combine, as the search continues on only
one half of the array.
Standard Multiplication: For two n×n matrices, it requires 8 multiplications and 4 additions of
n/2×n/2 sub-matrices.
Strassen's Approach: Instead of 8 recursive multiplications, Strassen's algorithm ingeniously uses
only 7 multiplications of sub-matrices and a series of additions and subtractions to compute the final
product.
Steps:
Let C[j] be the minimum number of coins to make change for amount j.
The recurrence relation is: C[j]=1+min(C[j−d1],C[j−d2],...)
Base case: C[0]=0.
C[0]=0
C[1]=1+C[0]=1 (1 coin of value 1)
C[2]=1+C[1]=2 (2 coins of value 1)
C[3]=1+C[2]=3 (3 coins of value 1)
C[4]=1+min(C[3],C[0])=1+C[0]=1 (1 coin of value 4)
C[5]=1+min(C[4],C[1])=1+C[1]=2 (1 coin of value 4, 1 coin of value 1)
C[6]=1+min(C[5],C[2],C[0])=1+C[0]=1 (1 coin of value 6)
C[7]=1+min(C[6],C[3],C[1])=1+C[1]=2 (1 coin of value 6, 1 coin of value 1)
C[8]=1+min(C[7],C[4],C[2])=1+C[4]=2 (1 coin of value 4, 1 coin of value 4)
Answer: The minimum number of coins for Rs. 8 is 2, using two coins of value 4.
Recurrence Relation:
If wi>w, then K[i][w]=K[i−1][w] (cannot include item i)
If wi≤w, then K[i][w]=max(K[i−1][w],Vi+K[i−1][w−wi]) (max of not including item i vs. including it)
Item/Weight (w) 0 1 2 3 4 5
0 0 0 0 0 0 0
Item/Weight (w) 0 1 2 3 4 5
(2,12) 0 0 12 12 12 12
(1,10) 0 10 12 22 22 22
(3,20) 0 10 12 20 30 32
(2,15) 0 10 15 25 30 35
Final Answer: The maximum value is 35. This is achieved by taking items with weights 1, 2, and 2,
which gives a total weight of 5 and a total value of 10+15+10. (wait, that's not right. Let's re-
calculate).
Correct items: (2,12), (1,10), (2,15). Total weight: 2+1+2=5. Total value: 12+10+15=37.
Let's re-examine the table. The cell K[4][5] is max(K[3][5], V_4 + K[3][5-w_4]) = max(32, 15
+ K[3][3]) = max(32, 15+20) = 35 .
Let's check the items for 35: We took item 4 (value 15, weight 2). We are left with capacity 3 and
items 1-3. K[3][3] is 20, which is item 3 (value 20, weight 3). So we took item 3 and 4. Total value
20+15=35. Total weight 3+2=5.
The final answer is 35.
Recurrence Relation:
If X[i−1]==Y[j−1], then LCS_Table[i][j]=1+LCS_Table[i−1][j−1].
If X[i−1] =Y[j−1], then LCS_Table[i][j]=max(LCS_Table[i−1][j],LCS_Table[i][j−1]).
Illustration:
X=A B C B D A B (length 7)
Y=B D C A B A (length 6)
B D C A B A
0 0 0 0 0 0 0
A 0 0 0 0 1 1 1
B 0 1 1 1 1 2 2
C 0 1 1 2 2 2 2
B 0 1 1 2 2 3 3
D 0 1 2 2 2 3 3
A 0 1 2 2 3 3 4
B 0 1 2 2 3 4 4
The value in the bottom-right cell LCS_Table[7][6] is 4, which is the length of the LCS.
To find the actual subsequence, we backtrack through the table from the bottom-right corner.
Start at (7,6), X[6] != Y[5] . Go up to (7,5).
At (7,5), X[6] == Y[4] ('B'). LCS length increased. Take 'B' and go diagonally up to (6,4).
At (6,4), X[5] == Y[3] ('A'). LCS length increased. Take 'A' and go diagonally up to (5,3).
At (5,3), X[4] != Y[2] . Go up to (4,3).
At (4,3), X[3] == Y[2] ('C'). LCS length increased. Take 'C' and go diagonally up to (3,2).
At (3,2), X[2] != Y[1] . Go up to (2,2).
At (2,2), X[1] == Y[1] ('B'). LCS length increased. Take 'B' and go diagonally up to (1,1).
At (1,1), X[0] != Y[0] . Go right to (1,0).
Backtracking gives us the sequence in reverse. So, the LCS is B C B A. (Wait, let's retrace the
table).
Backtracking (Correct):
(7,6) -> go left -> (7,5)
(7,5) -> match B -> go diagonally to (6,4) -> Subsequence: B
(6,4) -> match A -> go diagonally to (5,3) -> Subsequence: AB
(5,3) -> go left or up -> up to (4,3)
(4,3) -> match C -> go diagonally to (3,2) -> Subsequence: CAB
(3,2) -> go left or up -> up to (2,2)
(2,2) -> match B -> go diagonally to (1,1) -> Subsequence: BCAB
...
The final LCS is indeed BCAB (or BCBA, depending on which path is taken, but the length is the
same). Let's trace it carefully.
The 4 at the bottom right is from 3+1 . 3 is at LCS(X[1..6], Y[1..5]) . X[6] = B , Y[5]=A ,
no match. So we take max(LCS(X[1..6], Y[1..5]), LCS(X[1..7], Y[1..4])) .
Let's check the table again. The value 4 is obtained at LCS(X[1..7], Y[1..6]) from the
match X[6]=B and Y[4]=B .
The path is: (7,5) -> (6,4) -> (5,3) -> (4,3) -> (3,2) -> (2,2) -> (1,1) .
Matches are at (7,5) which is X[6] and Y[4] , both B . The match is at (7,5) , not (7,6) .
The LCS is B C B A. The table is filled correctly, but my backtracking was initially flawed. The
final length is indeed 4.