Intro to Dynamic Programming
Tags: programming, dp, computerscience, leetcode
Published: 2023-02-22 07:15:34

Dynamic Programming (DP) is a popular algorithmic technique used to solve optimization problems by breaking them down into smaller subproblems and reusing the solutions to those subproblems. It is a commonly used technique in coding interviews and competitive programming challenges. In this blog post, we will discuss how to identify and solve dynamic programming-related LeetCode questions in programming using Python, with an example and detailed explanation.

**Identifying Dynamic Programming Problems**
There are several characteristics of a problem that indicate that it can be solved using dynamic programming:

The problem can be broken down into smaller subproblems
The solutions to subproblems can be combined to solve the larger problem
The problem exhibits optimal substructure, meaning that the optimal solution to a problem can be constructed from optimal solutions to its subproblems.
Once we have identified a problem that can be solved using dynamic programming, the next step is to determine the optimal substructure and overlapping subproblems. We can then use this information to develop a dynamic programming solution.


**Developing a Dynamic Programming Solution**
A dynamic programming solution typically involves creating a table or memoization array to store the solutions to subproblems. We then use a bottom-up approach to fill in the table, starting with the base cases and working our way up to the solution of the larger problem.

To illustrate this process, we will use an example problem from LeetCode.

**Example Problem: Coin Change**

The Coin Change problem asks us to find the minimum number of coins required to make up a given amount. We are given a list of coin denominations, and we can use as many coins of each denomination as we need to make up the amount.

For example, if we have coin denominations [1, 5, 10] and we need to make up an amount of 12, the minimum number of coins required is 2 (one coin of denomination 10 and one coin of denomination 2).

**Step 1**: Identify the optimal substructure
To solve this problem using dynamic programming, we need to identify the optimal substructure. Let's start by defining a function minCoins(n) that returns the minimum number of coins required to make up the amount n.

If we have a list of coin denominations [c1, c2, ..., ck], we can consider two cases for each coin denomination:

We do not use the coin denomination ci. In this case, the minimum number of coins required to make up the amount n is the same as the minimum number of coins required to make up the amount n using only the coin denominations [c1, c2, ..., ci-1].

We use the coin denomination ci. In this case, the minimum number of coins required to make up the amount n is 1 + the minimum number of coins required to make up the amount n-ci using the coin denominations [c1, c2, ..., ci].

The optimal substructure of the problem can be defined using the following recurrence relation:

minCoins(n) = min(minCoins(n-ci) + 1) for all ci in coinDenominations

**Step 2**: Identify overlapping subproblems
The next step is to identify the overlapping subproblems. Since we are recursively computing the minimum number of coins required for each amount, we may end up computing the same subproblem multiple times. To avoid redundant calculations, we can use memoization to store the solutions to subproblems.

**Step 3**: Develop a dynamic programming solution
To develop a dynamic programming solution, we will create a memoization array memo[n] to store the minimum number of coins required to make up the amount n. We will initialize all values in the array to infinity, except for memo[0], which we will set to 0.

We will then use a bottom-up approach to fill in the memoization array. Starting with the base case memo[0], we will iterate through all amounts from 1 to the target amount, filling in the minimum number of coins required for each amount. For each amount n, we will iterate through all coin denominations ci and compute the minimum number of coins required using the recurrence relation:

memo[n] = min(memo[n-ci] + 1) for all ci in coinDenominations

Once we have filled in the memoization array, the solution to the problem is stored in memo[target]. If memo[target] is still infinity, then it is not possible to make up the target amount using the given coin denominations.

Here is the Python code for the dynamic programming solution to the Coin Change problem:


`
    
    def coinChange(coins, amount):
    memo = [float('inf')] * (amount + 1)
    memo[0] = 0
    for n in range(1, amount+1):
        for c in coins:
            if c <= n:
                memo[n] = min(memo[n], memo[n-c] + 1)
                
    return memo[amount] if memo[amount] != float('inf') else -1
`

**Best and Worst Case Space and Time Complexities**
The time complexity of the dynamic programming solution to the Coin Change problem is O(amount*k), where k is the number of coin denominations. This is because we are iterating through all amounts from 1 to the target amount, and for each amount, we are iterating through all coin denominations. Since the size of the memoization array is proportional to the target amount, the space complexity is also O(amount).

In the best case, if the target amount is 0, the time complexity is O(k) because we only need to initialize the memoization array. In the worst case, if it is not possible to make up the target amount using the given coin denominations, the time complexity is O(amount*k) because we will have to iterate through all amounts and coin denominations before determining that the solution is not possible.

**Conclusion**
Dynamic programming is a powerful technique that can be used to solve optimization problems by breaking them down into smaller subproblems and reusing the solutions to those subproblems. When solving dynamic programming problems, it is important to identify the optimal substructure and overlapping subproblems, and to develop a bottom-up dynamic programming solution using memoization. By following these steps and understanding the best and worst case space and time complexities, we can effectively solve dynamic programming-related LeetCode questions in programming with Python.