An Introduction to Bottom-Up Dynamic Programming
By: Jordan Moore
Tags: Algorithms, Coding, Programming, JavaScript, Software Development

An Introduction to Bottom-Up Dynamic Programming

Not confident with recursion? No worries —here’s the how-to guide for solving DP problems with a bottom-up approach.

Dynamic Programming (DP) is a funny term. It was named as such to avoid enraging a former Secretary of Defense, and the name has stuck ever since.

Essentially, there’s nothing dynamic about it. DP can simply be categorized as an algorithmic strategy where one breaks down a single problem into sub-problems. By solving each sub-problem, the algorithm gets closer and closer to solving the original problem. After solving all of the sub-problems, the answer to the original problem is found.

Experienced readers will most likely think that this definition is very close to the idea of recursion — it definitely is, and most problems that employ a DP solution can be solved with recursion.

For the purposes of this article, however, we will be taking a look at bottom-up dynamic programming (BUDP), which does not necessarily make use of recursion. After thoroughly examining the topic at hand, we will solve the problem Minimum Path Sum using BUDP, and do so with several illustrative diagrams.

A Quick Look at the Problem

Before diving into BUDP, let’s break down the Minimum Path Sum problem. We’ll do this now, as the explanation of what BUDP is and how it works will hinge on using this problem as an example.

The question description is as follows:

Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right, which minimizes the sum of all numbers along its path. Note: You can only move either down or right at any point in time.

So, we have a grid, which is a 2-dimensional array like [[1,3,1], [1,5,1], [4,2,1]] . It will look something like this, depending on the size of m && n :

In this picture, the blue squares represent the squares that are in the minimum path. This brings us to the next pertinent question: what is the minimum path?

The minimum path in this question is the smallest accumulated sum from the top left (first index) to the bottom right (last index). As we move across the grid, we need to connect squares into paths by adding a previous element (a number) to the current square.

This leads us to the next question: how do we move across the grid? According to the question’s description, we can only move down or right. So, the types of paths we can make is limited to two directions: down or right.

As one can see, the minimum path in this example is from 1 -> 3 -> 1 -> 1 -> 1 , which adds up to 7 . All of the other possible paths are greater than 7 (go ahead, try counting them out).

Now, we get to the interesting question: how do we solve this problem? Let’s find out with BUDP.

What is BUDP?

It’s important to first explain what problems are solved using DP.

Luckily, it’s fairly easy to spot DP problems. DP problems are ones that require finding a reduced or accumulative answer.

In our current example, we are looking for a reduced answer. We are looking for the smallest possible path, so we need to find the path to the last indices that has the smallest sum.

The reason why reduction/accumulation problems are perfect for DP is that DP employs the strategy of solving sub-problems to solve the overall problem. In these types of problems, we need to iterate over the entire input (oftentimes, it’s a matrix) to find the answer, since the entire input must be taken into account when finding the answer.

For instance, in our current example, we will need to find the smallest possible path to the end of the matrix. This means that we will need to check every element and solve a sub-problem for it. However, we need to start somewhere.

This is where we get to actually defining the bottom-up style of dynamic programming. With the bottom-up strategy, we start at the beginning and solve sub-problems until we reach the end. Once we have reached the end and calculated all of the sub-problems, the answer to the overall problem should be apparent. This is because the accumulated sub-problems are used to calculate the final answer to the overall problem.

Now, let’s apply this style of thinking to the example problem: what is the sub-problem in this problem?

The sub-problem mirrors the overall problem: the difference is that instead of finding the minimum path to the final indices, we need to find the minimum path leading to any given set of indices in the matrix. For this strategy, this means we will need to iterate through the entire matrix. By treating each element in the matrix as a tiny sub-problem of the overall problem, we can use the accumulated solutions to compute the overall solution.

NOTE: It’s okay if this doesn’t make sense — the next section includes diagrams that solidify the concept of sub-problem usage.

At this point, we have enough of an understanding of BUDP to actually practice it with the Minimum Path Sum problem.

Applying BUDP to Minimum Path Sum

For the remainder of this section, we will be using the following diagram to illustrate our use of BUDP:

This matrix represents the input array of [[1,3,1], [1,5,1], [4,2,1]] .

First, we will solve this problem with only the diagrams. Then, we will write out the code to implement our solution.

We’ve established that with BUDP, we start at the beginning of the input and work our way towards the end, which are its final indices. We’ve also established that we will need to iterate through the entire input array, since we will need to take all numbers into account in order to confidently assert that we have reached the minimum path sum at the end. So, let’s highlight the first element, 1 .

Now, let’s solve the sub-problem at this index, which is: what is the minimum path sum at this index? Since we’re at the first index, our path necessarily starts here. So, we leave it as 1 , as that is the only possible path sum at this index. Iterate to the second element 3 .

What’s the minimum path sum at this index? We have to think about how a path can reach this index. What are the two ways that we can construct a path across this matrix? That’s right — only down and right. For example, we cannot move from the 5 up to the 3 , nor can we move from the 2 back to the 4 before it.

This means that a path can only connect from the left or above the current element. We know this because we can only move either right or down in the path. So, we can check the minimum path sum at any given index by checking the following: the element to the left of the current element, the element directly above (row above) the current element, and then picking the one from them that is smaller. We will add the smaller of the left or top element to the current element to convert it to the minimum path sum for the current element.

In the top row, we can only check the left element, so that is necessarily the number to add to the current element. So, we add 1 to 3 and then continue iterating to 1 , the final element in the first row.

Since we’re still in the top row, the answer to this sub-problem is the same as the previous: we add the element to the left of the current element to our current element. This means that 4 , the previous minimum path sum, is added to 1 . Next, move down to the next row and iterate through those elements.

NOTE: that now we’ve begun highlighting the potential elements (yellow) to add to the current element (blue).

Now we’re onto the second row, which means that we do have a top element to check for the minimum path sum. However, at this first element, 1 , we do not have a left element, since we are at the first index of the row. So, we add 1 (from the top element) to our current element 1 . Move on to 5 .

5 is the first element that we actually need to check both the left and top element. Remember, since we are calculating the minimum path sum for every element as we iterate through the matrix, the element at any traversed index no longer represents the original element; rather, the element represents the minimum path sum. Using these new elements, we can calculate the minimum path sum for every element as we traverse through the matrix, all the way until the final indices.

Check the left and top minimum paths, and then add the smaller of the two to the current element, since we want the minimum path, not the maximum. We add 2 (from the left) to 5 and continue iterating on to 1 .

We are at 1 . Calculate the minimum path sum by finding the minimum of the left and top elements, 7 && 5 respectively. We add 5 to 1 , since that is the smaller of the two. Iterate to the next and final row of the matrix.

The element 4 has only one potential path from the top. Add 2 (the top element) to the current element, 4 , and continue iterating to 2 .

There are two options for paths here, both 6 and 7 . The smaller of the two is 6 , so add it to 2 and iterate to the final element, 1 .

The choice here is obvious: 6 is the minimum path, so add it to 1 , thereby connecting and creating our final minimum path sum for the overall problem, 7 . The final diagram looks like this:

Didn’t that seem like magic? By using the sub-problems calculated at every element, we were able to create the minimum path sum for the entire matrix. Now, let’s translate these drawings into code.

Translating to Code

We’ll need two loops, one nested inside of the other. The outer loop will iterate through the matrix rows, and the inner loop will iterate through the elements in the current row. Feel free to do this with either pointers or built-in iterative methods; however, it will arguably be easier with the iterative method like forEach , since it can pass the index of the current element easily.

Remember, we’ll need the indices of the current row and element to check the current element’s left and top neighbors.

Now that we have the iteration done, let’s move on to the conditional logic within the nested loop. Here, we will be checking the following branches:

Is this the first element of the first row? Is this element in the first row? Is this the first element in any row other than the first?

If any of these conditions are met we don’t need to check either the left or the top neighbor, depending on which condition is met. If none of these conditions are met, we must check both left and top neighbors to determine the minimum path sum at the current element. Let’s implement the conditional logic:

Now, let’s actually create the logic that will re-assign the current element to the minimum path sum.

Remember, in the else case we are checking both the left and top elements.

Don’t let all of the indices throw you; we’re simply checking either the element before the current element (to the left) or the element in the row before the current element’s row (to the top). Math.min is used to calculate the minimum of its arguments and works perfectly in this case.

Finally, we need to return the element at the last index of the final row in the matrix.

Great! We’re all finished. This code runs in O(m * n) time and takes up O(1) space, since we mutated the input array and didn’t make a copy of it.

Conclusion

I hope that Dynamic Programming and its Bottom-Up strategy is a little less daunting for you now. It’ll take much more practice than this article can provide, so I suggest applying the same strategies as used in this article to the following problems:

Good luck and happy coding!