Introduction To Dynamic Programming
Introduction To Dynamic Programming
Dynamic Programming is an algorithmic approach to solve some complex problems easily and
save time and number of comparisons by storing the results of past computations. The basic idea
of dynamic programming is to store the results of previous calculation and reuse it in future
instead of recalculating them.
We can also see Dynamic Programming as dividing a particular problem into subproblems and
then storing the result of these subproblems to calculate the result of the actual problem.
and,
fib(0) = 0
fib(1) = 1
We can see that the above function fib() to find the nth fibonacci number is divided into two
subproblems fib(n-1) and fib(n-2) each one of which will be further divided into subproblems
and so on.
int fib(int n)
{
if (n <= 1)
return n;
We can see that the function fib(3) is being called 2 times. If we would have stored the value of
fib(3), then instead of computing it again, we could have reused the old stored value.
The time complexity of the recursive solution is exponential. However, we can improve the
time complexity by using Dynamic Programming approach and storing the results of the
subproblems as shown below:
int fib(int n)
{
// Declare an array to store Fibonacci numbers
int f[n+2]; // 1 extra to handle case, n = 0
int i;
return f[n];
}
The time complexity of the above solution is linear.
Properties of a Dynamic Programming Problem
There are two main properties of any problem which identifies a problem that it can be solved
using the dynamic programming approach:
•
/* simple recursive program for Fibonacci numbers */
int fib(int n)
{
if ( n <= 1 )
return n;
return fib(n-1) + fib(n-2);
}
Recursion tree for execution of fib(5):
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
fib(1) fib(0)
We can see that the function fib(3) is being called 2 times. If we would have stored the value of
fib(3), then instead of computing it again, we could have reused the old stored value.
• Optimal Substructure: A given problem has Optimal Substructure Property if an optimal
solution of the given problem can be obtained by using optimal solutions of its subproblems. For
example, the Shortest Path problem has the following optimal substructure property: If a node x
lies in the shortest path from a source node u to destination node v then the shortest path from u
to v is combination of shortest path from u to x and shortest path from x to v. The standard All
Pair Shortest Path algorithms like Floyd–Warshall and Bellman-Ford are typical examples of
Dynamic Programming. On the other hand, the Longest Path problem doesn’t have the Optimal
Substructure property. Here, by Longest Path we mean longest simple path (path without cycle)
between any two nodes. Consider the following unweighted graph given in the CLRS book.
There are two longest paths from q to t: q->r->t and q->s->t. Unlike shortest paths, these longest
paths do not have the optimal substructure property. For example, the longest path q->r->t is not
a combination of the longest path from q to r and longest path from r to t, because the longest
We had already discussed the basics of Overlapping Subproblems property of a problem that
can be solved using Dynamic Programming algorithm. Let us extend our previous example of
Fibonacci Number to discuss the overlapping subproblems property in details.
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
fib(1) fib(0)
We already discussed how storing results of the subproblems can be effective in reducing the
number of calculations or operations to obtain the final result. As in the above recursion tree, we
can see that different values like fib(1), fib(0), fib(2) are being calculated more than once. There
are two different ways to store the values so that these values can be reused:
#define NIL -1
#define MAX 100
int lookup[MAX];
return lookup[n];
}
// Driver code
int main ()
{
int n = 40;
_initialize();
cout << "Fibonacci number is " << fib(n);
return 0;
}
Output:
Fibonacci number is 102334155
1. Tabulation (Bottom Up): The tabulated program for a given problem builds a table in
bottom up fashion and returns the last entry from table. For example, for the same
Fibonacci number, we first calculate fib(0) then fib(1) then fib(2) then fib(3) and so on.
So literally, we are building the solutions of subproblems bottom-up. Following is the
tabulated version for nth Fibonacci Number.
#include<bits/stdc++.h>
int fib(int n)
{
int f[n+1];
int i;
f[0] = 0; f[1] = 1;
return f[n];
}
// Driver Code
int main ()
{
int n = 9;
printf("Fibonacci number is %d ", fib(n));
return 0;
}
Output:
Fibonacci number is 34
Both Tabulated and Memoized approaches stores the solutions of subproblems. In Memoized
version, the table is filled on demand while in Tabulated version, starting from the first entry, all
entries are filled one by one. Unlike the Tabulated version, all entries of the lookup table are not
necessarily filled in Memoized version.
Optimal Substructure Property
A given problem has Optimal Substructure Property if the optimal solution of the given
problem can be obtained by using optimal solutions of its subproblems.
That is, say if a problem x is divided into subproblems A and B then the optimal solution of x can
be obtained by summing up the optimal solutions to the subproblems A and B.
For example, the Shortest Path problem has following optimal substructure property:
If a node x lies in the shortest path from a source node u to destination node v then the shortest
path from u to v is combination of shortest path from u to x and shortest path from x to v. The
standard All Pair Shortest Path algorithms like Floyd–Warshall and Bellman–Ford are typical
examples of Dynamic Programming.
Let us consider a simple example of 0-1 Knapsack Problem. The problem states that given
values and weight associated with N items. The task is to put these items into a Knapsack of
capacity W such that the value of all items in the Knapsack is maximum possible. You can either
include a complete element or do not include it, it is not allowed to add a fraction of an element.
For Example:
The answer will be 220. We will pick the 2nd and 3rd elements
and add them to the Knapsack for maximum value.
Optimal Substructure: To consider all subsets of items, there can be two cases for every item:
(1) the item is included in the optimal subset, (2) not included in the optimal set.
Therefore, the maximum value that can be obtained from N items is the max of the following
two values.
1. Maximum value obtained by n-1 items and W weight (excluding nth item).
2. Value of nth item plus maximum value obtained by n-1 items and W minus the weight of
the nth item (including nth item).
If the weight of the nth item is greater than W, then the nth item cannot be included and case 1 is
the only possibility.
Overlapping Subproblems: Let us first look at the recursive solution to the above problem:
It should be noted that the above function computes the same subproblems again and again. See
the following recursion tree when the above recursive function is evaluated with the sample
examples.
Since sub-problems are evaluated again, this problem has Overlapping Subproblems property. So
the 0-1 Knapsack problem has both properties of a dynamic programming problem. Like other
typical Dynamic Programming(DP) problems, recomputations of same subproblems can be
avoided by constructing a temporary array K[][] in a bottom-up manner. Following is Dynamic
Programming based implementation.
#include <bits/stdc++.h>
using namespace std;
return K[n][W];
}
// Driver Code
int main()
{
int val[] = { 60, 100, 120 };
int wt[] = { 10, 20, 30 };
int W = 50;
int n = sizeof(val) / sizeof(val[0]);
cout << knapSack(W, wt, val, n);
return 0;
}
Output:
220
Solving a Dynamic Programming Problem
Dynamic Programming (DP) is a technique that solves some particular type of problems in
Polynomial Time. Dynamic Programming solutions are faster than exponential brute method and
can be easily proved for their correctness. Before we study how to think Dynamically for a
problem, we need to learn:
1. Overlapping Subproblems
2. Optimal Substructure Property
Steps to solve a DP
1) Identify if it is a DP problem
2) Decide a state expression with
least parameters
3) Formulate state relationship
4) Do tabulation (or add memoization)
Step 1 : How to classify a problem as a Dynamic Programming Problem?
• Typically, all the problems that require to maximize or minimize certain quantity or
counting problems that say to count the arrangements under certain condition or certain
probability problems can be solved by using Dynamic Programming.
• All dynamic programming problems satisfy the overlapping subproblems property and
most of the classic dynamic problems also satisfy the optimal substructure property.
Once, we observe these properties in a given problem, be sure that it can be solved using
DP.
Step 2 : Deciding the state DP problems are all about state and their transition. This is the most
basic step which must be done very carefully because the state transition depends on the choice
of state definition you make. So, let's see what do we mean by the term "state". State A state can
be defined as the set of parameters that can uniquely identify a certain position or standing in the
given problem. This set of parameters should be as small as possible to reduce state space. For
example: In our famous Knapsack problem, we define our state by two parameters index and
weight i.e DP[index][weight]. Here DP[index][weight] tells us the maximum profit it can make
by taking items from range 0 to index having the capacity of sack to be weight. Therefore, here
the parameters index and weight together can uniquely identify a subproblem for the knapsack
problem. So, our first step will be deciding a state for the problem after identifying that the
problem is a DP problem. As we know DP is all about using calculated results to formulate the
final result. So, our next step will be to find a relation between previous states to reach the
current state.
Step 3 : Formulating a relation among the states This part is the hardest part of for solving a
DP problem and requires a lot of intuition, observation and practice. Let's understand it by
considering a sample problem
Given 3 numbers {1, 3, 5}, we need to tell
the total number of ways we can form a number 'N'
using the sum of the given three numbers.
(allowing repetitions and different arrangements).
Total number of ways to form 6 is: 8
1+1+1+1+1+1
1+1+1+3
1+1+3+1
1+3+1+1
3+1+1+1
3+3
1+5
5+1
Let's think dynamically about this problem. So, first of all, we decide a state for the given
problem. We will take a parameter n to decide state as it can uniquely identify any subproblem.
So, our state dp will look like state(n). Here, state(n) means the total number of arrangements to
form n by using {1, 3, 5} as elements. Now, we need to compute state(n). How to do it? So here
the intuition comes into action. As we can only use 1, 3 or 5 to form a given number. Let us
assume that we know the result for n = 1,2,3,4,5,6 ; being termilogistic let us say we know the
result for the state (n = 1), state (n = 2), state (n = 3) ......... state (n = 6) Now, we wish to know
the result of the state (n = 7). See, we can only add 1, 3 and 5. Now we can get a sum total of 7
by the following 3 ways: 1) Adding 1 to all possible combinations of state (n = 6) Eg : [
(1+1+1+1+1+1) + 1] [ (1+1+1+3) + 1] [ (1+1+3+1) + 1] [ (1+3+1+1) + 1] [ (3+1+1+1) + 1] [
(3+3) + 1] [ (1+5) + 1] [ (5+1) + 1] 2) Adding 3 to all possible combinations of state (n = 4);
Eg : [(1+1+1+1) + 3] [(1+3) + 3] [(3+1) + 3] 3) Adding 5 to all possible combinations of
state(n = 2) Eg : [ (1+1) + 5] Now, think carefully and satisfy yourself that the above three cases
are covering all possible ways to form a sum total of 7; Therefore, we can say that result for
state(7) = state (6) + state (4) + state (2) or state(7) = state (7-1) + state (7-3) + state (7-5) In
general, state(n) = state(n-1) + state(n-3) + state(n-5) So, our code will look like:
// Returns the number of arrangements to
// form 'n'
int solve(int n)
{
// base case
if (n < 0)
return 0;
if (n == 0)
return 1;
There are two different ways to store the values so that the values of a sub-problem can be
reused. Here, will discuss two patterns of solving dynamic programming (DP) problems:
1. Tabulation: Bottom Up
2. Memoization: Top Down
Before getting to the definitions of the above two terms consider the following statements:
• Version 1: I will study the theory of DP from GeeksforGeeks, then I will practice some
problems on classic DP and hence I will master DP.
• Version 2: To Master DP, I would have to practice Dynamic problems and practice
problems - Firstly, I would have to study some theories of DP from GeeksforGeeks
Both versions say the same thing, the difference simply lies in the way of conveying the message
and that's exactly what Bottom-Up and Top-Down DP do. Version 1 can be related to Bottom-
Up DP and Version-2 can be related as Top-Down DP.
As the name itself suggests starting from the bottom and accumulating answers to the top. Let's
discuss in terms of state transition.
Let's describe a state for our DP problem to be dp[x] with dp[0] as base state and dp[n] as our
destination state. So, we need to find the value of destination state i.e dp[n].
If we start our transition from our base state i.e dp[0] and follow our state transition relation to
reach our destination state dp[n], we call it the Bottom-Up approach as it is quite clear that we
started our transition from the bottom base state and reached the topmost desired state.
To know this let's first write some code to calculate the factorial of a number using a bottom-up
approach. Once, again as our general procedure to solve a DP we first define a state. In this case,
we define a state as dp[x], where dp[x] is to find the factorial of x.
// base case
int dp[0] = 1;
for (int i = 1; i< =n; i++)
{
dp[i] = dp[i-1] * i;
}
The above code clearly follows the bottom-up approach as it starts its transition from the bottom-
most base case dp[0] and reaches its destination state dp[n]. Here, we may notice that the DP
table is being populated sequentially and we are directly accessing the calculated states from the
table itself and hence, we call it the tabulation method.
Once, again let's describe it in terms of state transition. If we need to find the value for some
state say dp[n] and instead of starting from the base state that i.e dp[0] we ask our answer from
the states that can reach the destination state dp[n] following the state transition relation, then it
is the top-down fashion of DP.
Here, we start our journey from the top most destination state and compute its answer by taking
in count the values of states that can reach the destination state, till we reach the bottom-most
base state.
Once again, let's write the code for the factorial problem in the top-down fashion
// initialized to -1
int dp[MAXN]
// return fact x!
int solve(int x)
{
if (x==0)
return 1;
if (dp[x]!=-1)
return dp[x];
return (dp[x] = x * solve(x-1));
}
As we can see we are storing the most recent cache up to a limit so that if next time we got a call
from the same state we simply return it from the memory. So, this is why we call it memoization
as we are storing the most recent state values.
In this case, the memory layout is linear that's why it may seem that the memory is being filled in
a sequential manner like the tabulation method, but you may consider any other top-down DP
having 2D memory layout like Min Cost Path, here the memory is not filled in a sequential
manner.
Sample Problems on Dynamic Programming
1. A binomial coefficient C(n, k) can be defined as the coefficient of X^k in the expansion
of (1 + X)^n.
2. A binomial coefficient C(n, k) also gives the number of ways, disregarding order, that k
objects can be chosen from among n objects; more formally, the number of k-element
subsets (or k-combinations) of an n-element set.
Write a function that takes two parameters n and k and returns the value of Binomial Coefficient
C(n, k). For example, your function should return 6 for n = 4 and k = 2, and it should return 10
for n = 5 and k = 2.
Optimal Substructure
The value of C(n, k) can be recursively calculated using following standard formula for Binomial
Coefficients.
C(n, k) = C(n-1, k-1) + C(n-1, k)
C(n, 0) = C(n, n) = 1
Overlapping Subproblems
It should be noted that the above function computes the same subproblems again and again. See
the following recursion tree for n = 5 an k = 2. The function C(3, 1) is called two times. For large
values of n, there will be many common subproblems.
C(5, 2)
/ \
C(4, 1) C(4, 2)
/ \ / \
C(3, 0) C(3, 1) C(3, 1) C(3, 2)
/ \ / \ / \
C(2, 0) C(2, 1) C(2, 0) C(2, 1) C(2, 1) C(2, 2)
/ \ / \ / \
C(1, 0) C(1, 1) C(1, 0) C(1, 1) C(1, 0) C(1, 1)
Since same suproblems are called again, this problem has Overlapping Subproblems property
Pseudo Code
// Returns value of Binomial Coefficient C(n, k)
int binomialCoeff(int n, int k)
{
int C[n+1][k+1]
// Caculate value of Binomial Coefficient in bottom up manner
for (i = 0; i <= n; i++)
{
for (j = 0; j <= min(i, k); j++)
{
// Base Cases
if (j == 0 || j == i)
C[i][j] = 1
Given an array of integers where each element represents the max number of steps that can be
made forward from that element. Write a function to return the minimum number of jumps to
reach the end of the array (starting from the first element). If an element is 0, then cannot move
through that element.
Example
Input: arr[] = {1, 3, 5, 8, 9, 2, 6, 7, 6, 8, 9}
Output: 3 (1-> 3 -> 8 ->9)
First element is 1, so can only go to 3. Second element is 3, so can make at most 3 steps eg to 5
or 8 or 9.
Solution - we build a jumps[ ] array from left to right such that jumps[ i ] indicates the minimum
number of jumps needed to reach arr[ i ] from arr[ 0 ]. Finally, we return jumps[ n-1 ].
Pseudo Code
// Returns minimum number of jumps
// to reach arr[n-1] from arr[0]
int minJumps(int arr[], int n)
{
// jumps[n-1] will hold the result
int jumps[n]
if (n == 0 || arr[0] == 0)
return INT_MAX;
jumps[0] = 0
Description- The Longest Increasing Subsequence (LIS) problem is to find the length of the
longest subsequence of a given sequence such that all elements of the subsequence are sorted in
increasing order. For example, the length of LIS for {10, 22, 9, 33, 21, 50, 41, 60, 80} is 6 and
LIS is {10, 22, 33, 50, 60, 80}.
More Examples:
Input : arr[] = {3, 10, 2, 1, 20}
Output : Length of LIS = 3
The longest increasing subsequence is 3, 10, 20
Optimal Substructure
Let arr[0..n-1] be the input array and L(i) be the length of the LIS ending at index i such that
arr[i] is the last element of the LIS.
Then, L(i) can be recursively written as:
L(i) = 1 + max( L(j) ) where 0 < j < i and arr[j] < arr[i]; or
L(i) = 1, if no such j exists.
To find the LIS for a given array, we need to return max(L(i)) where 0 < i < n.
Thus, we see the LIS problem satisfies the optimal substructure property as the main problem
can be solved using solutions to subproblems.
Overlapping Subproblems
Considering the above implementation, following is recursion tree for an array of size 4. lis(n)
gives us the length of LIS for arr[ ].
lis(4)
/ | \
lis(3) lis(2) lis(1)
/ \ /
lis(2) lis(1) lis(1)
/
lis(1)
We can see that there are many subproblems which are solved again and again. So this problem
has Overlapping Substructure property and recomputation of same subproblems can be avoided
by either using Memoization or Tabulation.
Pseudo Code
/* lis() returns the length of the longest increasing
subsequence in arr[ ] of size n */
int lis( int arr[], int n )
{
int lis[n]
lis[0] = 1
/* Compute optimized LIS values in bottom up manner */
for (int i = 1; i < n; i++ )
{
lis[i] = 1;
for (int j = 0; j < i; j++ )
if ( arr[i] > arr[j] && lis[i] < lis[j] + 1)
lis[i] = lis[j] + 1
}
// Return maximum value in lis[]
return *max_element(lis, lis+n)
}
Description -Given weights and values of n items, put these items in a knapsack of capacity W
to get the maximum total value in the knapsack. In other words, given two integer arrays
val[0..n-1] and wt[0..n-1] which represent values and weights associated with n items
respectively. Also given an integer W which represents knapsack capacity, find out the
maximum value subset of val[] such that sum of the weights of this subset is smaller than or
equal to W. You cannot break an item, either pick the complete item, or don't pick it (0-1
property).
Optimal Substructure
To consider all subsets of items, there can be two cases for every item: (1) the item is included in
the optimal subset, (2) not included in the optimal set.
Therefore, the maximum value that can be obtained from n items is a max of the following two
values.
1. Maximum value obtained by n-1 items and W weight (excluding nth item).
2. Value of nth item plus maximum value obtained by n-1 items and W minus weight of the
nth item (including nth item).
If weight of nth item is greater than W, then the nth item cannot be included and case 1 is the
only possibility.
Overlapping Subproblems
In the following recursion tree, K() refers to knapSack().
The two parameters indicated in the following recursion tree are n and W.
The recursion tree is for following sample inputs.
wt[] = {1, 1, 1}, W = 2, val[] = {10, 20, 30}
Since suproblems are evaluated again, this problem has Overlapping Subprolems property. So
the 0-1 Knapsack problem has both properties -
Pseudo Code
// Returns the maximum value that can be put in a knapsack of capacity W
int knapSack(int W, int wt[], int val[], int n)
{
int K[n+1][W+1]
// Build table K[][] in bottom up manner
for (i = 0; i <= n; i++)
{
for (w = 0; w <= W; w++)
{
if (i==0 || w==0)
K[i][w] = 0
else if (wt[i-1] <= w)
K[i][w] = max(val[i-1] + K[i-1][w-wt[i-1]], K[i-1][w])
else
K[i][w] = K[i-1][w]
}
}
return K[n][W]
}
Longest Common Subsequence (Part 1)
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".
In order to find out the complexity of brute force approach, we need to first know the number of
possible different subsequences of a string with length n, i.e., find the number of subsequences
with lengths ranging from 1,2,..n-1. Recall from theory of permutation and combination that
number of combinations with 1 element are nC1. Number of combinations with 2 elements are
n
C2 and so forth and so on. We know that nC0 + nC1 + nC2 + ... nCn = 2n. So a string of length n
has 2n-1 different possible subsequences since we do not consider the subsequence with length 0.
This implies that the time complexity of the brute force approach will be O(n * 2n). Note that it
takes O(n) time to check if a subsequence is common to both the strings. This time complexity
can be improved using dynamic programming.
It is a classic computer science problem, the basis of diff (a file comparison program that outputs
the differences between two files), and has applications in bioinformatics.
Examples:
LCS for input Sequences "ABCDGH" and "AEDFHR" is "ADH" of length 3.
LCS for input Sequences "AGGTAB" and "GXTXAYB" is "GTAB" of length 4.
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 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.
/* Driver code */
int main()
{
char X[] = "AGGTAB";
char Y[] = "GXTXAYB";
int m = strlen(X);
int n = strlen(Y);
return 0;
}
Output
Length of LCS is 4
Time complexity of the above naive recursive approach is O(2^n) in worst case and worst case
happens when all characters of X and Y mismatch i.e., length of LCS is 0.
Considering the above implementation, following is a partial recursion tree for input strings
"AXYT" and "AYZX"
lcs("AXYT", "AYZX")
/
lcs("AXY", "AYZX") lcs("AXYT", "AYZ")
/ /
lcs("AX", "AYZX") lcs("AXY", "AYZ") lcs("AXY", "AYZ") lcs("AXYT", "AY")
In the above partial recursion tree, lcs("AXY", "AYZ") is being solved twice. If we draw the
complete recursion tree, then we can see that there are many subproblems which are solved again
and again. So this problem has Overlapping Substructure property and recomputation of same
subproblems can be avoided by either using Memoization or Tabulation.
if (dp[m][n] != -1) {
return dp[m][n];
}
return dp[m][n] = max(lcs(X, Y, m, n - 1, dp),
lcs(X, Y, m - 1, n, dp));
}
/* Driver code */
int main()
{
char X[] = "AGGTAB";
char Y[] = "GXTXAYB";
int m = strlen(X);
int n = strlen(Y);
vector<vector<int> > dp(m + 1, vector<int>(n + 1, -1));
cout << "Length of LCS is " << lcs(X, Y, m, n, dp);
return 0;
}
Output
Length of LCS is 4
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".
In order to find out the complexity of brute force approach, we need to first know the number of
possible different subsequences of a string with length n, i.e., find the number of subsequences
with lengths ranging from 1,2,..n-1. Recall from theory of permutation and combination that
number of combinations with 1 element are nC1. Number of combinations with 2 elements are
n
C2 and so forth and so on. We know that nC0 + nC1 + nC2 + ... nCn = 2n. So a string of length n
has 2n-1 different possible subsequences since we do not consider the subsequence with length 0.
This implies that the time complexity of the brute force approach will be O(n * 2n). Note that it
takes O(n) time to check if a subsequence is common to both the strings. This time complexity
can be improved using dynamic programming.
It is a classic computer science problem, the basis of diff (a file comparison program that outputs
the differences between two files), and has applications in bioinformatics.
Examples:
LCS for input Sequences "ABCDGH" and "AEDFHR" is "ADH" of length 3.
LCS for input Sequences "AGGTAB" and "GXTXAYB" is "GTAB" of length 4.
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 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.
/* Driver code */
int main()
{
char X[] = "AGGTAB";
char Y[] = "GXTXAYB";
int m = strlen(X);
int n = strlen(Y);
return 0;
}
Output
Length of LCS is 4
Time complexity of the above naive recursive approach is O(2^n) in worst case and worst case
happens when all characters of X and Y mismatch i.e., length of LCS is 0.
Considering the above implementation, following is a partial recursion tree for input strings
"AXYT" and "AYZX"
lcs("AXYT", "AYZX")
/
lcs("AXY", "AYZX") lcs("AXYT", "AYZ")
/ /
lcs("AX", "AYZX") lcs("AXY", "AYZ") lcs("AXY", "AYZ") lcs("AXYT", "AY")
In the above partial recursion tree, lcs("AXY", "AYZ") is being solved twice. If we draw the
complete recursion tree, then we can see that there are many subproblems which are solved again
and again. So this problem has Overlapping Substructure property and recomputation of same
subproblems can be avoided by either using Memoization or Tabulation.
/* Driver code */
int main()
{
char X[] = "AGGTAB";
char Y[] = "GXTXAYB";
int m = strlen(X);
int n = strlen(Y);
vector<vector<int> > dp(m + 1, vector<int>(n + 1, -1));
cout << "Length of LCS is " << lcs(X, Y, m, n, dp);
return 0;
}
Output
Length of LCS is 4
int m = strlen(X);
int n = strlen(Y);
return 0;
}
// code submitted by Aditya Yadav (adityayadav012552)
Output
Length of LCS is 4
Time Complexity of the above implementation is O(mn) which is much better than the worst-
case time complexity of Naive Recursive implementation.
Coin Change
Given a value sum, if we want to make change for sum cents, and we have an infinite supply of
each of coins[] = { coins1, coins2, .. , coinsn} valued coins, how many ways can we make the
change? The order of coins doesn't matter.
Examples:
1) Optimal Substructure
To count the total number of solutions, we can divide all set solutions into two sets.
Let count(coins[], n, sum) be the function to count the number of solutions, then it can be written
as sum of count(coins[], n-1, sum) and count(coins[], n, sum-coins[n-1]).
Therefore, the problem has optimal substructure property as the problem can be solved using
solutions to subproblems.
2) Overlapping Subproblems
Following is a simple recursive implementation of the Coin Change problem. The
implementation simply follows the recursive structure mentioned above.
3) Approach (Algorithm)
See, here each coin of a given denomination can come an infinite number of times. (Repetition
allowed), this is what we call UNBOUNDED KNAPSACK. We have 2 choices for a coin of a
particular denomination, either i) to include, or ii) to exclude. But here, the inclusion process is
not for just once; we can include any denomination any number of times until sum<0.
Basically, If we are at coins[n-1], we can take as many instances of that coin ( unbounded
inclusion ) i.e count(coins, n, sum - coins[n-1] ); then we move to coins[n-2]. After moving to
coins[n-2], we can't move back and can't make choices for coins[n-1] i.e count(coins, n-1, sum
).
Finally, as we have to find the total number of ways, so we will add these 2 possible choices, i.e
count(coins, n, sum - coins[n-1] ) + count(coins, n-1, sum ); which will be our required
answer.
// Driver code
int main()
{
int i, j;
int coins[] = { 1, 2, 3 };
int n = sizeof(coins) / sizeof(coins[0]);
int sum = 4;
return 0;
}
Output
4
It should be noted that the above function computes the same subproblems again and again. See
the following recursion tree for coins[] = {1, 2, 3} and n = 5.
The function C({1}, 3) is called two times. If we draw the complete tree, then we can see that
there are many subproblems being called more than once.
Since same subproblems are called again, this problem has Overlapping Subproblems property.
So the Coin Change problem has both properties (see this and this) of a dynamic programming
problem. Like other typical Dynamic Programming(DP) problems, recomputations of same
subproblems can be avoided by constructing a temporary array table[][] in bottom up manner.
// total count
table[i][j] = x + y;
}
}
return table[sum][n - 1];
}
// Driver Code
int main()
{
int coins[] = { 1, 2, 3 };
int n = sizeof(coins) / sizeof(coins[0]);
int sum = 4;
cout << count(coins, n, sum);
return 0;
}
Output
4
Output:
#include <bits/stdc++.h>
using namespace std;
Output
4
Time Complexity: O(M*sum)
Given two strings str1 and str2 and below operations that can be performed on str1. Find
minimum number of edits (operations) required to convert 'str1' into 'str2'.
1. Insert
2. Remove
3. Replace
Examples:
1. If last characters of two strings are same, nothing much to do. Ignore last characters and
get count for remaining strings. So we recur for lengths m-1 and n-1.
2. Else (If last characters are not same), we consider all operations on 'str1', consider all
three operations on last character of first string, recursively compute minimum cost for all
three operations and take minimum of three values.
1. Insert: Recur for m and n-1
2. Remove: Recur for m-1 and n
3. Replace: Recur for m-1 and n-1
// Driver code
int main()
{
// your code goes here
string str1 = "sunday";
string str2 = "saturday";
return 0;
}
Output
3
Output
3
The time complexity of above solution is exponential. In worst case, we may end up doing O(3m)
operations. The worst case happens when none of characters of two strings match. Below is a
recursive call diagram for worst case.
We can see that many subproblems are solved, again and again, for example, eD(2, 2) is called
three times. Since same subproblems are called again, this problem has Overlapping
Subproblems property. So Edit Distance problem has both properties (see this and this) of a
dynamic programming problem. Like other typical Dynamic Programming(DP) problems,
recomputations of same subproblems can be avoided by constructing a temporary array that
stores results of subproblems.
return dp[m][n];
}
// Driver code
int main()
{
// your code goes here
string str1 = "sunday";
string str2 = "saturday";
return 0;
}
Output
3
Space Complex Solution: In the above-given method we require O(m x n) space. This will not
be suitable if the length of strings is greater than 2000 as it can only create 2D array of 2000 x
2000. To fill a row in DP array we require only one row the upper row. For example, if we are
filling the i = 10 rows in DP array we require only values of 9th row. So we simply create a DP
array of 2 x str1 length. This approach reduces the space complexity. Here is the C++
implementation of the above-mentioned problem
// Driver program
int main()
{
string str1 = "food";
string str2 = "money";
EditDistDP(str1, str2);
return 0;
}
Output
4
#include <bits/stdc++.h>
using namespace std;
int minDis(string s1, string s2, int n, int m,
vector<vector<int> >& dp)
{
if (n == 0)
return m;
if (m == 0)
return n;
if (dp[n][m] != -1)
return dp[n][m];
// Driver program
int main()
{
Output
7
Applications: There are many practical applications of edit distance algorithm, refer Lucene API
for sample. Another example, display all the words in a dictionary that are near proximity to a
given wordincorrectly spelled word.
Longest Increasing Subsequence
The Longest Increasing Subsequence (LIS) problem is to find the length of the longest
subsequence of a given sequence such that all elements of the subsequence are sorted in
increasing order. For example, the length of LIS for {10, 22, 9, 33, 21, 50, 41, 60, 80} is 6 and
LIS is {10, 22, 33, 50, 60, 80}.
Examples:
Method 1: Recursion.
Optimal Substructure: Let arr[0..n-1] be the input array and L(i) be the length of the LIS ending
at index i such that arr[i] is the last element of the LIS.
L(i) = 1 + max( L(j) ) where 0 < j < i and arr[j] < arr[i]; or
L(i) = 1, if no such j exists.
To find the LIS for a given array, we need to return max(L(i)) where 0 < i < n.
Formally, the length of the longest increasing subsequence ending at index i, will be 1 greater
than the maximum of lengths of all longest increasing subsequences ending at indices before i,
where arr[j] < arr[i] (j < i).
Thus, we see the LIS problem satisfies the optimal substructure property as the main problem
can be solved using solutions to subproblems.
The recursive tree given below will make the approach clearer:
Input : arr[] = {3, 10, 2, 11}
f(i): Denotes LIS of subarray ending at index 'i'
(LIS(1)=1)
// returns max
return max;
}
Output
Length of lis is 5
Complexity Analysis:
Iteration-wise simulation :
1. arr[2] > arr[1] {LIS[2] = max(LIS [2], LIS[1]+1)=2}
2. arr[3] < arr[1] {No change}
3. arr[3] < arr[2] {No change}
4. arr[4] > arr[1] {LIS[4] = max(LIS [4], LIS[1]+1)=2}
5. arr[4] > arr[2] {LIS[4] = max(LIS [4], LIS[2]+1)=3}
6. arr[4] > arr[3] {LIS[4] = max(LIS [4], LIS[3]+1)=3}
We can avoid recomputation of subproblems by using tabulation as shown in the below code:
lis[0] = 1;
return 0;
}
Output
Length of lis is 5
Complexity Analysis:
Method 3 : Memoization DP
We can see that there are many subproblems in the above recursive solution which are solved
again and again. So this problem has Overlapping Substructure property and recomputation of
same subproblems can be avoided by either using Memoization
if (dp[idx][prev_idx + 1] != -1) {
return dp[idx][prev_idx + 1];
}
Output
Length of lis is 3
Complexity Analysis:
Given an array of random numbers. Find longest increasing subsequence (LIS) in the array. I
know many of you might have read recursive and dynamic programming (DP) solutions. There
are few requests for O(N log N) algo in the forum posts.
For the time being, forget about recursive and DP solutions. Let us take small samples and
extend the solution to large instances. Even though it may look complex at first time, once if we
understood the logic, coding is simple.
Consider an input array A = {2, 5, 3}. I will extend the array during explanation.
By observation we know that the LIS is either {2, 3} or {2, 5}. Note that I am considering only
strictly increasing sequences.
Let us add two more elements, say 7, 11 to the array. These elements will extend the existing
sequences. Now the increasing sequences are {2, 3, 7, 11} and {2, 5, 7, 11} for the input array
{2, 5, 3, 7, 11}.
Further, we add one more element, say 8 to the array i.e. input array becomes {2, 5, 3, 7, 11, 8}.
Note that the latest element 8 is greater than smallest element of any active sequence (will
discuss shortly about active sequences). How can we extend the existing sequences with 8? First
of all, can 8 be part of LIS? If yes, how? If we want to add 8, it should come after 7 (by replacing
11).
Since the approach is offline (what we mean by offline?), we are not sure whether adding 8 will
extend the series or not. Assume there is 9 in the input array, say {2, 5, 3, 7, 11, 8, 7, 9 ...}. We
can replace 11 with 8, as there is potentially best candidate (9) that can extend the new series {2,
3, 7, 8} or {2, 5, 7, 8}.
Our observation is, assume that the end element of largest sequence is E. We can add (replace)
current element A[i] to the existing sequence if there is an element A[j] (j > i) such that E < A[i]
< A[j] or (E > A[i] < A[j] - for replace). In the above example, E = 11, A[i] = 8 and A[j] = 9.
In case of our original array {2, 5, 3}, note that we face same situation when we are adding 3 to
increasing sequence {2, 5}. I just created two increasing sequences to make explanation simple.
Instead of two sequences, 3 can replace 5 in the sequence {2, 5}.
I know it will be confusing, I will clear it shortly!
The question is, when will it be safe to add or replace an element in the existing sequence?
Let us consider another sample A = {2, 5, 3}. Say, the next element is 1. How can it extend the
current sequences {2, 3} or {2, 5}. Obviously, it can't extend either. Yet, there is a potential that
the new smallest element can be start of an LIS. To make it clear, consider the array is {2, 5, 3, 1,
2, 3, 4, 5, 6}. Making 1 as new sequence will create new sequence which is largest.
The observation is, when we encounter new smallest element in the array, it can be a potential
candidate to start new sequence.
From the observations, we need to maintain lists of increasing sequences.
In general, we have set of active lists of varying length. We are adding an element A[i] to these
lists. We scan the lists (for end elements) in decreasing order of their length. We will verify the
end elements of all the lists to find a list whose end element is smaller than A[i] (floor value).
Our strategy determined by the following conditions,
1. If A[i] is smallest among all end
candidates of active lists, we will start
new active list of length 1.
2. If A[i] is largest among all end candidates of
active lists, we will clone the largest active
list, and extend it by A[i].
3. If A[i] is in between, we will find a list with
largest end element that is smaller than A[i].
Clone and extend this list by A[i]. We will discard all
other lists of same length as that of this modified list.
Note that at any instance during our construction of active lists, the following condition is
maintained.
"end element of smaller list is smaller than end elements of larger lists".
It will be clear with an example, let us take example from wiki {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5,
13, 3, 11, 7, 15}.
#include <iostream>
#include <vector>
return r;
}
int LongestIncreasingSubsequenceLength(std::vector<int>& v)
{
if (v.size() == 0)
return 0;
tail[0] = v[0];
for (size_t i = 1; i < v.size(); i++) {
return length;
}
int main()
{
std::vector<int> v{ 2, 5, 3, 7, 11, 8, 10, 13, 6 };
std::cout << "Length of Longest Increasing Subsequence is "
<< LongestIncreasingSubsequenceLength(v) << '\n';
return 0;
}
Output:
Complexity:
The loop runs for N elements. In the worst case (what is worst case input?), we may end up
querying ceil value using binary search (log i) for many A[i].
Therefore, T(n) < O( log N! ) = O(N log N). Analyse to ensure that the upper and lower bounds
are also O( N log N ). The complexity is THETA (N log N).
Exercises:
1. Design an algorithm to construct the longest increasing list. Also, model your solution using
DAGs.
2. Design an algorithm to construct all increasing lists of equal longest size.
3. Is the above algorithm an online algorithm?
4. Design an algorithm to construct the longest decreasing list..
Alternate implementation in various languages using their built in binary search functions
are given below:
#include <bits/stdc++.h>
using namespace std;
int LongestIncreasingSubsequenceLength(std::vector<int>& v)
{
if (v.size() == 0) // boundary case
return 0;
tail[0] = v[0];
return length;
}
int main()
{
std::vector<int> v{ 2, 5, 3, 7, 11, 8, 10, 13, 6 };
std::cout
<< "Length of Longest Increasing Subsequence is "
<< LongestIncreasingSubsequenceLength(v);
return 0;
}
Output:
Given a rod of length L, the task is to cut the rod in such a way that the total number of segments
of length p, q and r is maximized. The segments can only be of length p, q, and r.
Examples:
Input: l = 11, p = 2, q = 3, r = 5
Output: 5
Segments of 2, 2, 2, 2 and 3
Input: l = 7, p = 2, q = 5, r = 5
Output: 2
Segments of 2 and 5
Approach 1:
This can be visualized as a classical recursion problem , which further narrows down to
memoization ( top-down ) method of Dynamic Programming. Initially , we have length l
present with us , we'd have three size choices to cut from this , either we can make a cut of length
p , or q , or r. Let's say we made a cut of length p , so the remaining length would be l-p and
similarly with cuts q & r resulting in remaining lengths l-q & l-r respectively. We will call
recursive function for the remaining lengths and at any subsequent instance we'll have these three
choices. We will store the answer from all these recursive calls & take the maximum out of them
+1 as at any instance we'll have 1 cut from this particular call as well. Also , note that the
recursive call would be made if and only if the available length is greater than length we want to
cut i.e. suppose p=3 , and after certain recursive calls the available length is 2 only , so we can't
cut this line in lengths of p anymore.
int a,b,c;
if(p<=l)
a=func(l-p,p,q,r);
if(q<=l)
b=func(l-q,p,q,r);
if(r<=l)
c=func(l-r,p,q,r);
return 1+max({a,b,c});
One can clearly observe that at each call , the given length ( 4 initially ) is divided into 3
different subparts. Also , we can see that the recursion is being repeated for certain entries ( Red
arrow represents repetitive call for l=2, Yellow for l=3 and Blue for l=1). Therefore , we can
memoize the results in any container or array , so that repetition of same recursive calls is
avoided.
Let's now follow the code for implementation of the above code :
#include <bits/stdc++.h>
using namespace std;
if(ans<0)
return 0; // If returned answer is negative ,
that means cuts are not possible
return ans;
}
int main()
{
int l,p,q,r;
cout<<"ENTER THE LENGTH OF THE ROD "<<endl;
cin>>l;
cout<<"THE MAXIMUM NUMBER OF SEGMENTS THAT CAN BE CUT OF LENGTH p,q & r
FROM A ROD OF LENGTH l are "<<maximizeTheCuts(l,p,q,r)<<endl;
return 0;
}
Time Complexity : O(n) where n is the length of rod or line segment that has to be cut.
Space Complexity : O(n) where n is the length of rod or line segment that has to be cut.
Approach 2:
As the solution for a maximum number of cuts that can be made in a given length depends on the
maximum number of cuts previously made in shorter lengths, this question could be solved by
the approach of Dynamic Programming. Suppose we are given a length 'l'. For finding the
maximum number of cuts that can be made in length 'l', find the number of cuts made in shorter
previous length 'l-p', 'l-q', 'l-r' lengths respectively. The required answer would be the max(l-
p,l-q,l-r)+1 as one more cut should be needed after this to cut length 'l'. So for solving this
problem for a given length, find the maximum number of cuts that can be made in lengths
ranging from '1' to 'l'.
Example:
l = 11, p = 2, q = 3, r = 5
Analysing lengths from 1 to 11:
Algorithm:
Pseudo Code:
DP[l+1]={-1}
DP[0]=0
for(i from 0 to l)
if(DP[i]==-1)
continue
DP[i+p]=max(DP[i+p],DP[i]+1)
DP[i+q]=max(DP[i+q],DP[i]+1)
DP[i+r]=max(DP[i+r],DP[i]+1)
print(DP[l])
Implementation:
// if a segment of p is possible
if (i + p <= l)
dp[i + p] = max(dp[i + p], dp[i] + 1);
// if a segment of q is possible
if (i + q <= l)
dp[i + q] = max(dp[i + q], dp[i] + 1);
// if a segment of r is possible
if (i + r <= l)
dp[i + r] = max(dp[i + r], dp[i] + 1);
}
// if no segment can be cut then return 0
if (dp[l] == -1) {
dp[l] = 0;
}
// return value corresponding to length l
return dp[l];
}
// Driver Code
int main()
{
int l = 11, p = 2, q = 3, r = 5;
// Calling Function
int ans = findMaximum(l, p, q, r);
cout << ans;
return 0;
}
Output
5
Complexity Analysis:
Note: This problem can also be thought of as a minimum coin change problem because we are
given a certain length to acquire which is the same as the value of the amount whose minimum
change is needed. Now the x,y,z are the same as the denomination of the coin given. So length is
the same as the amount and x y z are the same as denominations, thus we need to change only
one condition that is instead of finding minimum we need to find the maximum and we will get
the answer. As the minimum coin change problem is the basic dynamic programming question
so this will help to solve this question also.
for(ll i=1;i<=n;i++)
{
for(ll j=1;j<=3;j++)
{
if(i>=a[j]&&m[i-a[j]]!=-1)
{
dp[i]=max(dp[i],1+dp[i-a[j]]);
}
}
}
Minimum coins to make a value
Given a value V, if we want to make a change for V cents, and we have an infinite supply of
each of C = { C1, C2, .., Cm} valued coins, what is the minimum number of coins to make the
change? If it's not possible to make a change, print -1.
Examples:
This problem is a variation of the problem discussed Coin Change Problem. Here instead of
finding the total number of possible solutions, we need to find the solution with the minimum
number of coins.
The minimum number of coins for a value V can be computed using the below recursive
formula.
// Initialize result
int res = INT_MAX;
Output
Minimum coins required is 2
The time complexity of the above solution is exponential and space complexity is way greater
than O(n). If we draw the complete recursion tree, we can observe that many subproblems are
solved again and again. For example, when we start from V = 11, we can reach 6 by subtracting
one 5 times and by subtracting 5 one time. So the subproblem for 6 is called twice.
Since the same subproblems are called again, this problem has the Overlapping Subproblems
property. So the min coins problem has both properties (see this and this) of a dynamic
programming problem. Like other typical Dynamic Programming(DP) problems, recomputations
of the same subproblems can be avoided by constructing a temporary array table[][] in a bottom-
up manner. Below is Dynamic Programming based solution.
if(table[V]==INT_MAX)
return -1;
return table[V];
}
Output
Minimum coins required is 2
Given an array arr[] where each element represents the max number of steps that can be made
forward from that index. The task is to find the minimum number of jumps to reach the end of
the array starting from index 0. If the end isn’t reachable, return -1.
Examples:
Minimum number of jumps to reach end using Dynamic Programming from left to right:
• Create jumps[] array from left to right such that jumps[i] indicate the minimum number of
jumps needed to reach arr[i] from arr[0].
• To fill the jumps array run a nested loop inner loop counter is j and the outer loop count is i.
o Outer loop from 1 to n-1 and inner loop from 0 to i.
o If i is less than j + arr[j] then set jumps[i] to minimum of jumps[i] and jumps[j] + 1.
initially set jump[i] to INT MAX
• Return jumps[n-1].
if (n == 0 || arr[0] == 0)
return INT_MAX;
jumps[0] = 0;
// Driver code
int main()
{
int arr[] = { 1, 3, 5, 8, 9, 2, 6, 7, 6, 8, 9 };
int size = sizeof(arr) / sizeof(int);
cout << "Minimum number of jumps to reach end is "
<< minJumps(arr, size);
return 0;
}
Output
// Handle overflow
if (min != INT_MAX)
jumps[i] = min + 1;
else
jumps[i] = min; // or INT_MAX
}
}
return jumps[0];
}
Output
Given weights and values of n items, put these items in a knapsack of capacity W to get the
maximum total value in the knapsack. In other words, given two integer arrays val[0..n-1] and
wt[0..n-1] which represent values and weights associated with n items respectively. Also given
an integer W which represents knapsack capacity, find out the maximum value subset of val[]
such that sum of the weights of this subset is smaller than or equal to W. You cannot break an
item, either pick the complete item or don't pick it (0-1 property).
Therefore, the maximum value that can be obtained from 'n' items is the max of the following
two values.
1. Maximum value obtained by n-1 items and W weight (excluding nth item).
2. Value of nth item plus maximum value obtained by n-1 items and W minus the weight of the nth
item (including nth item).
If the weight of 'nth' item is greater than 'W', then the nth item cannot be included and Case 1 is
the only possibility.
Below is the implementation of the above approach:
// Base Case
if (n == 0 || W == 0)
return 0;
// Driver code
int main()
{
int val[] = { 60, 100, 120 };
int wt[] = { 10, 20, 30 };
int W = 50;
int n = sizeof(val) / sizeof(val[0]);
cout << knapSack(W, wt, val, n);
return 0;
}
Output
220
It should be noted that the above function computes the same sub-problems again and again. See
the following recursion tree, K(1, 1) is being evaluated twice. The time complexity of this naive
recursive solution is exponential (2^n).
Complexity Analysis:
Since subproblems are evaluated again, this problem has Overlapping Sub-problems property. So
the 0-1 Knapsack problem has both properties (see this and this) of a dynamic programming
problem.
Approach: In the Dynamic programming we will work considering the same cases as mentioned
in the recursive approach. In a DP[][] table let's consider all the possible weights from '1' to 'W'
as the columns and weights that can be kept as the rows.
The state DP[i][j] will denote maximum value of 'j-weight' considering all values from '1 to ith'.
So if we consider 'wi' (weight in 'ith' row) we can fill it in all columns which have 'weight values
> wi'. Now two possibilities can take place:
0 1 2 3 4 5 6
0 0 0 0 0 0 0 0
1 0 10 10 10 10 10 10
2 0 10 15 25 25 25 25
3 0
Explanation:
For filling 'weight = 2' we come
across 'j = 3' in which
we take maximum of
(10, 15 + DP[1][3-2]) = 25
| |
'2' '2 filled'
not filled
0 1 2 3 4 5 6
0 0 0 0 0 0 0 0
1 0 10 10 10 10 10 10
2 0 10 15 25 25 25 25
3 0 10 15 40 50 55 65
Explanation:
For filling 'weight=3',
we come across 'j=4' in which
we take maximum of (25, 40 + DP[2][4-3])
= 50
// Driver Code
int main()
{
int val[] = { 60, 100, 120 };
int wt[] = { 10, 20, 30 };
int W = 50;
int n = sizeof(val) / sizeof(val[0]);
return 0;
}
Output
220
Complexity Analysis:
• Time Complexity: O(N*W).
where 'N' is the number of weight element and 'W' is capacity. As for every weight element we
traverse through all weight capacities 1<=w<=W.
• Auxiliary Space: O(N*W).
The use of 2-D array of size 'N*W'.
Scope for Improvement :- We used the same approach but with optimized space complexity
#include <bits/stdc++.h>
using namespace std;
// Driver Code
int main()
{
int val[] = { 60, 100, 120 };
int wt[] = { 10, 20, 30 };
int W = 50;
int n = sizeof(val) / sizeof(val[0]);
return 0;
}
Complexity Analysis:
• Time Complexity: O(N*W).
• Auxiliary Space: O(2*W)
As we are using a 2-D array but with only 2 rows.
Method 3: This method uses Memoization Technique (an extension of recursive approach).
This method is basically an extension to the recursive approach so that we can overcome the
problem of calculating redundant cases and thus increased complexity. We can solve this
problem by simply creating a 2-D array that can store a particular state (n, w) if we get it the first
time. Now if we come across the same state (n, w) again instead of calculating it in exponential
complexity we can directly return its result stored in the table in constant time. This method
gives an edge over the recursive approach in this aspect.
if (wt[i] > W) {
// Driver Code
int main()
{
int val[] = { 60, 100, 120 };
int wt[] = { 10, 20, 30 };
int W = 50;
int n = sizeof(val) / sizeof(val[0]);
cout << knapSack(W, wt, val, n);
return 0;
}
Output
220
Complexity Analysis:
•
Optimal Strategy for a Game
Consider a row of n coins of values v1 . . . vn, where n is even. We play a game against an
opponent by alternating turns. In each turn, a player selects either the first or last coin from the
row, removes it from the row permanently, and receives the value of the coin. Determine the
maximum possible amount of money we can definitely win if we move first.
Note: The opponent is as clever as the user.
Does choosing the best at each move gives an optimal solution? No.
In the second example, this is how the game can be finished:
1. .......User chooses 8.
.......Opponent chooses 15.
.......User chooses 7.
.......Opponent chooses 3.
Total value collected by user is 15(8 + 7)
2. .......User chooses 7.
.......Opponent chooses 8.
.......User chooses 15.
.......Opponent chooses 3.
Total value collected by user is 22(7 + 15)
So if the user follows the second game state, the maximum value can be collected although the
first move is not the best.
Approach: As both the players are equally strong, both will try to reduce the possibility of
winning of each other. Now let's see how the opponent can achieve this.
• The user chooses the 'ith' coin with value 'Vi': The opponent either chooses (i+1)th coin
or jth coin. The opponent intends to choose the coin which leaves the user with
minimum value.
i.e. The user can collect the value Vi + min(F(i+2, j), F(i+1, j-1) ) where [i+2,j] is the
range of array indices available to the user if the opponent chooses Vi+1 and [i+1,j-
1] is the range of array indexes available if opponent chooses the jth coin.
• The user chooses the 'jth' coin with value 'Vj': The opponent either chooses 'ith' coin or
'(j-1)th' coin. The opponent intends to choose the coin which leaves the user with
minimum value, i.e. the user can collect the value Vj + min(F(i+1, j-1), F(i, j-2) ) where
[i,j-2] is the range of array indices available for the user if the opponent picks jth
coin and [i+1,j-1] is the range of indices available to the user if the opponent picks
up the ith coin.
Following is the recursive solution that is based on the above two choices. We take a maximum
of two choices.
Base Cases
F(i, j) = Vi If j == i
F(i, j) = max(Vi, Vj) If j == i + 1
#include <bits/stdc++.h>
vector<int> arr;
map<vector<int>, int> memo;
int n = arr.size();
vector<int> k{ i, j };
if (memo[k] != 0)
return memo[k];
int optimalStrategyOfGame()
{
memo.clear();
return solve(0, n - 1);
}
// Driver code
int main()
{
arr.push_back(8);
arr.push_back(15);
arr.push_back(3);
arr.push_back(7);
n = arr.size();
cout << optimalStrategyOfGame() << endl;
arr.clear();
arr.push_back(2);
arr.push_back(2);
arr.push_back(2);
arr.push_back(2);
n = arr.size();
cout << optimalStrategyOfGame() << endl;
arr.clear();
arr.push_back(20);
arr.push_back(30);
arr.push_back(2);
arr.push_back(2);
arr.push_back(2);
arr.push_back(10);
n = arr.size();
cout << optimalStrategyOfGame() << endl;
}
int arr2[] = { 2, 2, 2, 2 };
n = sizeof(arr2) / sizeof(arr2[0]);
printf("%d\n", optimalStrategyOfGame(arr2, n));
return 0;
}
Output
22
4
42
Complexity Analysis:
•
Egg Dropping Puzzle
The following is a description of the instance of this famous puzzle involving n=2 eggs and a
building with k=36 floors.
Suppose that we wish to know which stories in a 36-storey building are safe to drop eggs from,
and which will cause the eggs to break on landing. We make a few assumptions:
.....An egg that survives a fall can be used again.
.....A broken egg must be discarded.
.....The effect of a fall is the same for all eggs.
.....If an egg breaks when dropped, then it would break if dropped from a higher floor.
.....If an egg survives a fall then it would survive a shorter fall.
.....It is not ruled out that the first-floor windows break eggs, nor is it ruled out that the 36th-floor
do not cause an egg to break.
If only one egg is available and we wish to be sure of obtaining the right result, the experiment
can be carried out in only one way. Drop the egg from the first-floor window; if it survives, drop
it from the second-floor window. Continue upward until it breaks. In the worst case, this method
may require 36 droppings. Suppose 2 eggs are available. What is the least number of egg-
droppings that is guaranteed to work in all cases?
The problem is not actually to find the critical floor, but merely to decide floors from which eggs
should be dropped so that the total number of trials are minimized.
Method 1: Recursion.
In this post, we will discuss a solution to a general problem with 'n' eggs and 'k' floors. The
solution is to try dropping an egg from every floor(from 1 to k) and recursively calculate the
minimum number of droppings needed in the worst case. The floor which gives the minimum
value in the worst case is going to be part of the solution.
In the following solutions, we return the minimum number of trials in the worst case; these
solutions can be easily modified to print floor numbers of every trial also.
Meaning of a worst-case scenario: Worst case scenario gives the user the surety of the threshold
floor. For example- If we have '1' egg and 'k' floors, we will start dropping the egg from the first
floor till the egg breaks suppose on the 'kth' floor so the number of tries to give us surety is 'k'.
1) Optimal Substructure:
When we drop an egg from a floor x, there can be two cases (1) The egg breaks (2) The egg
doesn't break.
1. If the egg breaks after dropping from 'xth' floor, then we only need to check for floors lower
than 'x' with remaining eggs as some floor should exist lower than 'x' in which egg would not
break; so the problem reduces to x-1 floors and n-1 eggs.
2. If the egg doesn't break after dropping from the 'xth' floor, then we only need to check for floors
higher than 'x'; so the problem reduces to 'k-x' floors and n eggs.
Since we need to minimize the number of trials in worst case, we take the maximum of two
cases. We consider the max of above two cases for every floor and choose the floor which yields
minimum number of trials.
#include <bits/stdc++.h>
using namespace std;
return min + 1;
}
Output
Minimum number of trials in worst case with 2 eggs and 10 floors is 4
Output:
It should be noted that the above function computes the same subproblems again and again. See
the following partial recursion tree, E(2, 2) is being evaluated twice. There will many repeated
subproblems when you draw the complete recursion tree even for small values of n and k.
E(2, 4)
|
-------------------------------------
| | | |
| | | |
x=1/ x=2/ x=3/ x=4/
/ / .... ....
/ /
E(1, 0) E(2, 3) E(1, 1) E(2, 2)
/ /... /
x=1/ .....
/
E(1, 0) E(2, 2)
/
......
Complexity Analysis:
Since same subproblems are called again, this problem has Overlapping Subproblems property.
So Egg Dropping Puzzle has both properties (see this and this) of a dynamic programming
problem. Like other typical Dynamic Programming(DP) problems, recomputations of same
subproblems can be avoided by constructing a temporary array eggFloor[][] in bottom up
manner.
Method 2: Dynamic Programming.
In this approach, we work on the same idea as described above neglecting the case of
calculating the answers to sub-problems again and again.. The approach will be to make a
table which will store the results of sub-problems so that to solve a sub-problem, it would only
require a look-up from the table which will take constant time, which earlier took exponential
time.
Formally for filling DP[i][j] state where 'i' is the number of eggs and 'j' is the number of floors:
• We have to traverse for each floor 'x' from '1' to 'j' and find minimum of:
(1 + max( DP[i-1][j-1], DP[i][j-x] )).
Output
Minimum number of trials in worst case with 2 eggs and 36 floors is 8
Complexity Analysis:
#include <bits/stdc++.h>
using namespace std;
#define MAX 1000
if (k == 1 || k == 0)
return k;
if (n == 1)
return k;
memo[n][k] = min+1;
return min + 1;
}
int main() {
int n = 2, k = 36;
cout<<solveEggDrop(n, k);
return 0;
}
// contributed by Shivam Agrawal(shivamagrawal3)
Output
8
Count BSTs with n keys
Given N, Find the total number of unique BSTs that can be made using values from 1 to N.
Examples:
Input: n = 3
Output: 5
For n = 3, preorder traversal of Unique BSTs are:
1. 1 2 3
2. 1 3 2
3. 2 1 3
4. 3 1 2
5. 3 2 1
Input: 4
Output: 14
In this post we will discuss a solution based on Dynamic Programming. For all possible values of
i, consider i as root, then [1....i-1] numbers will fall in the left subtree and [i+1....n] numbers will
fall in the right subtree.
Now, let's say count(n) denotes number of structurally different BST that can be made using
numbers from 1 to n. Then count(n) can be calculated as:-
count(n)=summation of (count(i-1)*count(n-i)).
// Base case
dp[0] = 1;
dp[1] = 1;
return dp[n];
}
// Driver Code
int main()
{
int n = 3;
cout << "Number of structurally Unique BST with " <<
n << " keys are : " << numberOfBST(n) << "\n";
return 0;
}
// This code is contributed by Aditya kumar (adityakumar129)
Output:
Number of structurally Unique BST with 3 keys are : 5
Given an array arr[] of positive numbers, The task is to find the maximum sum of a subsequence
such that no 2 numbers in the sequence should be adjacent in the array.
Examples:
Each element has two choices: either it can be the part of the subsequence with the highest sum
or it cannot be part of the subsequence. So to solve the problem, build all the subsequences of the
array and find the subsequence with the maximum sum such that no two adjacent elements are
present in the subsequence.
Maximum sum such that no two elements are adjacent using Dynamic Programming:
• As seen above, each element has two choices. If one element is picked then its neighbours
cannot be picked. Otherwise, its neighbours may be picked or may not be.
• So the maximum sum till ith index has two possibilities: the subsequence includes arr[i] or it does
not include arr[i].
• If arr[i] is included then the maximum sum depends on the maximum subsequence sum till (i-
1)th element excluding arr[i-1].
• Otherwise, the maximum sum is the same as the maximum subsequence sum till (i-1) where
arr[i-1] may be included or excluded.
So build a 2D dp[N][2] array where dp[i][0] stores maximum subsequence sum till ith index with
arr[i] excluded and dp[i][1] stores the sum when arr[i] is included.
The values will be obtained by the following relations: dp[i][1] = dp[i-1][0] + arr[i] and dp[i][0]
= max(dp[i-1][0], dp[i-1][1])
#include <bits/stdc++.h>
using namespace std;
// Driver Code
int main()
{
// Creating the array
vector<int> arr = { 5, 5, 10, 100, 10, 5 };
int N = arr.size();
// Function call
cout << findMaxSum(arr, N) << endl;
return 0;
}
Output:
110
Space Optimized Approach: The above approach can be optimized to be done in constant
space based on the following observation:
As seen from the previous dynamic programming approach, the value of current states (for ith
element) depends upon only two states of the previous element. So instead of creating a 2D
array, we can use only two variables to store the two states of the previous element.
• Say excl stores the value of the maximum subsequence sum till i-1 when arr[i-1] is excluded and
• incl stores the value of the maximum subsequence sum till i-1 when arr[i-1] is included.
• The value of excl for the current state( say excl_new) will be max(excl ,incl). And the value of incl
will be updated to excl + arr[i].
Illustration:
For i = 1: arr[i] = 5
=> excl_new = 5
=> incl = (excl + arr[i]) = 5
=> excl = excl_new = 5
For i = 2: arr[i] = 10
=> excl_new = max(excl, incl) = 5
=> incl = (excl + arr[i]) = 15
=> excl = excl_new = 5
For i = 4: arr[i] = 10
=> excl_new = max(excl, incl) = 105
=> incl = (excl + arr[i]) = 25
=> excl = excl_new = 105
For i = 5: arr[i] = 5
=> excl_new = max(excl, incl) = 105
=> incl = (excl + arr[i]) = 110
=> excl = excl_new = 105
#include <bits/stdc++.h>
using namespace std;
// Driver code
int main()
{
vector<int> arr = { 5, 5, 10, 100, 10, 5 };
int N = arr.size();
// Function call
cout << FindMaxSum(arr, N);
return 0;
}
// This approach is contributed by Debanjan
Output
110
Given a set of non-negative integers, and a value sum, determine if there is a subset of the given
set with sum equal to given sum.
Example:
Method 1: Recursion.
Approach: For the recursive approach we will consider two cases.
1. Consider the last element and now the required sum = target sum - value of 'last' element and
number of elements = total elements - 1
2. Leave the 'last' element and now the required sum = target sum and number of elements =
total elements - 1
isSubsetSum(set, n, sum)
= isSubsetSum(set, n-1, sum) ||
isSubsetSum(set, n-1, sum-set[n-1])
Base Cases:
isSubsetSum(set, n, sum) = false, if sum > 0 and n == 0
isSubsetSum(set, n, sum) = true, if sum == 0
set[]={3, 4, 5, 2}
sum=9
(x, y)= 'x' is the left number of elements,
'y' is the required sum
(4, 9)
{True}
/ \
(3, 6) (3, 9)
/ \ / \
(2, 2) (2, 6) (2, 5) (2, 9)
{True}
/ \
(1, -3) (1, 2)
{False} {True}
/ \
(0, 0) (0, 2)
{True} {False}
// Base Cases
if (sum == 0)
return true;
if (n == 0)
return false;
// Driver code
int main()
{
int set[] = { 3, 34, 4, 12, 5, 2 };
int sum = 9;
int n = sizeof(set) / sizeof(set[0]);
if (isSubsetSum(set, n, sum) == true)
cout <<"Found a subset with given sum";
else
cout <<"No subset with given sum";
return 0;
}
Output
Found a subset with given sum
Complexity Analysis: The above solution may try all subsets of given set in worst case.
Therefore time complexity of the above solution is exponential. The problem is in-fact NP-
Complete (There is no known polynomial time solution for this problem).
Method 2: To solve the problem in Pseudo-polynomial time use the Dynamic programming.
So we will create a 2D array of size (arr.size() + 1) * (target + 1) of type boolean. The state
DP[i][j] will be true if there exists a subset of elements from A[0....i] with sum value = 'j'. The
approach for the problem is:
if (A[i-1] > j)
DP[i][j] = DP[i-1][j]
else
DP[i][j] = DP[i-1][j] OR DP[i-1][j-A[i-1]]
1. This means that if current element has value greater than 'current sum value' we will copy the
answer for previous cases
2. And if the current sum value is greater than the 'ith' element we will see if any of previous states
have already experienced the sum='j' OR any previous states experienced a value 'j - A[i]'
which will solve our purpose.
0 1 2 3 4 5 6
0 T F F F F F F
3 T F F T F F F
4 T F F T T F F
5 T F F T T T F
2 T F T T T T T
return subset[n][sum];
}
// Driver code
int main()
{
int set[] = { 3, 34, 4, 12, 5, 2 };
int sum = 9;
int n = sizeof(set) / sizeof(set[0]);
if (isSubsetSum(set, n, sum) == true)
printf("Found a subset with given sum");
else
printf("No subset with given sum");
return 0;
}
// This code is contributed by Arjun Tyagi.
Output
Found a subset with given sum
Complexity Analysis:
Time Complexity: O(sum*n), where sum is the 'target sum' and 'n' is the size of array.
Auxiliary Space: O(sum*n), as the size of 2-D array is sum*n. + O(n) for recursive stack space
Method:
1. In this method, we also follow the recursive approach but In this method, we use another 2-D
matrix in we first initialize with -1 or any negative value.
2. In this method, we avoid the few of the recursive call which is repeated itself that's why we use
2-D matrix. In this matrix we store the value of the previous call value.
if (n <= 0)
return 0;
// Driver Code
int main()
{
// Storing the value -1 to the matrix
memset(tab, -1, sizeof(tab));
int n = 5;
int a[] = {1, 5, 3, 7, 4};
int sum = 12;
if (subsetSum(a, n, sum))
{
cout << "YES" << endl;
}
else
cout << "NO" << endl;
Complexity Analysis:
• Time Complexity: O(sum*n), where sum is the 'target sum' and 'n' is the size of array.
• Auxiliary Space: O(sum*n) + O(n) -> O(sum*n) = the size of 2-D array is sum*n and
O(n)=auxiliary stack space.
Matrix Chain Multiplication
Given the dimension of a sequence of matrices in an array arr[], where the dimension of the ith
matrix is (arr[i-1] * arr[i]), the task is to find the most efficient way to multiply these matrices
together such that the total number of element multiplications is minimum.
Examples:
We can solve the problem using recursion based on the following facts and observations:
Two matrices of size m*n and n*p when multiplied, they generate a matrix of size m*p and the
number of multiplications performed are m*n*p.
Now, for a given chain of N matrices, the first partition can be done in N-1 ways. For example,
sequence of matrices A, B, C and D can be grouped as (A)(BCD), (AB)(CD) or (ABC)(D) in
these 3 ways.
So a range [i, j] can be broken into two groups like {[i, i+1], [i+1, j]}, {[i, i+2], [i+2, j]}, . . . , {[i,
j-1], [j-1, j]}.
• Each of the groups can be further partitioned into smaller groups and we can find the total
required multiplications by solving for each of the groups.
• The minimum number of multiplications among all the first partitions is the required answer.
• Create a recursive function that takes i and j as parameters that determines the range of a
group.
o Iterate from k = i to j to partition the given range into two groups.
o Call the recursive function for these groups.
o Return the minimum value among all the partitions as the required minimum number of
multiplications to multiply all the matrices of this group.
• The minimum value returned for the range 0 to N-1 is the required answer.
#include <bits/stdc++.h>
using namespace std;
// Driver Code
int main()
{
int arr[] = { 1, 2, 3, 4, 3 };
int N = sizeof(arr) / sizeof(arr[0]);
// Function call
cout << "Minimum number of multiplications is "
<< MatrixChainOrder(arr, 1, N - 1);
return 0;
}
Output
Minimum number of multiplications is 30
Below is the recursion tree for the 2nd example of the above recursive approach:
1) Optimal Substructure: In the above case, we are breaking the bigger groups into smaller
subgroups and solving them to finally find the minimum number of multiplications. Therefore, it
can be said that the problem has optimal substructure property.
2) Overlapping Subproblems: We can see in the recursion tree that the same subproblems are
called again and again and this problem has the Overlapping Subproblems property.
// Driver Code
int main()
{
int arr[] = { 1, 2, 3, 4 };
int n = sizeof(arr) / sizeof(arr[0]);
memset(dp, -1, sizeof dp);
Output
Minimum number of multiplications is 18
Dynamic Programming Solution for Matrix Chain Multiplication using Tabulation (Iterative
Approach):
In iterative approach, we initially need to find the number of multiplications required to multiply
two adjacent matrices. We can use these values to find the minimum multiplication required for
matrices in a range of length 3 and further use those values for ranges with higher lengths.
Build on the answer in this manner till the range becomes [0, N-1].
int i, j, k, L, q;
// L is chain length.
for (L = 2; L < n; L++)
{
for (i = 1; i < n - L + 1; i++)
{
j = i + L - 1;
m[i][j] = INT_MAX;
for (k = i; k <= j - 1; k++)
{
// q = cost/scalar multiplications
q = m[i][k] + m[k + 1][j]
+ p[i - 1] * p[k] * p[j];
if (q < m[i][j])
m[i][j] = q;
}
}
}
// Driver Code
int main()
{
int arr[] = { 1, 2, 3, 4 };
int size = sizeof(arr) / sizeof(arr[0]);
getchar();
return 0;
}
Given a string, a partitioning of the string is a palindrome partitioning if every substring of the
partition is a palindrome. For example, "aba|b|bbabb|a|b|aba" is a palindrome partitioning of
"ababbbabbababa". Determine the fewest cuts needed for a palindrome partitioning of a given
string. For example, minimum of 3 cuts are needed for "ababbbabbababa". The three cuts are
"a|babbbab|b|ababa". If a string is a palindrome, then minimum 0 cuts are needed. If a string of
length n containing all different characters, then minimum n-1 cuts are needed.
Examples :
This problem is a variation of Matrix Chain Multiplication problem. If the string is a palindrome,
then we simply return 0. Else, like the Matrix Chain Multiplication problem, we try making cuts
at all possible places, recursively calculate the cost for each cut and return the minimum value.
Let the given string be str and minPalPartion() be the function that returns the fewest cuts needed
for palindrome partitioning. following is the optimal substructure property.
Using Recursion
Output:
// Driver code
int main()
{
string str = "ababbbabbababa";
cout << "Min cuts needed for Palindrome"
" Partitioning is "
<< minPalPartion(str);
return 0;
}
Output:
We can optimize the above code a bit further. Instead of calculating C[i] separately in O(n^2),
we can do it with the P[i] itself. Below is the highly optimized code of this problem:
#include <bits/stdc++.h>
using namespace std;
int minCut(string a)
{
int cut[a.length()];
bool palindrome[a.length()][a.length()];
memset(palindrome, false, sizeof(palindrome));
for (int i = 0; i < a.length(); i++)
{
int minCut = i;
for (int j = 0; j <= i; j++)
{
if (a[i] == a[j] && (i - j < 2 || palindrome[j + 1][i - 1]))
{
palindrome[j][i] = true;
minCut = min(minCut, j == 0 ? 0 : (cut[j - 1] + 1));
}
}
cut[i] = minCut;
}
return cut[a.length() - 1];
}
// Driver code
int main()
{
cout << minCut("aab") << endl;
cout << minCut("aabababaxx") << endl;
return 0;
}
// Return the min cut value for complete string. i.e., str[0..n-1]
return C[n - 1];
}
Output:
Min cuts needed for Palindrome Partitioning is 3
memo[ij] = minimum;
// Return the min cut value for complete string.
return memo[ij];
}
int main()
{
string input = "ababbbabbababa";
unordered_map<string, int> memo;
cout << minpalparti_memo(input, 0, input.length() - 1, memo) << endl;
return 0;
}
Given a number of pages in N different books and M students. The books are arranged in
ascending order of the number of pages. Every student is assigned to read some consecutive
books. The task is to assign books in such a way that the maximum number of pages assigned to
a student is minimum.
Example :
Naive Approach:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int arr[]={10,20,10,30};
int n=sizeof(arr)/sizeof(arr[0]);
int k=2;
cout<<minPages(arr,n,k);
}
Output:
40
Approach: A Binary Search method for solving the book allocation problem:
If the number of students is greater than the number of books (i.e, M > N), In this case at least 1
student will be left to which no book has been assigned.
The maximum possible answer could be when there is only one student. So, all the book will be
assigned to him and the result would be the sum of pages of all the books.
The minimum possible answer could be when number of student is equal to the number of book
(i.e, M == N) , In this case all the students will get at most one book. So, the result would be the
maximum number of pages among them (i.e, minimum(pages[])).
Hence, we can apply binary search in this given range and each time we can consider the mid
value as the maximum limit of pages one can get. And check for the limit if answer is valid then
update the limit accordingly.
// update curr_sum
curr_sum = arr[i];
else
// if not possible means pages should be
// increased so update start = mid + 1
start = mid + 1;
}
// Drivers code
int main()
{
// Number of pages in books
int arr[] = { 12, 34, 67, 90 };
int n = sizeof arr / sizeof arr[0];
int m = 2; // No. of students
Output
Time Complexity: O(N*log(N)), Where N is the total number of pages in the book.
Auxiliary Space: O(1)