This blog is about dynamic programming, and more specifically, the steps required to convert a recursive algorithm into a dynamic programming algorithm. We show how to carry out this type of conversion for the specific example of computing Fibonacci numbers. We also discuss how we can do something similar for other recursive algorithms to which dynamic programming is applicable.
Before we start our discussion on dynamic programming and the steps required to convert a recursive algorithm into a dynamic programming algorithm, let’s recall the elements of a recursive algorithm.
A recursive algorithm (or a recursive function) is an algorithm that calls itself. In other words, the same definition needs to be applied again, recursively. This is usually referred to as the recursive part.
Because we do not want to continue with this self-reference indefinitely, some stopping conditions need to be defined. These conditions are called base cases.
We use a simple example of computing Fibonacci numbers to exemplify the process through which we can convert a recursive algorithm into a dynamic programming algorithm (recursion with memoization).
Memoization is a technique where we save the result of function calls in an array.
Fibonacci numbers are a sequence of numbers in which the first two terms are
If we represent the
The formula given above is the recursive part of the mathematical definition of Fibonacci numbers.
The first two numbers are fixed and are defined to be equal to
This completes the recursive definition of the Fibonacci numbers. Let’s look at a recursive code snippet that computes the
n = 5def Fib(n):if(n==1 or n==2):return 1else:return Fib(n-1) + Fib(n-2)print(Fib(n))
In line 3, we define the function for the recursive function Fib
. Line 4 checks for the base cases and returns Fib(n-1) + Fib(n-2)
. Finally, the
The recursive calls can be visualized through the recursion tree. In any recursion tree, every function call is represented by a node. If the function Fib(x)
calls the function Fib(y)
recursively, then we add an edge from the node corresponding to Fib(x)
to the node corresponding to Fib(y)
.
The argument passed to the recursive function is written inside its corresponding node.
Let’s look at the recursion tree of Fib(5)
below:
The function starts with the function argument
The two edges of the root node lead to the nodes labeled with the arguments
In this recursion tree, we can see that there are two nodes with an argument of Fib(3)
. This means that Fib(3)
is computed twice.
For a larger starting value, there will be more function calls with the same argument that will be computed multiple times.
Also, the function Fib(3)
will be computed even more times. This is because the recursion tree doesn’t keep track of the number of times the recursive calls with various arguments are computed, or whether a function call with a particular argument has already been computed or not—therefore the need to recompute the function with the same arguments several times.
Notice that the nodes in the recursion tree have three different colors. These are used to differentiate between different types of nodes.
We are using the color green for the root node. There will be just one node of this type and it will be at the top of the recursion tree.
For the nodes corresponding to the base case, we are using the color pink. Because these nodes correspond to the base cases, they do not have any outgoing edge.
The nodes with the color blue are the intermediate nodes. They are called recursively and they call the function recursively with a different parameter.
Because we are needlessly recomputing some values multiple times, the algorithm will take more steps to complete its calculations, making it slow and inefficient. The obvious way to solve this issue of recomputation is to keep track of all the possible inputs to the recursive algorithm, and whether the result of the function calls with these arguments have been calculated or not. If they have been calculated, then their values should be saved.
Now, with this additional information, we don’t need to call the recursive algorithm blindly. All we need to do is to check whether a particular value has already been calculated or not. We can easily perform this check with the help of an array of boolean variables—one index for every argument to the function calls. For example, if we want to check whether the fifth Fibonacci number has been calculated or not, we’ll look at the fifth index of this boolean array. A true value at the fifth index will indicate that this value has already been calculated, and a false value will indicate otherwise.
Please note that we’ll need two arrays—one for tracking what’s already computed. The other array will store the calculated results.
Let’s list these changes down before we convert this strategy into code.
If the value is already calculated, then return the value.
If the value is not calculated, then do the following:
Calculate the value recursively.
Save the value.
Update the information that this value has been saved.
Return the value.
Additionally, we need to initialize these arrays with zeros.
We need the following three things in order to implement the above-mentioned strategy:
We need a boolean array to save the information of whether a particular value is already computed or not. Let’s use 1
to represent the already computed value, and 0
otherwise. For the function with parameter
We need an array to save the value for each variable. The result of the function call with a parameter
We need a helper function (helper_function
) that actually implements the strategy.
Over here, helper_function
is a function that makes the necessary checks and returns the calculated values. Using this function will give us two advantages.
For any recursive algorithm, its logic is kept separate and the logic of memoization is handled separately in helper_function
.
In order to implement this scheme, we just need to replace the recursive calls with helper_function
with the same parameters.
It is interesting to note that we are using memory on top of the recursive algorithm. This is why dynamic programming is sometimes called recursion with memoization.
Let’s look at how memoization works with the help of an example.
Suppose we want to calculate Fib(5)
.
We’ll create a boolean array is_calculated
of size
We’ll also create an integer array—value
—of size 5, as shown below. We only need to initialize it with the known values of the base cases.
Let’s define helper_function
as described above.
def helper_function(n):if (is_calculated[n]==1):return value[n]else:value[n] = Fib(n) #calculates the value of the n-th Fibonacci number and#saves it at the n-th locationis_calculated[n] = 1 #updates the n-th entry as calculatedreturn value[n] #returns the calculated value
In line 1, def helper_function(n):
defines the helper function.
In line 2, if (is_calculated[n]==1)
checks whether the function with parameter n
(here, it is the value[n]
, in line 3 is returned. Otherwise, in line 5, Fib(n)
calculates the correct value and value[n] = Fib(n)
saves it at its correct location. In line 7, is_calculated[n] = 1
updates the information that the return value[n]
returns the
Now, let’s update the recursive algorithm of Fibonacci.
def Fib(n):if(n==1 or n==2):return 1else:return helper_function(n-1) + helper_function(n-2)
To reiterate, when we are using helper_function
, the only change in the recursive code that we need is to replace the recursive calls with the function calls to helper_function
, with the same parameters.
Let’s look at the complete code, with both the functions Fib
and helper_function
.
In line 1, the value of n
is selected. Lines 3–8 are used for initializing the array. Here, we should note that we are initializing three values in lines 3–4 for the base cases, whereas the original function contains only two base cases. Because we store the first Fibonacci number at index None
. This value is never used.
The helper_function
is defined in lines 10–17, and Fib
is defined in lines 20–24. Finally, in line 26, Fib(n)
calculates the
n = 5is_calculated = [None, 1, 1]value = [None, 1, 1]for i in range(3, n+1):is_calculated.append(0)value.append(0)def helper_function(n):if (is_calculated[n]==1):return value[n]else:value[n] = Fib(n) #calculates the value of the n-th Fibonacci number and#saves it at the n-th locationis_calculated[n] = 1 #updates the n-th entry as calculatedreturn value[n] #returns the calculated valuedef Fib(n):if(n==1 or n==2):return 1else:return helper_function(n-1) + helper_function(n-2)print Fib(n)
In general, to memoize any other recursive function, say xyz
, all recursive calls to xyz
should be replaced by helper_function
. Of course, if the function xyz
contains a higher number of parameters, then helper_function
will have the same parameters and the arrays is_calculated
and value
will have the same dimensions as the number of these parameters.
It will be interesting to have a look at the recursion tree with the dynamic programming approach. It is shown below:
Note that there are two nodes with the same number Fib(3)
, which is called first. After its value is calculated and saved, we do not have to recalculate the value of the third Fibonacci number. This is why, when the function Fib(3)
—represented by the node with the value
Please note that the recursive calls in the recursion tree above are not direct. For example, the function Fib(5)
calls helper_function(4)
, which calls the function Fib(4)
. This means that the function Fib(5)
is calling Fib(4)
but not directly. This still remains a recursive call.
For a large input value, the number of nodes and the number of recursive calls in both trees will be substantially different. Try drawing the recursion tree for both cases with
Converting a recursive algorithm into a dynamic programming algorithm (recursion with memoization) is a simple and straightforward process with a defined number of steps.
You can replace the value of n
in the recursive code, as well as the dynamic programming code, to a large number—let’s say
Keep in mind that if the program takes too long to terminate, the system will generate a time-out message.
For more details regarding algorithms and dynamic programming, take a look at the following courses:
Free Resources