Skip to content

Commit b254f1b

Browse files
committed
Add recursive & bottom up approaches for Change
Also fixed a spelling mistake in collatz conjecture exercise.
1 parent 50ab52c commit b254f1b

File tree

10 files changed

+300
-22
lines changed

10 files changed

+300
-22
lines changed

exercises/practice/change/.approaches/config.json

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,33 @@
77
"approaches": [
88
{
99
"uuid": "d0b615ca-3a02-4d66-ad10-e0c513062189",
10-
"slug": "dynamic-programming",
11-
"title": "Dynamic Programming Approach",
12-
"blurb": "Use dynamic programming to find the most efficient change combination.",
10+
"slug": "dynamic-programming-top-down",
11+
"title": "Dynamic Programming: Top Down",
12+
"blurb": "Break the required amount into smaller amounts and reuse saved results to quickly find the final result.",
1313
"authors": [
1414
"jagdish-15"
1515
],
1616
"contributors": [
1717
"kahgoh"
1818
]
19+
},
20+
{
21+
"uuid": "daf47878-1607-4f22-b2df-1049f3d6802c",
22+
"slug": "dynamic-programming-bottom-up",
23+
"title": "Dynamic Programming: Bottom Up",
24+
"blurb": "Start from the available coins and calculate the amounts that can be made from them.",
25+
"authors": [
26+
"kahgoh"
27+
]
28+
},
29+
{
30+
"uuid": "06ae63ec-5bf3-41a0-89e3-2772e4cdbf5d",
31+
"slug": "recursive",
32+
"title": "Recursive",
33+
"blurb": "Use recursion to recursively find the most efficient change for a given amount.",
34+
"authors": [
35+
"kahgoh"
36+
]
1937
}
2038
]
2139
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Dynamic Programming - Bottom up
2+
3+
```java
4+
import java.util.ArrayDeque;
5+
import java.util.ArrayList;
6+
import java.util.Collections;
7+
import java.util.HashSet;
8+
import java.util.List;
9+
import java.util.Set;
10+
11+
class ChangeCalculator {
12+
13+
private final List<Integer> currencyCoins;
14+
15+
ChangeCalculator(List<Integer> currencyCoins) {
16+
this.currencyCoins = List.copyOf(currencyCoins);
17+
}
18+
19+
List<Integer> computeMostEfficientChange(int grandTotal) {
20+
if (grandTotal < 0) {
21+
throw new IllegalArgumentException("Negative totals are not allowed.");
22+
}
23+
if (grandTotal == 0) {
24+
return Collections.emptyList();
25+
}
26+
Set<Integer> reachableTotals = new HashSet<>();
27+
ArrayDeque<List<Integer>> queue = new ArrayDeque<>(currencyCoins.stream().map(List::of).toList());
28+
29+
while (!queue.isEmpty()) {
30+
List<Integer> next = queue.poll();
31+
int total = next.stream().mapToInt(Integer::intValue).sum();
32+
if (total == grandTotal) {
33+
return next;
34+
}
35+
if (total < grandTotal && reachableTotals.add(total)) {
36+
for (Integer coin : currencyCoins) {
37+
List<Integer> toCheck = new ArrayList<>(next);
38+
toCheck.add(coin);
39+
queue.offer(toCheck);
40+
}
41+
}
42+
}
43+
44+
throw new IllegalArgumentException("The total " + grandTotal + " cannot be represented in the given currency.");
45+
}
46+
}
47+
```
48+
49+
This approach starts from the coins and calculates which amounts can be made up by the coins.
50+
51+
The `grandTotal` is first validated to ensure that it is a positive number greater than 0.
52+
Two data structures are then created:
53+
54+
- a queue to maintain a combination of coins to check
55+
- a set to keep track of the totals from the combinations that have been seen
56+
57+
The queue is initialized with a number of combinations that consist just each of the coins.
58+
For example, if the available coins are 5, 10 and 20, then the queue begins with three combinations:
59+
60+
- the first combination has just 5
61+
- the second has just 10
62+
- the third has just 20
63+
64+
Thus, the queue contains `[[5], [10], [20]]`.
65+
66+
For each combination in the queue, the loop calculates the sum of the combination.
67+
If the sum equals the desired total, it has found the combination.
68+
Otherwise new combinations are added to the queue by adding each of the coins to the end of the combination:
69+
70+
- less than the desired total, and:
71+
- 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)
72+
73+
~~~~exercism/note
74+
If the total has been "seen", there is no need to recheck the amounts because shorter combinations are always checked before longer combinations.
75+
So, if the total is encountered again, we must have found a shorter combination to reach the same amount earlier.
76+
~~~~
77+
78+
Continuing with the above example, the first combination contains just `5`.
79+
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.
80+
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.
81+
82+
The total can not be reached when there are no combinations in the queue.
83+
84+
[set-add]: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Set.html#add(E)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
while (!queue.isEmpty()) {
2+
int total = next.stream().mapToInt(Integer::intValue).sum();
3+
if (total < grandTotal && reachableTotals.add(total)) {
4+
for (Integer coin : currencyCoins) {
5+
queue.add(append(next, coin));
6+
}
7+
}
8+
}

exercises/practice/change/.approaches/dynamic-programming/content.md renamed to exercises/practice/change/.approaches/dynamic-programming-top-down/content.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Dynamic Programming Approach
1+
# Dynamic Programming - Top Down
22

33
```java
44
import java.util.List;
@@ -12,7 +12,7 @@ class ChangeCalculator {
1212
}
1313

1414
List<Integer> computeMostEfficientChange(int grandTotal) {
15-
if (grandTotal < 0)
15+
if (grandTotal < 0)
1616
throw new IllegalArgumentException("Negative totals are not allowed.");
1717

1818
List<List<Integer>> coinsUsed = new ArrayList<>(grandTotal + 1);
@@ -64,5 +64,5 @@ It minimizes the number of coins needed by breaking down the problem into smalle
6464
## Time and Space Complexity
6565

6666
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`.
67-
67+
6868
The space complexity is **O(n)** due to the list `coinsUsed`, which stores the most efficient coin combination for each total up to `grandTotal`.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
for (int i = 1; i <= grandTotal; i++) {
2+
for (int coin: currencyCoins) {
3+
List<Integer> currentCombination = coinsUsed.get(i - coin).add(coin);
4+
if (bestCombination == null || currentCombination.size() < bestCombination.size())
5+
bestCombination = currentCombination;
6+
}
7+
coinsUsed.add(bestCombination);
8+
}

exercises/practice/change/.approaches/dynamic-programming/snippet.txt

Lines changed: 0 additions & 8 deletions
This file was deleted.

exercises/practice/change/.approaches/introduction.md

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,62 @@
1-
# Introduction
1+
# Introduction
22

3-
There is an idiomatic approach to solving "Change."
4-
You can use [dynamic programming][dynamic-programming] to calculate the minimum number of coins required for a given total.
3+
There are a couple of different ways to solve "Change".
4+
The [recursive approach][approach-recursive] uses recursion to find most efficient change for remaining amounts assuming a coin is included.
5+
[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]).
56

67
## General guidance
78

89
The key to solving "Change" is understanding that not all totals can be reached with the available coin denominations.
910
The solution needs to figure out which totals can be achieved and how to combine the coins optimally.
1011

11-
## Approach: Dynamic Programming
12+
## Approach: Recursive
13+
14+
```java
15+
import java.util.ArrayList;
16+
import java.util.Collections;
17+
import java.util.Comparator;
18+
import java.util.List;
19+
20+
class ChangeCalculator {
21+
22+
private final List<Integer> currencyCoins;
23+
24+
ChangeCalculator(List<Integer> currencyCoins) {
25+
this.currencyCoins = List.copyOf(currencyCoins);
26+
}
27+
28+
List<Integer> computeMostEfficientChange(int grandTotal) {
29+
if (grandTotal < 0) {
30+
throw new IllegalArgumentException("Negative totals are not allowed.");
31+
}
32+
if (grandTotal == 0) {
33+
return Collections.emptyList();
34+
}
35+
36+
return currencyCoins.stream().map(coin -> {
37+
int remaining = grandTotal - coin;
38+
if (remaining == 0) {
39+
return List.of(coin);
40+
}
41+
42+
try {
43+
List<Integer> result = new ArrayList<>(computeMostEfficientChange(remaining));
44+
result.add(coin);
45+
result.sort(Integer::compare);
46+
return result;
47+
} catch (IllegalArgumentException e) {
48+
return Collections.<Integer>emptyList();
49+
}
50+
})
51+
.filter(c -> !c.isEmpty())
52+
.min(Comparator.comparingInt(List::size))
53+
.orElseThrow(() -> new IllegalArgumentException("The total " + grandTotal + " cannot be represented in the given currency."));
54+
55+
}
56+
}
57+
```
58+
59+
## Approach: Dynamic Programming - Top down
1260

1361
```java
1462
import java.util.List;
@@ -22,7 +70,7 @@ class ChangeCalculator {
2270
}
2371

2472
List<Integer> computeMostEfficientChange(int grandTotal) {
25-
if (grandTotal < 0)
73+
if (grandTotal < 0)
2674
throw new IllegalArgumentException("Negative totals are not allowed.");
2775

2876
List<List<Integer>> coinsUsed = new ArrayList<>(grandTotal + 1);
@@ -49,7 +97,64 @@ class ChangeCalculator {
4997
}
5098
```
5199

52-
For a detailed look at the code and logic, see the full explanation in the [Dynamic Programming Approach][approach-dynamic-programming].
100+
For a detailed look at the code and logic, see the full explanation in the [Dynamic Programming - Top Down][approach-dynamic-programming-top-down].
101+
102+
## Approach: Dyanmic Programming - Bottom up
103+
104+
```java
105+
import java.util.ArrayDeque;
106+
import java.util.ArrayList;
107+
import java.util.Collections;
108+
import java.util.HashSet;
109+
import java.util.List;
110+
import java.util.Set;
111+
112+
class ChangeCalculator {
113+
114+
private final List<Integer> currencyCoins;
115+
116+
ChangeCalculator(List<Integer> currencyCoins) {
117+
this.currencyCoins = List.copyOf(currencyCoins);
118+
}
119+
120+
List<Integer> computeMostEfficientChange(int grandTotal) {
121+
if (grandTotal < 0) {
122+
throw new IllegalArgumentException("Negative totals are not allowed.");
123+
}
124+
if (grandTotal == 0) {
125+
return Collections.emptyList();
126+
}
127+
Set<Integer> reachableTotals = new HashSet<>();
128+
ArrayDeque<List<Integer>> queue = new ArrayDeque<>(currencyCoins.stream().map(List::of).toList());
129+
130+
while (!queue.isEmpty()) {
131+
List<Integer> next = queue.poll();
132+
int total = next.stream().mapToInt(Integer::intValue).sum();
133+
if (total == grandTotal) {
134+
return next;
135+
}
136+
if (total < grandTotal && reachableTotals.add(total)) {
137+
for (Integer coin : currencyCoins) {
138+
List<Integer> toCheck = new ArrayList<>(next);
139+
toCheck.add(coin);
140+
queue.offer(toCheck);
141+
}
142+
}
143+
}
144+
145+
throw new IllegalArgumentException("The total " + grandTotal + " cannot be represented in the given currency.");
146+
}
147+
}
148+
```
149+
150+
For a detailed look at the code and logic, see the full explanation in the [Dynamic Programming - Bottom Up][approach-dynamic-programming-bottom-up].
151+
152+
## Which approach to use?
153+
154+
The recursive approach is generally inefficient compared to either dynamic programming approach because the recursion requires recalculating the most efficient change for certain amounts.
155+
Both dynamic programming approaches avoids this by building on the results computed previously at each step.
53156

54-
[approach-dynamic-programming]: https://exercism.org/tracks/java/exercises/change/approaches/dynamic-programming
157+
[approach-recursive]: https://exercism.org/tracks/java/exercises/change/approaches/recursive
158+
[approach-dynamic-programming-top-down]: https://exercism.org/tracks/java/exercises/change/approaches/dynamic-programming-top-down
159+
[approach-dynamic-programming-bottom-up]: https://exercism.org/tracks/java/exercises/change/approaches/dynamic-programming-bottom-up
55160
[dynamic-programming]: https://en.wikipedia.org/wiki/Dynamic_programming
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Recursive
2+
3+
```java
4+
import java.util.ArrayList;
5+
import java.util.Collections;
6+
import java.util.Comparator;
7+
import java.util.List;
8+
9+
class ChangeCalculator {
10+
11+
private final List<Integer> currencyCoins;
12+
13+
ChangeCalculator(List<Integer> currencyCoins) {
14+
this.currencyCoins = List.copyOf(currencyCoins);
15+
}
16+
17+
List<Integer> computeMostEfficientChange(int grandTotal) {
18+
if (grandTotal < 0) {
19+
throw new IllegalArgumentException("Negative totals are not allowed.");
20+
}
21+
if (grandTotal == 0) {
22+
return Collections.emptyList();
23+
}
24+
25+
return currencyCoins.stream().map(coin -> {
26+
int remaining = grandTotal - coin;
27+
if (remaining == 0) {
28+
return List.of(coin);
29+
}
30+
31+
try {
32+
List<Integer> result = new ArrayList<>(computeMostEfficientChange(remaining));
33+
result.add(coin);
34+
result.sort(Integer::compare);
35+
return result;
36+
} catch (IllegalArgumentException e) {
37+
return Collections.<Integer>emptyList();
38+
}
39+
})
40+
.filter(c -> !c.isEmpty())
41+
.min(Comparator.comparingInt(List::size))
42+
.orElseThrow(() -> new IllegalArgumentException("The total " + grandTotal + " cannot be represented in the given currency."));
43+
44+
}
45+
}
46+
```
47+
48+
The recursive approach works by iterating through the available coins and recursively calling itself to find the most efficient change with it.
49+
It starts by validating the `grandTotal` argument.
50+
If valid, use a stream to go through the available coins and determines how much change is still required if the coin is included.
51+
If no more change is required, the most efficient change consists simply of the coin on its own.
52+
Otherwise it will recursively call itself to find the most efficient change for the remaining amount.
53+
The recursive call is done in a `try-catch` block because the method throws an `IllegalArgumentionException` if the change can not be made.
54+
An empty list is used to indicate when the change can not be made in the stream.
55+
The stream filters out the empty list in the next step before finding the smallest list.
56+
If the stream is empty, an `IllegalArgumentException` is thrown to indicate the change could not be made.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
List<Integer> computeMostEfficientChange(int grandTotal) {
2+
if (remaining == 0)
3+
return List.of(coin);
4+
5+
return currencyCoins.stream().map(coin ->
6+
new ArrayList<>(computeMostEfficientChange(remaining)).add(coin));
7+
}

exercises/practice/collatz-conjecture/.approaches/introduction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Introduction
22

3-
There are at east a couple of ways to solve Collatz Conjecture.
3+
There are at least a couple of ways to solve Collatz Conjecture.
44
One approach is to use a [`while`][while-loop] loop to iterate to the answer.
55
Another approach is to use `IntStream.iterate()` to iterate to the answer.
66

0 commit comments

Comments
 (0)