diff --git a/DIRECTORY.md b/DIRECTORY.md index 0fd346b1..77c530a6 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -57,6 +57,8 @@ * [Test Max Profit With Fee](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/buy_sell_stock/test_max_profit_with_fee.py) * Climb Stairs * [Test Climb Stairs](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/climb_stairs/test_climb_stairs.py) + * Coin Change + * [Test Coin Change](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/coin_change/test_coin_change.py) * Countingbits * [Test Counting Bits](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/countingbits/test_counting_bits.py) * Decodeways @@ -1076,7 +1078,6 @@ * [Test Bowling](https://github.com/BrianLusina/PythonSnips/blob/master/tests/puzzles/test_bowling.py) * [Test Cake Is Not A Lie](https://github.com/BrianLusina/PythonSnips/blob/master/tests/puzzles/test_cake_is_not_a_lie.py) * [Test Chess Board Cell Color](https://github.com/BrianLusina/PythonSnips/blob/master/tests/puzzles/test_chess_board_cell_color.py) - * [Test Coin Change](https://github.com/BrianLusina/PythonSnips/blob/master/tests/puzzles/test_coin_change.py) * [Test Coin Flip](https://github.com/BrianLusina/PythonSnips/blob/master/tests/puzzles/test_coin_flip.py) * [Test Count Vegetables](https://github.com/BrianLusina/PythonSnips/blob/master/tests/puzzles/test_count_vegetables.py) * [Test Doomsday Fuel](https://github.com/BrianLusina/PythonSnips/blob/master/tests/puzzles/test_doomsday_fuel.py) diff --git a/algorithms/dynamic_programming/climb_stairs/__init__.py b/algorithms/dynamic_programming/climb_stairs/__init__.py index a77adcf1..1e80fe96 100644 --- a/algorithms/dynamic_programming/climb_stairs/__init__.py +++ b/algorithms/dynamic_programming/climb_stairs/__init__.py @@ -13,24 +13,40 @@ def climb_stairs(n: int) -> int: return second -def climb(n: int) -> int: +def climb_stairs_dp_bottom_up(n: int) -> int: + if n < 0: + return 0 + if n <= 1: + return 1 + + dp = [0] * (n + 1) + dp[1] = 1 + dp[2] = 2 + + for idx in range(3, n + 1): + dp[idx] = dp[idx - 1] + dp[idx - 2] + + return dp[n] + + +def climb_stairs_dp_top_down(n: int) -> int: """ Finds the number of possible ways to climb up n steps given the steps can be climbed wither 1 at a time or 2 at a time :param n: number of steps :return: number of possible ways to climb up the steps - >>> climb(2) + >>> climb_stairs_dp_top_down(2) 2 - >>> climb(1) + >>> climb_stairs_dp_top_down(1) 1 - >>> climb(3) + >>> climb_stairs_dp_top_down(3) 3 """ if n < 0: return 0 if n == 1 or n == 0: return 1 - return climb(n - 1) + climb(n - 2) + return climb_stairs_dp_top_down(n - 1) + climb_stairs_dp_top_down(n - 2) diff --git a/algorithms/dynamic_programming/climb_stairs/test_climb_stairs.py b/algorithms/dynamic_programming/climb_stairs/test_climb_stairs.py index 96c4b40a..59f84b7f 100644 --- a/algorithms/dynamic_programming/climb_stairs/test_climb_stairs.py +++ b/algorithms/dynamic_programming/climb_stairs/test_climb_stairs.py @@ -1,23 +1,34 @@ import unittest - -from . import climb_stairs +from parameterized import parameterized +from algorithms.dynamic_programming.climb_stairs import ( + climb_stairs, + climb_stairs_dp_bottom_up, + climb_stairs_dp_top_down, +) + +CLIMB_STAIRS_TEST_CASES = [ + (2, 2), + (3, 3), + (4, 5), + (4, 5), + (5, 8), +] class ClimbStairsTestCase(unittest.TestCase): - def test_1(self): - """should return 2 for n = 2""" - n = 2 - expected = 2 + @parameterized.expand(CLIMB_STAIRS_TEST_CASES) + def test_climb_stairs(self, n: int, expected: int): actual = climb_stairs(n) - self.assertEqual(expected, actual) - def test_2(self): - """should return 3 for n = 3""" - n = 3 - expected = 3 - actual = climb_stairs(n) + @parameterized.expand(CLIMB_STAIRS_TEST_CASES) + def test_climb_stairs_dp_bottom_up(self, n: int, expected: int): + actual = climb_stairs_dp_bottom_up(n) + self.assertEqual(expected, actual) + @parameterized.expand(CLIMB_STAIRS_TEST_CASES) + def test_climb_stairs_dp_top_down(self, n: int, expected: int): + actual = climb_stairs_dp_top_down(n) self.assertEqual(expected, actual) diff --git a/algorithms/dynamic_programming/coin_change/README.md b/algorithms/dynamic_programming/coin_change/README.md new file mode 100644 index 00000000..ed74a26b --- /dev/null +++ b/algorithms/dynamic_programming/coin_change/README.md @@ -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. + +![Coin Change Recursive Tree](images/solutions/coin_change_recursive_tree_1.png) + +> 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. + +![Solution 1](./images/solutions/coin_change_solution_1.png) +![Solution 2](./images/solutions/coin_change_solution_2.png) +![Solution 3](./images/solutions/coin_change_solution_3.png) +![Solution 4](./images/solutions/coin_change_solution_4.png) +![Solution 5](./images/solutions/coin_change_solution_5.png) +![Solution 6](./images/solutions/coin_change_solution_6.png) +![Solution 7](./images/solutions/coin_change_solution_7.png) +![Solution 8](./images/solutions/coin_change_solution_8.png) + +### 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. diff --git a/algorithms/dynamic_programming/coin_change/__init__.py b/algorithms/dynamic_programming/coin_change/__init__.py new file mode 100644 index 00000000..123994b0 --- /dev/null +++ b/algorithms/dynamic_programming/coin_change/__init__.py @@ -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 [] + + +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) diff --git a/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_recursive_tree_1.png b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_recursive_tree_1.png new file mode 100644 index 00000000..db5c5eaa Binary files /dev/null and b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_recursive_tree_1.png differ diff --git a/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_1.png b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_1.png new file mode 100644 index 00000000..3a36d36b Binary files /dev/null and b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_1.png differ diff --git a/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_2.png b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_2.png new file mode 100644 index 00000000..d32620f8 Binary files /dev/null and b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_2.png differ diff --git a/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_3.png b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_3.png new file mode 100644 index 00000000..568152b7 Binary files /dev/null and b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_3.png differ diff --git a/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_4.png b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_4.png new file mode 100644 index 00000000..5613bc05 Binary files /dev/null and b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_4.png differ diff --git a/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_5.png b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_5.png new file mode 100644 index 00000000..33e880b9 Binary files /dev/null and b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_5.png differ diff --git a/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_6.png b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_6.png new file mode 100644 index 00000000..fc07577e Binary files /dev/null and b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_6.png differ diff --git a/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_7.png b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_7.png new file mode 100644 index 00000000..343f97fd Binary files /dev/null and b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_7.png differ diff --git a/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_8.png b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_8.png new file mode 100644 index 00000000..480d9fa0 Binary files /dev/null and b/algorithms/dynamic_programming/coin_change/images/solutions/coin_change_solution_8.png differ diff --git a/algorithms/dynamic_programming/coin_change/test_coin_change.py b/algorithms/dynamic_programming/coin_change/test_coin_change.py new file mode 100644 index 00000000..147e0af2 --- /dev/null +++ b/algorithms/dynamic_programming/coin_change/test_coin_change.py @@ -0,0 +1,79 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.dynamic_programming.coin_change import ( + find_minimum_coins, + coin_change, + coin_change_dp, +) + +FIND_MINIMUM_COINS_TEST_CASES = [ + (25, [1, 5, 10, 25, 100], [25]), + (15, [1, 5, 10, 25, 100], [5, 10]), + (23, [1, 4, 15, 20, 50], [4, 4, 15]), + (63, [1, 5, 10, 21, 25], [21, 21, 21]), + ( + 999, + [1, 2, 5, 10, 20, 50, 100], + [2, 2, 5, 20, 20, 50, 100, 100, 100, 100, 100, 100, 100, 100, 100], + ), + (21, [2, 5, 10, 20, 50], [2, 2, 2, 5, 10]), + (27, [4, 5], [4, 4, 4, 5, 5, 5]), + (0, [1, 5, 10, 21, 25], []), +] + + +class FindMinimumCoinsTest(unittest.TestCase): + @parameterized.expand(FIND_MINIMUM_COINS_TEST_CASES) + def test_find_minimum_coins( + self, total_change: int, coins: List[int], expected: List[int] + ): + actual = find_minimum_coins(total_change, coins) + self.assertEqual(expected, actual) + + def test_error_testing_for_change_smaller_than_smallest_coin(self): + with self.assertRaisesWithMessage(ValueError): + find_minimum_coins(3, [5, 10]) + + def test_error_if_no_combination_can_add_up_to_target(self): + with self.assertRaisesWithMessage(ValueError): + find_minimum_coins(94, [5, 10]) + + def test_cannot_find_negative_change_values(self): + with self.assertRaisesWithMessage(ValueError): + find_minimum_coins(-5, [1, 2, 5]) + + # Utility functions + def setUp(self): + try: + self.assertRaisesRegex + except AttributeError: + self.assertRaisesRegex = self.assertRaisesRegexp + + def assertRaisesWithMessage(self, exception): + return self.assertRaisesRegex(exception, r".+") + + +COIN_CHANGE_TEST_CASES = [ + ([1, 2, 5], 11, 3), + ([2], 4, 2), + ([5], 3, -1), + ([1, 2, 5], 0, 0), + ([2, 3, 4, 6, 8], 23, 4), +] + + +class CoinChangeTest(unittest.TestCase): + @parameterized.expand(COIN_CHANGE_TEST_CASES) + def test_coin_change(self, coins: List[int], total: int, expected: int): + actual = coin_change(coins, total) + self.assertEqual(expected, actual) + + @parameterized.expand(COIN_CHANGE_TEST_CASES) + def test_coin_change_dp(self, coins: List[int], total: int, expected: int): + actual = coin_change_dp(coins, total) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/puzzles/coin_change/README.md b/puzzles/coin_change/README.md deleted file mode 100644 index 3acc2018..00000000 --- a/puzzles/coin_change/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# 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) diff --git a/puzzles/coin_change/__init__.py b/puzzles/coin_change/__init__.py deleted file mode 100644 index 93039494..00000000 --- a/puzzles/coin_change/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -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 itertools import combinations_with_replacement - - -def find_minimum_coins(total_change, coins): - """ - 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") diff --git a/tests/puzzles/test_coin_change.py b/tests/puzzles/test_coin_change.py deleted file mode 100644 index 5f6cb17c..00000000 --- a/tests/puzzles/test_coin_change.py +++ /dev/null @@ -1,58 +0,0 @@ -import unittest - -from puzzles.coin_change import find_minimum_coins - - -class ChangeTest(unittest.TestCase): - def test_single_coin_change(self): - self.assertEqual(find_minimum_coins(25, [1, 5, 10, 25, 100]), [25]) - - def test_multiple_coin_change(self): - self.assertEqual(find_minimum_coins(15, [1, 5, 10, 25, 100]), [5, 10]) - - def test_change_with_Lilliputian_Coins(self): - self.assertEqual(find_minimum_coins(23, [1, 4, 15, 20, 50]), [4, 4, 15]) - - def test_change_with_Lower_Elbonia_Coins(self): - self.assertEqual(find_minimum_coins(63, [1, 5, 10, 21, 25]), [21, 21, 21]) - - def test_large_target_values(self): - self.assertEqual( - find_minimum_coins(999, [1, 2, 5, 10, 20, 50, 100]), - [2, 2, 5, 20, 20, 50, 100, 100, 100, 100, 100, 100, 100, 100, 100], - ) - - def test_possible_change_without_unit_coins_available(self): - self.assertEqual(find_minimum_coins(21, [2, 5, 10, 20, 50]), [2, 2, 2, 5, 10]) - - def test_another_possible_change_without_unit_coins_available(self): - self.assertEqual(find_minimum_coins(27, [4, 5]), [4, 4, 4, 5, 5, 5]) - - def test_no_coins_make_0_change(self): - self.assertEqual(find_minimum_coins(0, [1, 5, 10, 21, 25]), []) - - def test_error_testing_for_change_smaller_than_smallest_coin(self): - with self.assertRaisesWithMessage(ValueError): - find_minimum_coins(3, [5, 10]) - - def test_error_if_no_combination_can_add_up_to_target(self): - with self.assertRaisesWithMessage(ValueError): - find_minimum_coins(94, [5, 10]) - - def test_cannot_find_negative_change_values(self): - with self.assertRaisesWithMessage(ValueError): - find_minimum_coins(-5, [1, 2, 5]) - - # Utility functions - def setUp(self): - try: - self.assertRaisesRegex - except AttributeError: - self.assertRaisesRegex = self.assertRaisesRegexp - - def assertRaisesWithMessage(self, exception): - return self.assertRaisesRegex(exception, r".+") - - -if __name__ == "__main__": - unittest.main()