Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions exercises/practice/change/.approaches/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -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<Integer> currencyCoins;

ChangeCalculator(List<Integer> currencyCoins) {
this.currencyCoins = List.copyOf(currencyCoins);
}

List<Integer> computeMostEfficientChange(int grandTotal) {
if (grandTotal < 0) {
throw new IllegalArgumentException("Negative totals are not allowed.");
}
if (grandTotal == 0) {
return Collections.emptyList();
}
Set<Integer> reachableTotals = new HashSet<>();
ArrayDeque<List<Integer>> queue = new ArrayDeque<>(currencyCoins.stream().map(List::of).toList());

while (!queue.isEmpty()) {
List<Integer> 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<Integer> 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)
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Dynamic Programming Approach
# Dynamic Programming - Top Down

```java
import java.util.List;
Expand All @@ -12,7 +12,7 @@ class ChangeCalculator {
}

List<Integer> computeMostEfficientChange(int grandTotal) {
if (grandTotal < 0)
if (grandTotal < 0)
throw new IllegalArgumentException("Negative totals are not allowed.");

List<List<Integer>> coinsUsed = new ArrayList<>(grandTotal + 1);
Expand Down Expand Up @@ -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`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
for (int i = 1; i <= grandTotal; i++) {
for (int coin: currencyCoins) {
List<Integer> currentCombination = coinsUsed.get(i - coin).add(coin);
if (bestCombination == null || currentCombination.size() < bestCombination.size())
bestCombination = currentCombination;
}
coinsUsed.add(bestCombination);
}

This file was deleted.

119 changes: 112 additions & 7 deletions exercises/practice/change/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -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<Integer> currencyCoins;

ChangeCalculator(List<Integer> currencyCoins) {
this.currencyCoins = List.copyOf(currencyCoins);
}

List<Integer> 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<Integer> result = new ArrayList<>(computeMostEfficientChange(remaining));
result.add(coin);
result.sort(Integer::compare);
return result;
} catch (IllegalArgumentException e) {
return Collections.<Integer>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;
Expand All @@ -22,7 +70,7 @@ class ChangeCalculator {
}

List<Integer> computeMostEfficientChange(int grandTotal) {
if (grandTotal < 0)
if (grandTotal < 0)
throw new IllegalArgumentException("Negative totals are not allowed.");

List<List<Integer>> coinsUsed = new ArrayList<>(grandTotal + 1);
Expand All @@ -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<Integer> currencyCoins;

ChangeCalculator(List<Integer> currencyCoins) {
this.currencyCoins = List.copyOf(currencyCoins);
}

List<Integer> computeMostEfficientChange(int grandTotal) {
if (grandTotal < 0) {
throw new IllegalArgumentException("Negative totals are not allowed.");
}
if (grandTotal == 0) {
return Collections.emptyList();
}
Set<Integer> reachableTotals = new HashSet<>();
ArrayDeque<List<Integer>> queue = new ArrayDeque<>(currencyCoins.stream().map(List::of).toList());

while (!queue.isEmpty()) {
List<Integer> 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<Integer> 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
56 changes: 56 additions & 0 deletions exercises/practice/change/.approaches/recursive/content.md
Original file line number Diff line number Diff line change
@@ -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<Integer> currencyCoins;

ChangeCalculator(List<Integer> currencyCoins) {
this.currencyCoins = List.copyOf(currencyCoins);
}

List<Integer> 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<Integer> result = new ArrayList<>(computeMostEfficientChange(remaining));
result.add(coin);
result.sort(Integer::compare);
return result;
} catch (IllegalArgumentException e) {
return Collections.<Integer>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.
7 changes: 7 additions & 0 deletions exercises/practice/change/.approaches/recursive/snippet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
List<Integer> computeMostEfficientChange(int grandTotal) {
if (remaining == 0)
return List.of(coin);

return currencyCoins.stream().map(coin ->
new ArrayList<>(computeMostEfficientChange(remaining)).add(coin));
}
Original file line number Diff line number Diff line change
@@ -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.

Expand Down