What Is Dynamic Programming
What Is Dynamic Programming
As a beginner, these theoretical definitions may be hard to wrap your head around.
Don't worry though - at the end of this chapter, we'll talk about how to practically spot
when DP is applicable. For now, let's look a little deeper at both characteristics.
The Fibonacci sequence is a classic example used to explain DP. For those who are
unfamiliar with the Fibonacci sequence, it is a sequence of numbers that starts with 0, 1,
and each subsequent number is obtained by adding the previous two numbers
together.
These attributes may seem familiar to you. Greedy problems have optimal substructure,
but not overlapping subproblems. Divide and conquer algorithms break a problem into
subproblems, but these subproblems are not overlapping (which is why DP and divide
and conquer are commonly mistaken for one another).
Dynamic programming is a powerful tool because it can break a complex problem into
manageable subproblems, avoid unnecessary recalculation of overlapping subproblems,
and use the results of those subproblems to solve the initial complex problem. DP not
only aids us in solving complex problems, but it also greatly improves the time
complexity compared to brute force solutions. For example, the brute force solution for
calculating the Fibonacci sequence has exponential time complexity, while the dynamic
programming solution will have linear time complexity. Throughout this explore card,
you will gain a better understanding of what makes DP so powerful. In the next section,
we'll discuss the two main methods of implementing a DP algorithm.
Bottom-up (Tabulation)
Bottom-up is implemented with iteration and starts at the base cases. Let's use the
Fibonacci sequence as an example again. The base cases for the Fibonacci sequence
are F(0) = 0F(0)=0 and F(1) = 1F(1)=1. With bottom-up, we would use these base
cases to calculate F(2)F(2), and then use that result to calculate F(3)F(3), and so on
all the way up to F(n)F(n).
F = array of length (n + 1)
F[0] = 0
F[1] = 1
for i from 2 to n:
Notice that we need to calculate F(2)F(2) three times. This might not seem like a big
deal, but if we were to calculate F(6)F(6), this entire image would be only one child of
the root. Imagine if we wanted to find F(100)F(100) - the amount of computation is
exponential and will quickly explode. The solution to this is to memoize results.
memoizing a result means to store the result of a function call, usually in a hashmap or
an array, so that when the same function call is made again, we can simply return
the memoized result instead of recalculating the result.
memo = hashmap
if i is 0 or 1:
return i
return memo[i]
Which is better?
Any DP algorithm can be implemented with either method, and there are reasons for
choosing either over the other. However, each method has one main advantage that
stands out:
We'll be talking more about these two options throughout the card. For now, all you
need to know is that top-down uses recursion, and bottom-up uses iteration.
When to Use DPReport Issue
Unfortunately, it is hard to identify when a problem fits into these definitions. Instead,
let's discuss some common characteristics of DP problems that are easy to identify.
The first characteristic that is common in DP problems is that the problem will ask for
the optimum value (maximum or minimum) of something, or the number of ways there
are to do something. For example:
Note: Not all DP problems follow this format, and not all problems that follow these
formats should be solved using DP. However, these formats are very common for DP
problems and are generally a hint that you should consider using dynamic
programming.
When it comes to identifying if a problem should be solved with DP, this first
characteristic is not sufficient. Sometimes, a problem in this format (asking for the
max/min/longest etc.) is meant to be solved with a greedy algorithm. The next
characteristic will help us determine whether a problem should be solved using a greedy
algorithm or dynamic programming.
You are a professional robber planning to rob houses along a street. Each house has a
certain amount of money stashed, the only constraint stopping you from robbing each
of them is that adjacent houses have security systems connected and it will
automatically contact the police if two adjacent houses were broken into on the same
night.
Given an integer array nums representing the amount of money of each house, return
the maximum amount of money you can rob tonight without alerting the police.
In this problem, each decision will affect what options are available to the robber in the
future. For example, with the test case \text{nums = [2, 7, 9, 3,
1]}nums = [2, 7, 9, 3, 1], the optimal solution is to rob the houses with \text{2}2, \
text{9}9, and \text{1}1 money. However, if we were to iterate from left to right in a
greedy manner, our first decision would be whether to rob the first or second house. 7 is
way more money than 2, so if we were greedy, we would choose to rob house 7.
However, this prevents us from robbing the house with 9 money. As you can see, our
decision between robbing the first or second house affects which options are available
for future decisions.
When you're solving a problem on your own and trying to decide if the second
characteristic is applicable, assume it isn't, then try to think of a counterexample that
proves a greedy algorithm won't work. If you can think of an example where earlier
decisions affect future decisions, then DP is applicable.
To summarize: if a problem is asking for the maximum/minimum/longest/shortest of
something, the number of ways to do something, or if it is possible to reach a certain
point, it is probably greedy or DP. With time and practice, it will become easier to
identify which is the better approach for a given problem. Although, in general, if the
problem has constraints that cause decisions to affect other decisions, such as using one
element prevents the usage of other elements, then we should consider using dynamic
programming to solve the problem. These two characteristics can be used to identify
if a problem should be solved with DP.
Note: these characteristics should only be used as guidelines - while they are extremely
common in DP problems, at the end of the day DP is a very broad topic.
Optimal substructure
Non-overlapping subproblems
Overlapping subproblems
RedoSubmit
Correct.
Problems that have optimal substructure and overlapping subproblems are good
problems to be solved with dynamic programming.