diff --git a/exercises/practice/change/.approaches/config.json b/exercises/practice/change/.approaches/config.json index 00716db81..46b784e67 100644 --- a/exercises/practice/change/.approaches/config.json +++ b/exercises/practice/change/.approaches/config.json @@ -7,15 +7,33 @@ "approaches": [ { "uuid": "d0b615ca-3a02-4d66-ad10-e0c513062189", - "slug": "dynamic-programming", - "title": "Dynamic Programming Approach", - "blurb": "Use dynamic programming to find the most efficient change combination.", + "slug": "dynamic-programming-top-down", + "title": "Dynamic Programming: Top Down", + "blurb": "Break the required amount into smaller amounts and reuse saved results to quickly find the final result.", "authors": [ "jagdish-15" ], "contributors": [ "kahgoh" ] + }, + { + "uuid": "daf47878-1607-4f22-b2df-1049f3d6802c", + "slug": "dynamic-programming-bottom-up", + "title": "Dynamic Programming: Bottom Up", + "blurb": "Start from the available coins and calculate the amounts that can be made from them.", + "authors": [ + "kahgoh" + ] + }, + { + "uuid": "06ae63ec-5bf3-41a0-89e3-2772e4cdbf5d", + "slug": "recursive", + "title": "Recursive", + "blurb": "Use recursion to recursively find the most efficient change for a given amount.", + "authors": [ + "kahgoh" + ] } ] } diff --git a/exercises/practice/change/.approaches/dynamic-programming-bottom-up/content.md b/exercises/practice/change/.approaches/dynamic-programming-bottom-up/content.md new file mode 100644 index 000000000..84983df5e --- /dev/null +++ b/exercises/practice/change/.approaches/dynamic-programming-bottom-up/content.md @@ -0,0 +1,84 @@ +# Dynamic Programming - Bottom up + +```java +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +class ChangeCalculator { + + private final List currencyCoins; + + ChangeCalculator(List currencyCoins) { + this.currencyCoins = List.copyOf(currencyCoins); + } + + List computeMostEfficientChange(int grandTotal) { + if (grandTotal < 0) { + throw new IllegalArgumentException("Negative totals are not allowed."); + } + if (grandTotal == 0) { + return Collections.emptyList(); + } + Set reachableTotals = new HashSet<>(); + ArrayDeque> queue = new ArrayDeque<>(currencyCoins.stream().map(List::of).toList()); + + while (!queue.isEmpty()) { + List next = queue.poll(); + int total = next.stream().mapToInt(Integer::intValue).sum(); + if (total == grandTotal) { + return next; + } + if (total < grandTotal && reachableTotals.add(total)) { + for (Integer coin : currencyCoins) { + List toCheck = new ArrayList<>(next); + toCheck.add(coin); + queue.offer(toCheck); + } + } + } + + throw new IllegalArgumentException("The total " + grandTotal + " cannot be represented in the given currency."); + } +} +``` + +This approach starts from the coins and calculates which amounts can be made up by the coins. + +The `grandTotal` is first validated to ensure that it is a positive number greater than 0. +Two data structures are then created: + +- a queue to maintain a combination of coins to check +- a set to keep track of the totals from the combinations that have been seen + +The queue is initialized with a number of combinations that consist just each of the coins. +For example, if the available coins are 5, 10 and 20, then the queue begins with three combinations: + +- the first combination has just 5 +- the second has just 10 +- the third has just 20 + +Thus, the queue contains `[[5], [10], [20]]`. + +For each combination in the queue, the loop calculates the sum of the combination. +If the sum equals the desired total, it has found the combination. +Otherwise new combinations are added to the queue by adding each of the coins to the end of the combination: + +- less than the desired total, and: +- the total has _not_ yet been "seen" (the Set's [add][set-add] method returns `true` if a new item is being added and `false` if it is already in the Set) + +~~~~exercism/note +If the total has been "seen", there is no need to recheck the amounts because shorter combinations are always checked before longer combinations. +So, if the total is encountered again, we must have found a shorter combination to reach the same amount earlier. +~~~~ + +Continuing with the above example, the first combination contains just `5`. +When this is processed, the combinations `[5, 5]`, `[5, 10]` and `[5, 20]` would be added to the end of the queue and the queue becomes `[[10], [20],[5 ,5], [5, 10], [5, 20]]` for the next iteration. +Adding to the end of the queue ensures that the shorter combinations are checked first and allows the combination to simply be returned when the total is reached. + +The total can not be reached when there are no combinations in the queue. + +[set-add]: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Set.html#add(E) diff --git a/exercises/practice/change/.approaches/dynamic-programming-bottom-up/snippet.txt b/exercises/practice/change/.approaches/dynamic-programming-bottom-up/snippet.txt new file mode 100644 index 000000000..354ea1739 --- /dev/null +++ b/exercises/practice/change/.approaches/dynamic-programming-bottom-up/snippet.txt @@ -0,0 +1,8 @@ +while (!queue.isEmpty()) { + int total = next.stream().mapToInt(Integer::intValue).sum(); + if (total < grandTotal && reachableTotals.add(total)) { + for (Integer coin : currencyCoins) { + queue.add(append(next, coin)); + } + } +} \ No newline at end of file diff --git a/exercises/practice/change/.approaches/dynamic-programming/content.md b/exercises/practice/change/.approaches/dynamic-programming-top-down/content.md similarity index 98% rename from exercises/practice/change/.approaches/dynamic-programming/content.md rename to exercises/practice/change/.approaches/dynamic-programming-top-down/content.md index 640c58a47..06dbf9ba8 100644 --- a/exercises/practice/change/.approaches/dynamic-programming/content.md +++ b/exercises/practice/change/.approaches/dynamic-programming-top-down/content.md @@ -1,4 +1,4 @@ -# Dynamic Programming Approach +# Dynamic Programming - Top Down ```java import java.util.List; @@ -12,7 +12,7 @@ class ChangeCalculator { } List computeMostEfficientChange(int grandTotal) { - if (grandTotal < 0) + if (grandTotal < 0) throw new IllegalArgumentException("Negative totals are not allowed."); List> coinsUsed = new ArrayList<>(grandTotal + 1); @@ -64,5 +64,5 @@ It minimizes the number of coins needed by breaking down the problem into smalle ## Time and Space Complexity The time complexity of this approach is **O(n * m)**, where `n` is the `grandTotal` and `m` is the number of available coin denominations. This is because we iterate over all coin denominations for each amount up to `grandTotal`. - + The space complexity is **O(n)** due to the list `coinsUsed`, which stores the most efficient coin combination for each total up to `grandTotal`. diff --git a/exercises/practice/change/.approaches/dynamic-programming-top-down/snippet.txt b/exercises/practice/change/.approaches/dynamic-programming-top-down/snippet.txt new file mode 100644 index 000000000..d79e5e5f6 --- /dev/null +++ b/exercises/practice/change/.approaches/dynamic-programming-top-down/snippet.txt @@ -0,0 +1,8 @@ +for (int i = 1; i <= grandTotal; i++) { + for (int coin: currencyCoins) { + List currentCombination = coinsUsed.get(i - coin).add(coin); + if (bestCombination == null || currentCombination.size() < bestCombination.size()) + bestCombination = currentCombination; + } + coinsUsed.add(bestCombination); +} \ No newline at end of file diff --git a/exercises/practice/change/.approaches/dynamic-programming/snippet.txt b/exercises/practice/change/.approaches/dynamic-programming/snippet.txt deleted file mode 100644 index 25f90e6f5..000000000 --- a/exercises/practice/change/.approaches/dynamic-programming/snippet.txt +++ /dev/null @@ -1,8 +0,0 @@ -class ChangeCalculator { - private final List currencyCoins; - - ChangeCalculator(List currencyCoins) { - this.currencyCoins = currencyCoins; - } - // computeMostEfficientChange method -} diff --git a/exercises/practice/change/.approaches/introduction.md b/exercises/practice/change/.approaches/introduction.md index 672aae038..60f5e2937 100644 --- a/exercises/practice/change/.approaches/introduction.md +++ b/exercises/practice/change/.approaches/introduction.md @@ -1,14 +1,62 @@ -# Introduction +# Introduction -There is an idiomatic approach to solving "Change." -You can use [dynamic programming][dynamic-programming] to calculate the minimum number of coins required for a given total. +There are a couple of different ways to solve "Change". +The [recursive approach][approach-recursive] uses recursion to find most efficient change for remaining amounts assuming a coin is included. +[Dynamic programming][dynamic-programming] calculates the solution starting from the required total ([the top][approach-dynamic-programming-top-down]) or from the amounts that can be covered by the coins ([the bottom][approach-dynamic-programming-bottom-up]). ## General guidance The key to solving "Change" is understanding that not all totals can be reached with the available coin denominations. The solution needs to figure out which totals can be achieved and how to combine the coins optimally. -## Approach: Dynamic Programming +## Approach: Recursive + +```java +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +class ChangeCalculator { + + private final List currencyCoins; + + ChangeCalculator(List currencyCoins) { + this.currencyCoins = List.copyOf(currencyCoins); + } + + List computeMostEfficientChange(int grandTotal) { + if (grandTotal < 0) { + throw new IllegalArgumentException("Negative totals are not allowed."); + } + if (grandTotal == 0) { + return Collections.emptyList(); + } + + return currencyCoins.stream().map(coin -> { + int remaining = grandTotal - coin; + if (remaining == 0) { + return List.of(coin); + } + + try { + List result = new ArrayList<>(computeMostEfficientChange(remaining)); + result.add(coin); + result.sort(Integer::compare); + return result; + } catch (IllegalArgumentException e) { + return Collections.emptyList(); + } + }) + .filter(c -> !c.isEmpty()) + .min(Comparator.comparingInt(List::size)) + .orElseThrow(() -> new IllegalArgumentException("The total " + grandTotal + " cannot be represented in the given currency.")); + + } +} +``` + +## Approach: Dynamic Programming - Top down ```java import java.util.List; @@ -22,7 +70,7 @@ class ChangeCalculator { } List computeMostEfficientChange(int grandTotal) { - if (grandTotal < 0) + if (grandTotal < 0) throw new IllegalArgumentException("Negative totals are not allowed."); List> coinsUsed = new ArrayList<>(grandTotal + 1); @@ -49,7 +97,64 @@ class ChangeCalculator { } ``` -For a detailed look at the code and logic, see the full explanation in the [Dynamic Programming Approach][approach-dynamic-programming]. +For a detailed look at the code and logic, see the full explanation in the [Dynamic Programming - Top Down][approach-dynamic-programming-top-down]. + +## Approach: Dyanmic Programming - Bottom up + +```java +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +class ChangeCalculator { + + private final List currencyCoins; + + ChangeCalculator(List currencyCoins) { + this.currencyCoins = List.copyOf(currencyCoins); + } + + List computeMostEfficientChange(int grandTotal) { + if (grandTotal < 0) { + throw new IllegalArgumentException("Negative totals are not allowed."); + } + if (grandTotal == 0) { + return Collections.emptyList(); + } + Set reachableTotals = new HashSet<>(); + ArrayDeque> queue = new ArrayDeque<>(currencyCoins.stream().map(List::of).toList()); + + while (!queue.isEmpty()) { + List next = queue.poll(); + int total = next.stream().mapToInt(Integer::intValue).sum(); + if (total == grandTotal) { + return next; + } + if (total < grandTotal && reachableTotals.add(total)) { + for (Integer coin : currencyCoins) { + List toCheck = new ArrayList<>(next); + toCheck.add(coin); + queue.offer(toCheck); + } + } + } + + throw new IllegalArgumentException("The total " + grandTotal + " cannot be represented in the given currency."); + } +} +``` + +For a detailed look at the code and logic, see the full explanation in the [Dynamic Programming - Bottom Up][approach-dynamic-programming-bottom-up]. + +## Which approach to use? + +The recursive approach is generally inefficient compared to either dynamic programming approach because the recursion requires recalculating the most efficient change for certain amounts. +Both dynamic programming approaches avoids this by building on the results computed previously at each step. -[approach-dynamic-programming]: https://exercism.org/tracks/java/exercises/change/approaches/dynamic-programming +[approach-recursive]: https://exercism.org/tracks/java/exercises/change/approaches/recursive +[approach-dynamic-programming-top-down]: https://exercism.org/tracks/java/exercises/change/approaches/dynamic-programming-top-down +[approach-dynamic-programming-bottom-up]: https://exercism.org/tracks/java/exercises/change/approaches/dynamic-programming-bottom-up [dynamic-programming]: https://en.wikipedia.org/wiki/Dynamic_programming diff --git a/exercises/practice/change/.approaches/recursive/content.md b/exercises/practice/change/.approaches/recursive/content.md new file mode 100644 index 000000000..25ef36dd4 --- /dev/null +++ b/exercises/practice/change/.approaches/recursive/content.md @@ -0,0 +1,56 @@ +# Recursive + +```java +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +class ChangeCalculator { + + private final List currencyCoins; + + ChangeCalculator(List currencyCoins) { + this.currencyCoins = List.copyOf(currencyCoins); + } + + List computeMostEfficientChange(int grandTotal) { + if (grandTotal < 0) { + throw new IllegalArgumentException("Negative totals are not allowed."); + } + if (grandTotal == 0) { + return Collections.emptyList(); + } + + return currencyCoins.stream().map(coin -> { + int remaining = grandTotal - coin; + if (remaining == 0) { + return List.of(coin); + } + + try { + List result = new ArrayList<>(computeMostEfficientChange(remaining)); + result.add(coin); + result.sort(Integer::compare); + return result; + } catch (IllegalArgumentException e) { + return Collections.emptyList(); + } + }) + .filter(c -> !c.isEmpty()) + .min(Comparator.comparingInt(List::size)) + .orElseThrow(() -> new IllegalArgumentException("The total " + grandTotal + " cannot be represented in the given currency.")); + + } +} +``` + +The recursive approach works by iterating through the available coins and recursively calling itself to find the most efficient change with it. +It starts by validating the `grandTotal` argument. +If valid, use a stream to go through the available coins and determines how much change is still required if the coin is included. +If no more change is required, the most efficient change consists simply of the coin on its own. +Otherwise it will recursively call itself to find the most efficient change for the remaining amount. +The recursive call is done in a `try-catch` block because the method throws an `IllegalArgumentionException` if the change can not be made. +An empty list is used to indicate when the change can not be made in the stream. +The stream filters out the empty list in the next step before finding the smallest list. +If the stream is empty, an `IllegalArgumentException` is thrown to indicate the change could not be made. diff --git a/exercises/practice/change/.approaches/recursive/snippet.txt b/exercises/practice/change/.approaches/recursive/snippet.txt new file mode 100644 index 000000000..64c6f9df8 --- /dev/null +++ b/exercises/practice/change/.approaches/recursive/snippet.txt @@ -0,0 +1,7 @@ +List computeMostEfficientChange(int grandTotal) { + if (remaining == 0) + return List.of(coin); + + return currencyCoins.stream().map(coin -> + new ArrayList<>(computeMostEfficientChange(remaining)).add(coin)); +} \ No newline at end of file diff --git a/exercises/practice/collatz-conjecture/.approaches/introduction.md b/exercises/practice/collatz-conjecture/.approaches/introduction.md index 9965c8ffc..6717d2046 100644 --- a/exercises/practice/collatz-conjecture/.approaches/introduction.md +++ b/exercises/practice/collatz-conjecture/.approaches/introduction.md @@ -1,6 +1,6 @@ # Introduction -There are at east a couple of ways to solve Collatz Conjecture. +There are at least a couple of ways to solve Collatz Conjecture. One approach is to use a [`while`][while-loop] loop to iterate to the answer. Another approach is to use `IntStream.iterate()` to iterate to the answer.