Learning Dynamic Programming...From Staircases?
Prerequisites: Time complexity, basic coding knowledge
Dynamic Programming is considered one of the more challenging topics in competitive programming and coding interviews. Unlike other topics, dynamic programming can appear anywhere and is often paired with other algorithms. I get more questions about DP than any other topic, so I decided to write a blog teaching DP from scratch.
This blog is for readers who aren’t comfortable with DP and those preparing for coding interviews, so it won’t include any advanced techniques.
(Note: I’ll use C++ examples but avoid C++‑specific syntax so this tutorial stays clear for readers using other languages.)
What is Dynamic Programming?
The word “Dynamic Programming” might sound fancy, but it actually just boils down to 2 simple ideas:
-
We can usually build the answer of a problem from subproblems. This is usually called the optimal substructure of a problem. Consider the example of calculating fibbonachi:
Notice that from this definition, we can compute using the subproblems and . This idea becomes clearer when we start solving questions.
-
An algorithm often has overlapping subproblems, and we don’t want to recalculate everytime:
Let’s again use the example of fibbonachi. We could easily turn the recursion into code:1
2
3
4
5int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
}However, this code is terribly inefficient. Notice that fib() calls fib(), and both also calls fib(). The current implementation always recalculates a value, even though we have calculated it before.
This is the case of having an overlapping subproblem, and we could optimize this code by storing results from subproblems:1
2
3
4
5
6
7
8int dp[N]; // dp[i] stores the result of fib(i), initialized to 0
int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
// If we haven’t computed fib(n) before, compute it now
if (dp[n] == 0) dp[n] = fib(n - 1) + fib(n - 2);
return dp[n];
}In summary, DP breaks a problem into overlapping subproblems, solves each exactly once, and combines those solutions to build the final answer.
State, base cases, Bellman (DP) equation
Alright, time for some jargon!
There are three important aspects of a DP algorithm: the state, the initial state, and the DP equation.
-
The state is the information that you want to store for each subproblem. Before you write any code, ask yourself “what does each dp[…] actually represent?” This is the first thing you should think about when designing a DP algorithm. In almost every DP problem, the state is exactly the answer you want for a smaller input. In other words, (or , etc.) should capture the answer of the original problem restricted to that sub-input.
There will be cases where the answer might be the min/max of all of them or the sum of them, but trying out using the answer as the state is always a decent start.
For Fibonacci, denotes the value of fib(). The solution is .
Note: The DP state size doesn’t have to be the same as the question itself! A 1D problem might use a 2D state, or vice versa. It depends on the time constraint of the problem, and what information do you need for the question.
-
The base cases are the subproblems you know outright, which are also the initial building blocks we have to start off our dp process.
For Fibbonachi, we have and according to the definition.
If you can’t find an easily computable base case, you might need to revise your state.
-
Finally, the DP equation (or Bellman equation) shows how we transition from one state to the next. In other words, “given the subproblems I’ve defined, how do I combine their answers to get the answer for this one?”
It’s not easy to come up with the DP equation, and it takes a lot of practice!
For Fibbonachi, we have .
Remember to check whether your equation works with the base cases! If not, you may need different base cases or a revised state.
It’s fine if you don’t understand fully on how to come up with these. There will be many examples later that aim to teach you how to think this through systematically :D
Top-down (memorized recursion) vs Bottom-up (tabular)
DP can be implemented in two ways: top-down and bottom-up.
Top-down defines a recursive function from the DP equation, expressing the solution in terms of smaller subproblems. Before each call, we check if we’ve already solved that subproblem to avoid repeating work and store the result. Our Fibonacci example above uses the top-down approach.
However, most competitive programmers (including me) use bottom-up, where instead of recursing from the top, we build states from the bottom up in order, ensuring that when we compute , all previous values are already available.
1 | int fib(int n) { |
Apart from rare cases like graph DP, most prefer bottom-up since it has less stack overhead. That said, it largely comes down to personal preference and doesn’t usually affect performance significantly. I will demonstrate bottom-up DP, but you should be able to translate it to a top-down approach!
There are two ways to transition between states: push DP or pull DP.
Push DP updates future states based on the current state, while pull DP updates the current state based on previous states.
Pull DP is often more intuitive and is what we’ll use in this tutorial. However, there are situations where you might prefer using push DP.
Building Intuition Via Staircases
Alright, now let’s start working with problems! I have created several variations of the Climbing Stairs problem, and we’ll solve all of them to get a feel for DP!
Climbing Stairs 1
This question is the same as the leetcode version.
You are climbing a staircase. It takes steps to reach the top.
Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?
Constraints:
To approach this problem, think of the three DP aspects we discussed earlier.
Here, we could let the state be the answer itself:
and the answer would be .
Now that we have the state, what are the base cases? What can we know directly?
We can hand calculate some simple base values: , , .
Note: You could also set or even as the base case if you want—it just ensures the DP equation has the right building blocks. Sometimes it helps to write the DP equation first and then pick the base cases you need.
Great! Finally, what do we need to calculate ?
Since we can climb either 1 or 2 steps, we can reach step from either step or step . We can just sum up the ways to get to either steps.
We can write it into an equation as
For all .
Now we have everything we need to write our code! Can you implement it?
1 | int dp[1001]; |
Complexity:
Great! We’ve solved our first DP problem. From now on, I won’t explicitly prompt each step unless a problem is tricky, but always keep the state, base cases, and DP equation in mind!
Climbing Stairs, but k-steps?
You are climbing a staircase. It takes steps to reach the top.
Each time you can take from 1 to steps. In how many distinct ways can you climb to the top?
Constraints:
The state should be the same as before:
and the answer we want would be .
Hmm okay, It’s not obvious which base cases we need. Do we have to set the first k values? That would be expensive!
How about let’s just keep it simple, and have ?
For , the equation is
However, how do we deal with the cases when ?
Let’s use the example when :
1 | dp[1] = dp[0] // Can jump from 0 |
It seems that for the first values we just need to make sure that we stop at , so we have the unified formula
1 | int dp[1001]; |
Complexity:
It is possible to solve this question in time. Can you figure it out?
Assuming ,
Notice most terms overlap between the transitions for and . Can we avoid recomputing them each time? What happens when ? How about ?
With our observation, we get
And when (why does not work with the formula?), the formula simplifies to
Also, .
Writing it as code, we have
1 | int dp[1001]; |
Complexity:
You can also use a prefix sum or sliding window to track the running sum.
Climbing Stairs, but only odd steps?
The solution should be relatively similar to the previous question.
You are climbing a staircase. It takes steps to reach the top.
Each time you can only take odd steps. In how many distinct ways can you climb to the top?
Constraints:
Let be the number of ways we can reach the -th step, with base cases , .
Now let’s think of how we can get to the -th step: Since we can only climb an odd number of steps, we could either climb 1 step (come from ), 3 step (from ), 5 step (from ) … etc. Hence, is just the sum of all previous dp states that we could’ve jumped from.
Our DP equation would be
The code would be
1 | int dp[1001]; |
Time Complexity:
Like the previous question, this is also solvable with O(N). Try applying the same trick to find the simpler recurrence!
Observe that
1 | dp[i] = dp[i - 1] + dp[i - 3] + dp[i - 5] + ... |
So it reduces to the classic Fibonacci sequence, solvable in ! (With matrix exponentiation you can get , but is out of our scope today.)
Climbing Stairs, but can I reach it?
This question asks something different. What state would you use?
You are climbing a staircase. It takes steps to reach the top.
However, you can only move or steps each time. Can you reach the top?
Return “YES” if you can reach the top, or “NO” otherwise.
Constraints:
Since the question is now a true/false question, let be true if we can reach step , false otherwise. The answer would be true if is true.
Our base case can be .
Setting or could be dangerous if the dp array has length since it might be smaller than !
Then, our DP equation would be
(Be careful when or !)
The final code would be
1 | bool dp[1005]; |
Time Complexity:
The question equal tofinding if there exists integers and such that
Which according to Bézout’s lemma has a solution if and only if divides .
However, we would need to use the extended euclidean algorithm to check if there are actually positive solutions, since Bezout doesn’t guarantee the existance of positive solutions.
1 | tuple<int,int,int> extgcd(int a, int b) { |
(Note: Special care needs to be made for division for large due to floating point precision.)
Time Complexity:
Original Problem Link
You are given an integer x. Can you make x by summing up some number of 11,111,1111,11111,…? (You can use any number among them any number of times).
(You will be given numbers in total, and you have to answer all in the time limit.)
Constraints: , .
Any form of the numbers can we written as a combination of and .
Chicken McNugget Theorem or let and enumerate through possible b’s.
Min Cost Climbing Stairs
You are given an integer array cost where is the cost of -th step on a staircase. Once you pay the cost, you can either climb one or two steps.
You can either start from the step with index 0, or the step with index 1.
Return the minimum cost to reach the top of the floor.
Constraints: ,
Since we need the minimum cost this time, how about we let be the minimum cost to reach the -th floor? Our final answer will be , where is the length of cost.
Because we can start at step 0 or step 1 without paying anything first, we set:
To reach step , we could come from with cost , or with cost . Since we want the minimum cost, we will choose the minimum between the two values.
1 | int dp[1005]; |
Time Complexity:
Climbing Stairs, but step consistency?
You are given an integer array score where is the points you get when stepping on the -th step of a staircase (0-based), and a penalty .
You start just before step 0, and want to reach step . On each move you can climb 1 or 2 steps, and you earn the score on the step you land on.
However, If this move’s length (1 or 2) is different from your previous move’s length, you incur a penalty of k points. The very first step you take has no penalty since the “previous” move length doesn’t exist.
Return the maximum total score you can get when you reach the top.
Constraints: ,
1 | Example 1: |
Hmm, the difficulty seems to have ramped up a bit, so let’s walk through how to break it down. This also shows the back-and-forth of developing a DP solution (you rarely nail state and the equation recurrence on the first try).
Alright, so the first question we should ask is “What Is The State?” According to previous experiences, a natural start would be
with the answer being .
We could also define base cases with and . So far so good, right?
However, there will be anissue when we try to design the DP equation. A critical information in this problem is that the size of your previous step will impact the value you obtain this step. With our current design of , we have no way in knowing whether this state comes from an even step or an odd step!
Hence, we need to revise our state to encorporate the knowledge of step parity. We could do this by adding an additional dimension recording whether the step taken was even or odd. That is,
With this new state design, the final answer would be , and we have base case
1 | dp[0][1] = score[0]; |
(Make sure to know why!)
Finally, we can write out a new DP equation.
Let’s think of how we can get . Since we know the step taken to get here was odd, it must come from the step. There are now 2 cases: either we came from a 1-step and not take any penalty, or pfrom a 2-step and take the penalty.
The case for is similar, so we can get the following equation:
1 | dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - k) + score[i]; |
1 | int dp[50001][2]; |
Complexity:
Climbing Stairs, but only to larger values?
You are given an integer array nums where is the value of the -th step of a staircase (0-based).
You start just before step 0, and you want to try climbing the stairs. However, you can only climb to a step if it’s value is strictly larger than the one you are standing on (The first climb is always valid). What is the maximum amount of steps you can take? (You are not required to climb to the top of the stairs.)
Constraints: ,
1 | Example 1: |
A natural way to track progress is:
Then, the answer would be the maximum of all states.
(Check the solution of the next question to see more discussion on states.)
There are two ways we can set the base case. The first one is to set , and the second one would be to set every state to 1. The only difference is if you need to set to 1 if there are no previous smaller steps.
We can only jump from steps before us with a strictly smaller value, so
and if there are no previous smaller steps.
In fact, this question is equivalent to a very classic problem called Longest Increasing Subsequence, and we just solved it!
1 | int dp[20005]; |
Complexity:
LIS can actually be solved in using greedy!
The idea is that we want to have the subsequence as long as possible. But if we cannot make the subsequence longer, we last value of the subsuequence (the tail) as small as possible, so there are more chances to append values in the future!
1 | int lengthOfLIS(vector<int>& nums) { |
Time complexity:
Climbing Stairs, but you get tired?
You are in front of a staircase with steps (indexed from to ). Two arrays of length are given:
: points that you collect when you land on step .
: the energy cost you expend when landing on step (The cost depends only on the landing step since this is a magic staircase.)
You start just before step 0 with a energy budget , and your task is to return the maximum possible points obtained during this trip without using up all your energy.
Constraints:
What extra information do you need so you know if you can afford to land on a step? Since each step can be taken or skipped, how would that choice appear in your DP definition?
This problem is the classic “0/1 Knapsack Problem”, with the energy in the problem statement usually being the capacity of the knapsack we have, and each step corresponding to an item with some weight and value.
Our problem asks for the maximum value, but we also need to encode how much energy has been used so far. A reasonable state design would be
The answer would then be .
(Note that doesn’t require us to actually step on the -th step.)
Our initial values can be and .
For the DP equation, we can enumerate through the energy used, and choose to take the step or not.
1 | dp[i][e] = max( |
1 | int dp[101][20001]; |
Time Complexity:
I hope you feel weird about the state design.
You might wonder why we defined
Instead of something like
Our design was closer to the latter during LIS!
To answer this, Let’s look at the LIS quesiton first. Assume that for that question, we set our state as
Then the answer would be simply (0-base).
However, writing the DP equation will be difficult, since LIS requires us to have the information of “What was the last value?” which the current design does not properly encode.
We could encode the information in the state:
Which is fine, but suffers from worse space complexity.
Now let’s look at this problem. If we make the state as
The DP equation isn’t very difficult to come up with. However, the original state design creates a convenient monotonicity of the states (i.e for all ).
With this monotonicity, we can obtain the answer by .
On the other hand, our new design makes our state no longer monotonically increasing with respect to , which then require us to enumerate through both dimensions to obtain the answer. Even though the complexity is the same, one comes with additional complexity and inconvenience.
To summarize, the reason why we chose let
is that we require the information of the -th step itself during our DP recurrence.
On the other hand, knapsack does not need any information on the values itself and instead only requires the current accumulated energy. This is why we have our original design, whose monotonicity makes finding the final answer simple.
Notice in our DP equation:
1 | dp[i][e] = max( |
For each i, only the array and matters, which means that we can actually just use 2 1D arrays to simulate the process! (This technique is called “Rolling DP”.)
1 | dp_cur[i][e] = max( |
In fact, we could optimize this further by only using one 1D array!
1 | dp[e] = max( |
However, with this design we need to be careful, and enumerate through from largest to smallest. (Try to understand why this is the case!)
Consider the case where there is only 1 step with weight 3 and value 1. If we enumerate through like usual:
1 | init(dp, -1e9); |
You can notice that we have now repeatedly used the -th step! In the first 2 state designs, there was a clear before/after relationship so each step will only get taken once. However, the current state design does not have such relationship, so we would double count if not careful enough.
Phew! You just solved 8 DP questions, good job! I hope after these questions, you have a better understanding of DP, how to construct states, base cases and DP equations. Do note that even though in our example of climbing stairs we always work the states upwards, there might be some DP questions that require you to reverse, or even work from the middle!
A good thing with coming up problems about staircases is that you can generate a lot of variations easily. Knapsack stair climbing + LIS? Penalize by the difference of step length? Multiple staircases? The possibilities are infinite!
The point of the staircase is just a simple template, and to tell you that we can create solve so many different DP problems just from understanding the basics.
Further Practice
This section will no longer be about staircases (Since I’m not that interested in making up stories about magical staircases), and instead be real DP problems from various sources that I think are worth doing.
Atcoder Educational DP Contest
I highly recommend doing problems A ~ H. A, B, D, E should look relatively familiar if you did the previous staircase problems.
The other problems are not that easy. L and M should require the knowledge of prefix sums, so try it out if you know it!
CF 1526C1. Potions
This is one of the first 1500 problems I’ve solved when I started competitive programming in high school. It’s a great question, with an intuitive DP solution and an elegant greedy solution. It’s always my goto problem for anyone interested in competitive programming!
Appendix
Guessing Complexity from contraints
Let n be the main variable in the problem.
If n ≤ 12, the time complexity can be .
If n ≤ 25, the time complexity can be .
If n ≤ 100, the time complexity can be .
If n ≤ 500, the time complexity can be .
If n ≤ 10^4, the time complexity can be .
If n ≤ 10^6, the time complexity can be .
If n ≤ 10^8, the time complexity can be .
If n > 10^8, the time complexity can be or .
Examples of each common time complexity:
[Factorial time]: Permutations of 1 … n
[Exponential time]: Exhaust all subsets of an array of size n
[Cubic time]: Exhaust all triangles with side length less than n
[Quadratic time]: Slow comparison-based sorting (eg. Bubble Sort, Insertion Sort, Selection Sort)
[Linearithmic time]: Fast comparison-based sorting (eg. Merge Sort)
[Linear time]: Linear Search (Finding maximum/minimum element in a 1D array), Counting Sort
[Logarithmic time]: Binary Search, finding GCD (Greatest Common Divisor) using Euclidean Algorithm
[Constant time]: Calculation (eg. Solving linear equations in one unknown)
Leetcode often gives relaxed constraints, but knowing what complexities will TLE is very important.
