-
Notifications
You must be signed in to change notification settings - Fork 2
feat(algorithms, dynamic programming): coin change #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
BrianLusina
merged 5 commits into
main
from
feat/algorithms-dynamic-programming-coin-change
Jan 25, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e4fe821
feat(algorithms, dynamic-programming): coin change
BrianLusina e3eb616
updating DIRECTORY.md
bf436bf
Update algorithms/dynamic_programming/coin_change/test_coin_change.py
BrianLusina c80e003
Update algorithms/dynamic_programming/coin_change/README.md
BrianLusina 8018a3d
Update algorithms/dynamic_programming/climb_stairs/__init__.py
BrianLusina File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 23 additions & 12 deletions
35
algorithms/dynamic_programming/climb_stairs/test_climb_stairs.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| # Coin Change | ||
|
|
||
| Correctly determine the fewest number of coins to be given to a customer such that the sum of the coins' value would | ||
| equal the correct amount of change. | ||
|
|
||
| ## For example | ||
|
|
||
| - An input of 15 with [1, 5, 10, 25, 100] should return one nickel (5) | ||
| and one dime (10) or [0, 1, 1, 0, 0] | ||
| - An input of 40 with [1, 5, 10, 25, 100] should return one nickel (5) | ||
| and one dime (10) and one quarter (25) or [0, 1, 1, 1, 0] | ||
|
|
||
| ## Edge cases | ||
|
|
||
| - Does your algorithm work for any given set of coins? | ||
| - Can you ask for negative change? | ||
| - Can you ask for a change value smaller than the smallest coin value? | ||
|
|
||
| ## Exception messages | ||
|
|
||
| Sometimes it is necessary to raise an exception. When you do this, you should include a meaningful error message to | ||
| indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. Not | ||
| every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include a | ||
| message. | ||
|
|
||
| To raise a message with an exception, just write it as an argument to the exception type. For example, instead of | ||
| `raise Exception`, you should write: | ||
|
|
||
| ```python | ||
| raise Exception("Meaningful message indicating the source of the error") | ||
| ``` | ||
|
|
||
| ## Source | ||
|
|
||
| Software Craftsmanship - Coin Change | ||
| Kata [https://web.archive.org/web/20130115115225/http://craftsmanship.sv.cmu.edu:80/exercises/coin-change-kata](https://web.archive.org/web/20130115115225/http://craftsmanship.sv.cmu.edu:80/exercises/coin-change-kata) | ||
|
|
||
| ## Solution | ||
|
|
||
| If we look at the problem, we might immediately think that it could be solved through a greedy approach. However, if we | ||
| look at it closely, we’ll know that it’s not the correct approach here. Let’s take a look at an example to understand why | ||
| this problem can’t be solved with a greedy approach. | ||
|
|
||
| Let's suppose we have coins = [1, 3, 4, 5] and we want to find the total = 7 and we try to solve the problem with a greedy | ||
| approach. In a greedy approach, we always start from the very end of a sorted array and traverse backward to find our | ||
| solution because that allows us to solve the problem without traversing the whole array. However, in this situation, we | ||
| start off with a 5 and add that to our total. We then check if it’s possible to get a 7 with the help of either 4 or 3, | ||
| but as expected, that won't be the case, and we would need to add 1 twice to get our required total. | ||
|
|
||
| The problem seems to be solved, and we have concluded that we need maximum 3 coins to get to the total of 7. However, if | ||
| we take a look at our array, that isn’t the case. In fact, we could have reached the total of 7 with just 2 coins: 4 and 3. | ||
| So, the problem needs to be broken down into subproblems, and an optimal solution can be reached from the optimal | ||
| solutions of its subproblems. | ||
|
|
||
| To split the problem into subproblems, let's assume we know the number of coins required for some total value and the | ||
| last coin denomination is C. Because of the optimal substructure property, the following equation will be true: | ||
|
|
||
| Min(total)=Min(total−C)+1 | ||
|
|
||
| But, we don't know what is the value of C yet, so we compute it for each element of the coins array and select the | ||
| minimum from among them. This creates the following recurrence relation: | ||
|
|
||
| Min(total)=mini=0.....n-1(Min(total−Ci)+1), such that | ||
| Min(total)=0, for total=0 | ||
| Min(total)= -1, for n=0 | ||
|
|
||
| > Note: The problem can also be solved with the help of a simple recursive tree without any backtracking, but that would | ||
| > take extra memory and time complexity, as we can see in the illustration below. | ||
|
|
||
|  | ||
|
|
||
| > Recursive tree for finding minimum number of coins for the total 5 with the coins [1,2,3] | ||
|
|
||
| ### Step-by-step solution construction | ||
|
|
||
| The idea is to solve the problem using the top-down technique of dynamic programming. If the required total is less than | ||
| the number that’s being evaluated, the algorithm doesn’t make any more recursive calls. Moreover, the recursive tree | ||
| calculates the results of many subproblems multiple times. Therefore, if we store the result of each subproblem in a | ||
| table, we can drastically improve the algorithm’s efficiency by accessing the required value at a constant time. This | ||
| massively reduces the number of recursive calls we need to make to reach our results. | ||
|
|
||
| We start our solution by creating a helper function that assists us in calculating the number of coins we need. It has | ||
| three base cases to cover about what to return if the remaining amount is: | ||
|
|
||
| - Less than zero | ||
| - Equal to zero | ||
| - Neither less than zero nor equal to zero | ||
|
|
||
| > The top-down solution, commonly known as the memoization technique, is an enhancement of the recursive solution. It | ||
| > solves the problem of calculating redundant solutions over and over by storing them in an array. | ||
|
|
||
| In the last case, when the remaining amount is neither of the base cases, we traverse the coins array, and at each | ||
| element, we recursively call the calculate_minimum_coins() function, passing the updated remaining amount remaining_amount | ||
| minus the value of the current coin. This step effectively evaluates the number of coins needed for each possible | ||
| denomination to make up the remaining amount. We store the return value of the base cases for each subproblem in a | ||
| variable named result. We then add 1 to the result variable indicating that we're using this coin denomination in the | ||
| process of making up the corresponding total. Now, we assign this value to minimum, which is initially set to infinity | ||
| at the start of each path. | ||
|
|
||
| To avoid recalculating the minimum values for subproblems, we utilize the counter array, which serves as a memoization | ||
| table. This array stores the minimum number of coins required to make up each specific amount of money up to the given | ||
| total. At the end of each path traversal, we update the corresponding index of the counter array with the calculated | ||
| minimum value. Finally, we return the minimum number of coins needed for the given total amount. | ||
|
|
||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|
|
||
| ### Summary | ||
|
|
||
| To recap, the solution to this problem can be divided into the following parts: | ||
|
|
||
| 1. We first check the base cases, if total is either 0 or less than 0: | ||
| - 0 means no new coins need to be added because we have reached a viable solution. | ||
| - Less than 0 means our path can’t lead to this solution, so we need to backtrack. | ||
| 2. After this, we use the top-down approach and traverse the given coin denominations. | ||
| 3. At each iteration, we either pick a coin or we don’t. | ||
| - If we pick a coin, we move on to solve a new subproblem based on the reduced total value. | ||
| - If we don’t pick a coin, then we look up the answer from the counter array if it is already computed to avoid | ||
| recalculation. | ||
| 4. Finally, we return the minimum number of coins required for the given total. | ||
|
|
||
| ### Time Complexity | ||
|
|
||
| The time complexity for the above algorithm is O(n*m). Here, | ||
| n represents the total and m represents the number of coins we have. In the worst case, the height of the recursive tree | ||
| is n as the subproblems solved by the algorithm will be n because we're storing precalculated solutions in a table. Each | ||
| subproblem takes m iterations, one per coin value. So, the time complexity is O(n*m). | ||
|
|
||
| ### Space Complexity | ||
|
|
||
| The space complexity for this algorithm is O(n) because we’re using the counter array which is the size of total. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| """ | ||
| Finds the minimum number of coins that sum up to the total change given the total change due and the list of coins with | ||
| denominations | ||
| """ | ||
|
|
||
| from typing import List | ||
| from itertools import combinations_with_replacement | ||
|
|
||
|
|
||
| def find_minimum_coins(total_change: int, coins: List[int]) -> List[int]: | ||
| """ | ||
| Finds the minimum number coins that add up to the total change given a list of coins. This should return a list of | ||
| the coins that add up to the total change | ||
| :param total_change: Total change due | ||
| :type total_change int | ||
| :param coins: list of denominations | ||
| :type coins list | ||
| :return: list with the least amount of coins that sum up to the total change | ||
| :rtype list | ||
| """ | ||
| # return early when there is no change | ||
| if total_change == 0: | ||
| return [] | ||
|
|
||
| if total_change < 0: | ||
| raise ValueError("Cannot find change of negative values") | ||
|
|
||
| if total_change < min(coins): | ||
| raise ValueError( | ||
| "Cannot find change if total change is smaller than smallest coin" | ||
| ) | ||
|
|
||
| result = None | ||
|
|
||
| for n in range(total_change): | ||
| for combination in combinations_with_replacement(coins, n): | ||
| if sum(combination) == total_change: | ||
| return list(combination) | ||
| if result is None: | ||
| raise ValueError("No combination can add up to target") | ||
| return [] | ||
|
BrianLusina marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def coin_change(coins: List[int], total: int) -> int: | ||
| if total == 0: | ||
| return 0 | ||
| # Initialize dimensions: number of coin types and target amount | ||
| num_coins = len(coins) | ||
|
|
||
| # Create 2D DP table | ||
| # dp[i][j] represents minimum coins needed to make amount j using first i coin types | ||
| dp = [[float("inf")] * (total + 1) for _ in range(num_coins + 1)] | ||
|
|
||
| # Base case: 0 coins needed to make amount 0 | ||
| dp[0][0] = 0 | ||
|
|
||
| # Fill the DP table | ||
| for coin_idx in range(1, num_coins + 1): | ||
| current_coin_value = coins[coin_idx - 1] | ||
|
|
||
| for current_amount in range(total + 1): | ||
| # Option 1: Don't use the current coin type | ||
| dp[coin_idx][current_amount] = dp[coin_idx - 1][current_amount] | ||
|
|
||
| # Option 2: Use the current coin if possible | ||
| if current_amount >= current_coin_value: | ||
| # Compare with using one more of the current coin | ||
| dp[coin_idx][current_amount] = min( | ||
| dp[coin_idx][current_amount], | ||
| dp[coin_idx][current_amount - current_coin_value] + 1, | ||
| ) | ||
|
|
||
| # Return result: -1 if impossible, otherwise the minimum number of coins | ||
| return -1 if dp[num_coins][total] == float("inf") else dp[num_coins][total] | ||
|
|
||
|
|
||
| def coin_change_dp(coins: List[int], total: int) -> int: | ||
| if total < 1: | ||
| return 0 | ||
| counter: List[int | float] = [float("inf")] * total | ||
|
|
||
| def calculate_minimum_coins(remaining_amount: int) -> int: | ||
| if remaining_amount < 0: | ||
| return -1 | ||
| if remaining_amount == 0: | ||
| return 0 | ||
| if counter[remaining_amount - 1] != float("inf"): | ||
| return counter[remaining_amount - 1] | ||
|
|
||
| minimum = float("inf") | ||
|
|
||
| for coin in coins: | ||
| result = calculate_minimum_coins(remaining_amount - coin) | ||
| if 0 <= result < minimum: | ||
| minimum = 1 + result | ||
|
|
||
| counter[remaining_amount - 1] = minimum if minimum != float("inf") else -1 | ||
| return counter[remaining_amount - 1] | ||
|
|
||
| return calculate_minimum_coins(remaining_amount=total) | ||
Binary file added
BIN
+114 KB
...namic_programming/coin_change/images/solutions/coin_change_recursive_tree_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+56.9 KB
...hms/dynamic_programming/coin_change/images/solutions/coin_change_solution_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+61.3 KB
...hms/dynamic_programming/coin_change/images/solutions/coin_change_solution_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+83 KB
...hms/dynamic_programming/coin_change/images/solutions/coin_change_solution_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+63.6 KB
...hms/dynamic_programming/coin_change/images/solutions/coin_change_solution_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+110 KB
...hms/dynamic_programming/coin_change/images/solutions/coin_change_solution_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+81.8 KB
...hms/dynamic_programming/coin_change/images/solutions/coin_change_solution_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+98.6 KB
...hms/dynamic_programming/coin_change/images/solutions/coin_change_solution_7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+65 KB
...hms/dynamic_programming/coin_change/images/solutions/coin_change_solution_8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.