In this article, I will introduce the concept of dynamic programming, developed by Richard Bellman in the 1950s, a powerful algorithm design technique to solve problems by breaking them down into smaller problems, storing their solutions, and combining these to get to the solution of the original problem.
The hardest problems asked in FAANG coding interviews usually fall under this category. It is likely that you will get tasked with solving one during your interviews, hence the importance of knowing this technique. I will explain what dynamic programming is, give you a recipe to tackle dynamic programming problems, and will take you through a few examples so that you can understand better when and how to apply it.
As I already did in my previous post about coding interviews, I will share my thought process when solving problems that can be solved using this methodology, so that you can do the same when you face one of them. I don’t want you to memorize anything. You need to understand the technique and practice to acquire the skill of turning ideas into code. Coding is not about learning programming languages. It is about analyzing a problem, considering different solutions, choosing the best one, and then implementing it in some programming language.
Dynamic programming is a general technique for solving optimization, search and counting problems that can be decomposed into subproblems. To apply dynamic programming, the problem must present the following two attributes:
A problem has optimal substructure if the optimal solution to a problem of size n can be derived from the optimal solution of the same instance of that problem of size smaller than n.
For example, if the shortest path to go from Paris to Moscow goes through Berlin, it will be made of the shortest path from Paris to Berlin and the shortest path from Berlin to Moscow.
If a problem can be solved by combining optimal solutions to non-overlapping subproblems, the strategy is called divide and conquer. This is why merge sort and quick sort are not classified as dynamic programming problems.
Let’s take an example you’re probably familiar with, the Fibonacci numbers, where every number is the sum of the previous two Fibonacci numbers. The Fibonacci series can be expressed as:
F(0) = F(1) = 1
F(n) = F(n-1) + F(n-2)
They say a picture is worth a thousand words, so here it is (from Elements of programming interviews):
To solve F(n), you need to solve F(n-1) and F(n-2), but F(n-1) needs F(n-2) and F(n-3). F(n-2) is repeated, coming from two different instances of the same problem – computing a Fibonacci number.
This can be expressed in a recursive function:
This leads us to the relationship between recursion and dynamic programming.
Conceptually dynamic programming involves recursion. You want to solve your problem based on smaller instances of the same problem, and recursion is a natural way of expressing this in code. The difference with a pure recursive function is that we will trade space for time: we will store the optimal solution to the subproblems to be able to efficiently find the optimal solution to the problem that we originally wanted to solve.
This is not to say that you must use recursion to solve dynamic programming problems. There is also an iterative way of coding a dynamic programming solution.
You need to fill a table with the solution to all the subproblems (starting from the base cases) and use it to build the solution you are looking for. This is done in an iterative fashion, using one of the following:
as your data structure to store the solutions to the subproblems.
Code the recursive algorithm and add a cache layer to avoid repeating function calls.
This will all be much clearer when we start with the examples.
Optimal substructure and overlapping subproblems are the two attributes a problem must have to be solved used dynamic programming. You will need to verify this when your intuition tells you dynamic programming might be a viable solution.
Let’s try to get a feel for what kind of problems can be solved using dynamic programming. Things that start with “find”:
Are potential candidates.
Unfortunately, there is no universal recipe to solve a dynamic programming problem. You need to go through many problems until you start getting the hang of it. Do not get discouraged. This is hard. Maybe the hardest type of problems you will face in an interview. This is about modeling a problem with relatively simple tools – no need for fancy data structures or algorithms.
I have solved tons of them and still, sometimes I find it difficult to get to the solution. The more you practice, the easier it will be. This is the closest to a recipe to solve dynamic programming problems:
Complexity analysis varies from problem to problem, but in general, the time complexity can be expressed as:
Time ~ Number of subproblems * time per subproblem
It is straightforward to compute the space complexity for a bottom-up solution since it is equal to the space required to store solutions to the subproblems (multidimensional array).
I’ve categorized some problems according to the number of independent dimensions involved. This is not necessary, but something that I have found it useful to have a mental model to follow when designing a solution. You will see patterns, as you code more and more. This is one of them (which I have not found explicitly described anywhere else). Use it if you find it helpful.
Since by now you are very familiar with this problem, I am just going to present the recursive solution:
Going from recursive to top-down is usually mechanical:
And here, the bottom-up solution, where we build a table (from the base cases) to form the solution to the problem we’re looking for. This table is a 1D array: we only need to store the solution to a smaller version of the same problem to be able to derive the solution to the original problem.
This approach could be further optimized in memory, not time (there are faster techniques to compute Fibonacci numbers, but that is a topic for another article), by using just 3 variables instead of an array since we only need to keep track of 2 values, f(n-1) and f(n-2), to produce the output we want, f(n).
This is more advance, but a common pattern. If you only need to keep track of:
By reducing dimensions we improve our space complexity. For now, you can forget about this, but after you get some practice, try to come up with these optimizations yourself to increase your ability to analyze problems and turn your ideas into code. In an interview, I would just go for the simpler version, just discussing potential optimizations and only implementing them if there is enough time after coding your “standard” dynamic programming solution.
You are climbing a staircase. It takes n steps to reach to the top. Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?
Example 1:
Example 2:
Try to solve this problem on your own. You might be able to come up with a recursive solution. Go through my explanation and the previous examples to see if you can code a top-down solution.
A little hint: The fact that the question starts with “In how many ways”, should already make you think of a potential candidate for dynamic programming.
In this case, you want to reach step N. You can reach step number N from step N – 1 or N – 2 because you can jump 1 or 2 steps at a time. If you can solve these two subproblems, you can find the solution to the general problem. Let’s call f(N) the number of ways you can get to step N.
I don’t need to continue. You can already see that:
which means dynamic programming can be used to solve it.
I will not write the code for this problem because … I have already done it in the previous example!
You can write and test your solution here.
Given an unsorted array of integers, find the length of the longest increasing subsequence.
[10,9,2,5,3,7,101,18]
The output would be 4, for the sequence [2,3,7,101]
We need to find the length of the longest increasing subsequence for an array of size n. This sounds like an optimization problem, which could be a candidate for dynamic programming, so let’s try. Imagine that you already have the solution for a problem of size N – let’s call it s(n) – and you add an extra element to the array, called Y. Can you reuse part of the solution to X to solve this new problem? This mental experiment usually gives some good insight into the problem.
In this case, you need to know if the new element can extend one of the existing sequences:
We seem to have an algorithm. Let’s continue our analysis:
Here’s the code for the bottom-up solution.
You can write and test your solution here.
Given n, how many structurally unique BSTs (binary search trees) that store values 1 … n?
Example:
Let’s go through that example. Let’s imagine we have numbers the numbers 1,2,3,4,5. How can I define a BST?
The only thing I really need to do is to pick one of the numbers as the root. Let’s say that element is number 3. I will have:
I can solve the same subproblem for (1,2) – let’s call this solution L – and (4,5) – let’s call this solution R – and count how many BST can be formed with 3 as its root, which is the product L * R. If we do this for every possible root and add all the results up, we have our solution, C(n). As you can see, being methodical and working from a few good examples helps design your algorithms.
In fact, this is all that needs to be done:
In fact, we don’t really care what numbers lie in each side of the array. We just need the size of the subtrees, i.e. the number of elements to the left and to the right of the root. Every instance of this problem will produce the same result. In our previous example, L is the solution to C(2) and so is R. We only need to compute C(2) once, cache it, and reuse it.
You can code and test your solution here.
These problems are usually a little harder to model because they involve two dimensions. A common example is a problem where you have to iterate through two strings or to move through a map.
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.
Example:
Minimizes should make you think of dynamic programming. Let’s analyze this further. We can get from any cell C with indices (i,j) (that is not on the top or left border) from cells A = (i-1, j) and B = (i,j-1). From this, we can see that some problems are going to be computed multiple times. Also, we if know the optimal solution to A and B, we can compute the optimal solution to the current cell as min(sol(A), sol(B)) + 1 – since we can only get to the current cell form A or B and we need one extra step to move from these cells to the current cell. In other words, this problem presents optimal substructure and overlapping problems. We can use dynamic programming.
Here’s the bottom-up solution.
The boundary conditions are defined over the border of the matrix. You can only get to the elements in the border in one way: moving one square to the right or down from the previous element.
You can code and test your solution here.
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 the 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).
Try to come up with a recursive solution. From there, add a cache layer and you’ll have a top-down dynamic programming solution!
The main idea is that, for every item, we have two choices:
After we’ve gone through every single combination, we just need to pick the max value. This is extremely slow, but it’s the first step towards a solution.
Having to decide between two options (add an element to a set or skip it) is a very common pattern that you will see in many problems, so it’s worth knowing it and understanding when and how to apply it.
A bottom-up solution is presented here:
Given two strings text1 and text2, return the length of their longest common subsequence.
A subsequence of a string is a new string generated from the original string with some characters(can be none) deleted without changing the relative order of the remaining characters. (eg, “ace” is a subsequence of “abcde” while “aec” is not). A common subsequence of two strings is a subsequence that is common to both strings.
If there is no common subsequence, return 0.
Example:
Again, compute the longest X makes me think that dynamic programming could help here.
Since you already have some experience with dynamic programming, I’ll go straight to the 2 properties, from the example. Let’s call the strings A and B, and our solution to this problem f(A, B). The idea is to see whether the 2 last characters are equal:
Using examples to prove things is not the way you start a mathematical demonstration, but for a coding interview is more than enough.
You can code and test your solution here.
I have not completed a time/space analysis. That is an exercise for you. Feel free to reach out with questions or comments.
You have probably heard lately the terms tokens, crypto tokens, and cryptocurrencies. In this article,…
Here you are going to see how to approach coding challenges like a pro and…
UTXO vs Account-Based Blockchains Introduction Bitcoin and Ethereum differ in many ways. In this article,…
This guide is a summary of my study notes for the Certified Kubernetes Application Developer…
Bloom filters are a data structure developed by Burton Howard Bloom in 1970. You can…
Merkle trees, also known as binary hash trees, are a type of binary tree. They…