Unit 4 ADSA 13 mark
Unit 4 ADSA 13 mark
Unit 4 ADSA 13 mark
13-mark questions:
1. Explain about dynamic programming and its elements.
A dynamic programming algorithm will examine the previously solved sub problems
and will combine their solutions to give the best solution for the given problem.
Dynamic programming is used where we have problems, which can be divided into
similar sub-problems, so that their results can be re-used. Mostly, these algorithms are
used for optimization. Before solving the in-hand sub-problem, dynamic algorithm will
try to examine the results of the previously solved sub-problems. The solutions of sub-
problems are combined in order to achieve the best solution. It is used when the solution
can be recursively described in terms of solutions to subproblems (optimal
substructure).
The following computer problems can be solved using dynamic programming approach
–
• Fibonacci sequence
• 0-1 knapsack problem
• Longest Common Subsequence problem
• Matrix chain multiplication
• All pairs shortest path problem
• Subset-sum problem
• Optimal Binary Search Tree
In the case of any dynamic programming problem, when we divide the problem, the
subproblems are not independent, instead the solutions to these subproblems are used
to compute the values for the larger problems.
These repeating subproblems are used again and again and hence their results are
stored. If the results of subproblems are not stored, we would have to compute them
again and again, which will be very costly.
By storing the solutions of sub-problems in extra memory space, we thereby reduce the
time taken to calculate the complete solution for a problem. This method of storing the
answers to similar subproblems is called memoization.
Because of this, dynamic programming can solve a vast variety of problems involving
optimal solutions, which means finding the best case out of all. In these cases, also,
dynamic programming takes help of smaller subproblems to reach to the final optimal
solution.
In Dynamic Programming, we analyse all subproblems to find out the most optimal
solution, but because we store these values, no subproblem is recomputed. This
increases the efficiency of a Dynamic Programming solution and also assures a correct
solution as we are basically checking all the cases, but efficiently.
Two characteristics that a problem has for a Dynamic Programming solution to work
are:
1. Optimal Substructure
2. Overlapping Subproblems
Optimal Substructure
Taking the example of the Fibonacci Sequence again, the nth term is given by :
So, to find the nth term, we need to find the correct solution for the (n-1) th and
the (n-2)th term, which are smaller subproblems of the complete problem. And again
to find the (n-1)th term and the (n-2)th term, we need to find the solution to even
smaller subproblems.
Overlapping Subproblems
From this, it is clear that the subproblems F(n-2), F(n-3), and F(n-4) are used again and
again to find the final solution.
1. Stages
2. States and state variables
3. State Transition
4. Optimal Choice
1. Stages
When a complex problem is divided into several subproblems, each subproblem forms
a stage of the solution. After calculating the solution for each stage and choosing the
best ones we get to the final optimized solution.
Each subproblem can be associated with several states. States differ in their solutions
because of different choices. A state for a subproblem is therefore defined more clearly
based on a parameter, which is called the state variable. It is possible in some problems,
that one than one variable is needed to define a state distinctly. In these cases, there are
more than one state variables.
3. State Transition
State Transition simply refers to how one subproblem relates to other subproblems. By
using this state transition, we calculate our end solution.
4. Optimal Choice
At each stage, we need to choose the option which leads to the most desirable solution.
Choosing the most desirable option at every stage will eventually lead to an optimal
solution in the end.
Elements Of Dynamic Programming
1. Substructure
2. Table Structure
3. Bottom-Up Computation (Memoization)
• To solve a given complex problem and to find its optimal solution, it is broken
down into similar but smaller and easily computable problems called
subproblems. Hence, the complete solution depends on many smaller problems
and their solutions. We get to the final optimal solution after going through all
subproblems and selecting the most optimal ones. This is the substructure
element of any Dynamic Programming solution.
• Any Dynamic Programming solution involves storing the optimal solutions of
the subproblems so that they don't have to be computed again and again. To
store these solutions a table structure is needed. So, for example arrays in C++
or ArrayList in Java can be used. By using this structured table, the solutions of
previous subproblems are reused.
• The solutions to subproblems need to be computed first to be reused again. This
is called Bottom-Up Computation because we start storing values from the
bottom and then consequently upwards. The solutions to the smaller
subproblems are combined to get the final solution to the original problem.
2. Explain how to solve the following problems using dynamic programming with
example.
LCS Problem Statement: Given two sequences, find the length of longest
subsequence present in both of them. A subsequence is a sequence that appears in the
same relative order, but not necessarily contiguous. For example, “abc”, “abg”, “bdf”,
“aeg”, ‘”acefg”, .. etc are subsequences of “abcdefg”.
The naive solution for this problem is to generate all subsequences of both given
sequences and find the longest matching subsequence. This solution is exponential
in term of time complexity. Let us see how this problem possesses both important
properties of a Dynamic Programming (DP) Problem.
1) Optimal Substructure:
Let the input sequences be X[0..m-1] and Y[0..n-1] of lengths m and n respectively.
And let L(X[0..m-1], Y[0..n-1]) be the length of LCS of the two sequences X and Y.
Following is the recursive definition of L(X[0..m-1], Y[0..n-1]).
If last characters of both sequences match (or X[m-1] == Y[n-1]) then
L(X[0..m-1], Y[0..n-1]) = 1 + L(X[0..m-2], Y[0..n-2])
If last characters of both sequences do not match (or X[m-1] != Y[n-1]) then
L(X[0..m-1], Y[0..n-1]) = MAX ( L(X[0..m-2], Y[0..n-1]), L(X[0..m-1], Y[0..n-2]) )
Examples:
1) Consider the input strings “AGGTAB” and “GXTXAYB”. Last characters match
for the strings. So length of LCS can be written as:
L(“AGGTAB”, “GXTXAYB”) = 1 + L(“AGGTA”, “GXTXAY”)
2) Consider the input strings “ABCDGH” and “AEDFHR. Last characters do not
match for the strings. So length of LCS can be written as:
L(“ABCDGH”, “AEDFHR”) = MAX ( L(“ABCDG”, “AEDFHR”), L(“ABCDGH”,
“AEDFH”) )
So the LCS problem has optimal substructure property as the main problem can be
solved using solutions to subproblems.
2) Overlapping Subproblems:
Following is simple recursive implementation of the LCS problem. The
implementation simply follows the recursive structure mentioned above .
#include <bits/stdc++.h>
using namespace std;
/* Returns length of LCS for X[0..m-1], Y[0..n-1] */
int lcs( char *X, char *Y, int m, int n )
{
if (m == 0 || n == 0)
return 0;
if (X[m-1] == Y[n-1])
return 1 + lcs(X, Y, m-1, n-1);
else
return max(lcs(X, Y, m, n-1), lcs(X, Y, m-1, n));
}
/* Driver code */
int main()
{
char X[] = "AGGTAB";
char Y[] = "GXTXAYB";
int m = strlen(X);
int n = strlen(Y);
cout<<"Length of LCS is "<< lcs( X, Y, m, n ) ;
return 0;
}
Output: Length of LCS is 4
Matrix Chain Multiplication
Given a sequence of matrices, find the most efficient way to multiply these matrices
together. The problem is not actually to perform the multiplications, but merely to
decide in which order to perform the multiplications.
We have many options to multiply a chain of matrices because matrix multiplication
is associative. In other words, no matter how we parenthesize the product, the result
will be the same. For example, if we had four matrices A, B, C, and D, we would
have:
(ABC)D = (AB)(CD) = A(BCD) = ....
However, the order in which we parenthesize the product affects the number of simple
arithmetic operations needed to compute the product, or the efficiency. For example,
suppose A is a 10 × 30 matrix, B is a 30 × 5 matrix, and C is a 5 × 60 matrix. Then,
(AB)C = (10×30×5) + (10×5×60) = 1500 + 3000 = 4500 operations
A(BC) = (30×5×60) + (10×30×60) = 9000 + 18000 = 27000 operations.
Clearly the first parenthesization requires less number of operations.
Given an array p[] which represents the chain of matrices such that the ith matrix Ai
is of dimension p[i-1] x p[i]. We need to write a function MatrixChainOrder() that
should return the minimum number of multiplications needed to multiply the chain.
Input: p[] = {10, 20, 30}
Output: 6000
There are only two matrices of dimensions 10x20 and 20x30. So there
is only one way to multiply the matrices, cost of which is 10*20*30
1) Optimal Substructure:
A simple solution is to place parenthesis at all possible places, calculate the cost for
each placement and return the minimum value. In a chain of matrices of size n, we
can place the first set of parenthesis in n-1 ways. For example, if the given chain is
of 4 matrices. let the chain be ABCD, then there are 3 ways to place first set of
parenthesis outer side: (A)(BCD), (AB)(CD) and (ABC)(D). So when we place a set
of parenthesis, we divide the problem into subproblems of smaller size. Therefore,
the problem has optimal substructure property and can be easily solved using
recursion.
Minimum number of multiplication needed to multiply a chain of size n = Minimum
of all n-1 placements (these placements create subproblems of smaller size)
2) Overlapping Subproblems
Following is a recursive implementation that simply follows the above optimal
substructure property.
Below is the implementation of the above idea:
#include <bits/stdc++.h>
using namespace std;
// Matrix Ai has dimension p[i-1] x p[i]
// for i = 1..n
int MatrixChainOrder(int p[], int i, int j)
{
if (i == j)
return 0;
int k;
int min = INT_MAX;
int count;
// place parenthesis at different places
// between first and last matrix, recursively
// calculate count of multiplications for
// each parenthesis placement and return the
// minimum count
for (k = i; k < j; k++)
{
count = MatrixChainOrder(p, i, k) + MatrixChainOrder(p, k + 1, j)+ p[i - 1] *
p[k] * p[j];
if (count < min)
min = count;
}
// Return minimum count
return min;
}
// Driver Code
int main()
{
int arr[] = { 1, 2, 3, 4, 3 };
int n = sizeof(arr) / sizeof(arr[0]);
• To construct the solution in an optimal way, this algorithm creates two sets where
one set contains all the chosen items, and another set contains the rejected items.
• A Greedy algorithm makes good local choices in the hope that the solution should
be either feasible or optimal.
Advantages
The biggest advantage that the Greedy algorithm has over others is that it is easy to
Greedy algorithm makes decisions based on the information available at each phase
without considering the broader problem. So, there might be a possibility that the
greedy solution does not give the best solution for every problem.
It follows the local optimum choice at each stage with a intend of finding the global
optimum.
Greedy Algorithm
2. At each step, an item is added to the solution set until a solution is reached.
If a Greedy Algorithm can solve a problem, then it generally becomes the best method
to solve that problem as the Greedy algorithms are in general more efficient than other
techniques like Dynamic Programming. But Greedy algorithms cannot always be
applied. For example, the Fractional Knapsack problem can be solved using Greedy,
but 0-1 Knapsack cannot be solved using Greedy.
Following are some standard algorithms that are Greedy algorithms.
The greedy algorithms are sometimes also used to get an approximation for Hard
optimization problems. For example, Traveling Salesman Problem is an NP-Hard
problem. A Greedy choice for this problem is to pick the nearest unvisited city from the
current city at every step. These solutions don’t always produce the best optimal
solution but can be used to get an approximately optimal solution.
Step 2: Extract two minimum frequency nodes from min heap. Add a new internal
node with frequency 5 + 9 = 14.
character Frequency
c 12
d 13
Internal Node 14
e 16
f 45
Step 3: Extract two minimum frequency nodes from heap. Add a new internal
node with frequency 12 + 13 = 25
Now min heap contains 4 nodes where 2 nodes are roots of trees with single
element each, and two heap nodes are root of tree with more than one nodes
character Frequency
Internal Node 14
e 16
Internal Node 25
f 45
Step 4: Extract two minimum frequency nodes. Add a new internal node with
frequency 14 + 16 = 30
Huffman Coding step 4
character Frequency
Internal Node 25
Internal Node 30
f 45
Step 5: Extract two minimum frequency nodes. Add a new internal node with
frequency 25 + 30 = 55
character Frequency
f 45
Internal Node 55
Step 6: Extract two minimum frequency nodes. Add a new internal node with
frequency 45 + 55 = 100
character code-word
f 0
c 100
d 101
a 1100
b 1101
e 111