1+ package com .williamfiset .algorithms .dp ;
2+
3+ import java .util .ArrayList ;
4+ import java .util .Arrays ;
5+ import java .util .List ;
6+ import java .util .Optional ;
7+
18/**
2- * The coin change problem is an unbounded knapsack problem variant. The problem asks you to find
3- * the minimum number of coins required for a certain amount of change given the coin denominations.
4- * You may use each coin denomination as many times as you please.
9+ * Coin Change Problem (Unbounded Knapsack Variant)
10+ *
11+ * Given a set of coin denominations and a target amount, find the minimum
12+ * number of coins needed to make that amount. Each coin denomination may
13+ * be used unlimited times.
14+ *
15+ * Three implementations are provided:
516 *
6- * <p>Tested against: https://leetcode.com/problems/coin-change
17+ * 1. coinChange() — 2D DP table, O(m*n) time/space, recovers selected coins
18+ * 2. coinChangeSpaceEfficient() — 1D DP array, O(m*n) time, O(n) space, recovers selected coins
19+ * 3. coinChangeRecursive() — top-down with memoization, skips unreachable states
720 *
8- * <p>Run locally:
21+ * Where m = number of coin denominations, n = target amount.
922 *
10- * <p>bazel run //src/main/java/ com/williamfiset/algorithms/dp:CoinChange
23+ * Tested against: https://leetcode. com/problems/coin-change
1124 *
1225 * @author William Fiset, william.alexandre.fiset@gmail.com
1326 */
14- package com .williamfiset .algorithms .dp ;
15-
16- import java .util .ArrayList ;
17- import java .util .List ;
18- import java .util .Optional ;
19-
2027public class CoinChange {
2128
29+ /**
30+ * Holds the result of a coin change computation: the minimum number of coins
31+ * (if a solution exists) and the actual coins selected.
32+ */
2233 public static class Solution {
23- // Contains the minimum number of coins to make a certain amount, if a solution exists.
34+ /** The minimum number of coins to make the target amount, or empty if impossible. */
2435 Optional <Integer > minCoins = Optional .empty ();
2536
26- // The coins selected as part of the optimal solution.
27- List <Integer > selectedCoins = new ArrayList <Integer >();
37+ /** The coins selected as part of the optimal solution. */
38+ List <Integer > selectedCoins = new ArrayList <>();
2839 }
2940
30- // TODO(william): setting an explicit infinity could lead to a wrong answer for
31- // very large values. Prefer to use null instead.
32- private static final int INF = Integer .MAX_VALUE / 2 ;
33-
41+ // ==================== Implementation 1: 2D DP table ====================
42+
43+ /**
44+ * Solves coin change using a 2D DP table.
45+ *
46+ * dp[i][j] = minimum coins needed to make amount j using the first i coin types.
47+ * After computing the table, backtracks to recover which coins were selected.
48+ *
49+ * @param coins array of coin denominations (all positive)
50+ * @param n the target amount
51+ * @return a Solution containing the min coin count and selected coins
52+ *
53+ * Time: O(m*n) where m = coins.length
54+ * Space: O(m*n)
55+ */
3456 public static Solution coinChange (int [] coins , final int n ) {
3557 if (coins == null ) throw new IllegalArgumentException ("Coins array is null" );
3658 if (coins .length == 0 ) throw new IllegalArgumentException ("No coin values :/" );
@@ -41,40 +63,41 @@ public static Solution coinChange(int[] coins, final int n) {
4163 }
4264
4365 final int m = coins .length ;
44- // Initialize table and set first row to be infinity
45- int [][] dp = new int [m + 1 ][n + 1 ];
46- java .util .Arrays .fill (dp [0 ], INF );
47- dp [1 ][0 ] = 0 ;
4866
49- // Iterate through all the coins
67+ // dp[i][j] = min coins using first i denominations to make amount j.
68+ // Row 0 is a sentinel: no coins available, so everything is impossible (null).
69+ // Column 0 is always 0: it takes 0 coins to make amount 0.
70+ Integer [][] dp = new Integer [m + 1 ][n + 1 ];
71+ for (int i = 0 ; i <= m ; i ++)
72+ dp [i ][0 ] = 0 ;
73+
5074 for (int i = 1 ; i <= m ; i ++) {
5175 int coinValue = coins [i - 1 ];
5276 for (int j = 1 ; j <= n ; j ++) {
5377
54- // Consider not selecting this coin
78+ // Option 1: don't use coin i — carry forward from previous row
5579 dp [i ][j ] = dp [i - 1 ][j ];
5680
57- // Try selecting this coin if it's better
58- if (j - coinValue >= 0 && dp [i ][j - coinValue ] + 1 < dp [i ][j ]) {
59- dp [i ][j ] = dp [i ][j - coinValue ] + 1 ;
81+ // Option 2: use coin i if it fits and yields fewer coins
82+ if (j - coinValue >= 0 && dp [i ][j - coinValue ] != null ) {
83+ int withCoin = dp [i ][j - coinValue ] + 1 ;
84+ if (dp [i ][j ] == null || withCoin < dp [i ][j ]) {
85+ dp [i ][j ] = withCoin ;
86+ }
6087 }
6188 }
6289 }
6390
64- // p(dp);
65-
6691 Solution solution = new Solution ();
6792
68- if (dp [m ][n ] != INF ) {
69- solution .minCoins = Optional .of (dp [m ][n ]);
70- } else {
71- return solution ;
72- }
93+ if (dp [m ][n ] == null ) return solution ;
94+ solution .minCoins = Optional .of (dp [m ][n ]);
7395
96+ // Backtrack to recover selected coins
7497 for (int change = n , coinIndex = m ; coinIndex > 0 ; ) {
7598 int coinValue = coins [coinIndex - 1 ];
76- boolean canSelectCoin = change - coinValue >= 0 ;
77- if (canSelectCoin && dp [coinIndex ][change - coinValue ] < dp [coinIndex ][change ]) {
99+ boolean canSelect = change - coinValue >= 0 && dp [ coinIndex ][ change - coinValue ] != null ;
100+ if (canSelect && dp [coinIndex ][change - coinValue ] < dp [coinIndex ][change ]) {
78101 solution .selectedCoins .add (coinValue );
79102 change -= coinValue ;
80103 } else {
@@ -85,55 +108,82 @@ public static Solution coinChange(int[] coins, final int n) {
85108 return solution ;
86109 }
87110
111+ // ==================== Implementation 2: Space-efficient 1D DP ====================
112+
113+ /**
114+ * Solves coin change using a space-efficient 1D DP array.
115+ *
116+ * dp[j] = minimum coins needed to make amount j using any denomination.
117+ * After computing, backtracks greedily to recover selected coins.
118+ *
119+ * Compare with coinChange(): same time complexity but uses O(n) space
120+ * instead of O(m*n) by collapsing the coin dimension.
121+ *
122+ * @param coins array of coin denominations (all positive)
123+ * @param n the target amount
124+ * @return a Solution containing the min coin count and selected coins
125+ *
126+ * Time: O(m*n)
127+ * Space: O(n)
128+ */
88129 public static Solution coinChangeSpaceEfficient (int [] coins , int n ) {
89130 if (coins == null ) throw new IllegalArgumentException ("Coins array is null" );
90131
91- // Initialize table and set everything to infinity except first cell
92- int [] dp = new int [n + 1 ];
93- java .util .Arrays .fill (dp , INF );
132+ // dp[j] = min coins to make amount j, null means impossible
133+ Integer [] dp = new Integer [n + 1 ];
94134 dp [0 ] = 0 ;
95135
96136 for (int i = 1 ; i <= n ; i ++) {
97137 for (int coin : coins ) {
98- if (i - coin < 0 ) {
99- continue ;
100- }
101- if ( dp [ i - coin ] + 1 < dp [i ]) {
102- dp [ i ] = dp [ i - coin ] + 1 ;
138+ if (i - coin >= 0 && dp [ i - coin ] != null ) {
139+ int withCoin = dp [ i - coin ] + 1 ;
140+ if ( dp [ i ] == null || withCoin < dp [ i ]) {
141+ dp [i ] = withCoin ;
142+ }
103143 }
104144 }
105145 }
106146
107147 Solution solution = new Solution ();
108- if (dp [n ] != INF ) {
109- solution .minCoins = Optional .of (dp [n ]);
110- } else {
111- return solution ;
112- }
148+ if (dp [n ] == null ) return solution ;
149+ solution .minCoins = Optional .of (dp [n ]);
113150
151+ // Backtrack greedily: at each amount, pick the coin that leads to the fewest coins
114152 for (int i = n ; i > 0 ; ) {
115- int selectedCoinValue = INF ;
116- int cellWithFewestCoins = dp [i ];
153+ int bestCoin = - 1 ;
154+ int bestCount = dp [i ];
117155 for (int coin : coins ) {
118- if (i - coin < 0 ) {
119- continue ;
120- }
121- if (dp [i - coin ] < cellWithFewestCoins ) {
122- cellWithFewestCoins = dp [i - coin ];
123- selectedCoinValue = coin ;
156+ if (i - coin >= 0 && dp [i - coin ] != null && dp [i - coin ] < bestCount ) {
157+ bestCount = dp [i - coin ];
158+ bestCoin = coin ;
124159 }
125160 }
126- solution .selectedCoins .add (selectedCoinValue );
127- i -= selectedCoinValue ;
161+ solution .selectedCoins .add (bestCoin );
162+ i -= bestCoin ;
128163 }
129164
130- // Return the minimum number of coins needed
131165 return solution ;
132166 }
133167
134- // The recursive approach has the advantage that it does not have to visit
135- // all possible states like the tabular approach does. This can speedup
136- // things especially if the coin denominations are large.
168+ // ==================== Implementation 3: Top-down recursive with memoization ====================
169+
170+ /**
171+ * Solves coin change using top-down recursion with memoization.
172+ *
173+ * Unlike the two bottom-up implementations above, the recursive approach
174+ * only visits states reachable from the target amount. This can be faster
175+ * when coin denominations are large (many states are skipped).
176+ *
177+ * Note: returns -1 instead of Optional.empty() for impossible cases,
178+ * and does not recover the selected coins.
179+ *
180+ * @param coins array of coin denominations (all positive)
181+ * @param n the target amount
182+ * @return the minimum number of coins, or -1 if impossible
183+ *
184+ * Time: O(m*n)
185+ * Space: O(n)
186+ */
137187 public static int coinChangeRecursive (int [] coins , int n ) {
138188 if (coins == null ) throw new IllegalArgumentException ("Coins array is null" );
139189 if (n < 0 ) return -1 ;
@@ -142,77 +192,62 @@ public static int coinChangeRecursive(int[] coins, int n) {
142192 return coinChangeRecursive (n , coins , dp );
143193 }
144194
145- // Private helper method to actually go the recursion
146195 private static int coinChangeRecursive (int n , int [] coins , int [] dp ) {
147196 if (n < 0 ) return -1 ;
148197 if (n == 0 ) return 0 ;
149198 if (dp [n ] != 0 ) return dp [n ];
150199
151- int minCoins = INF ;
152- for (int coinValue : coins ) {
153- int value = coinChangeRecursive (n - coinValue , coins , dp );
154- if (value != -1 && value < minCoins ) minCoins = value + 1 ;
200+ int minCoins = Integer .MAX_VALUE ;
201+ for (int coin : coins ) {
202+ int value = coinChangeRecursive (n - coin , coins , dp );
203+ if (value != -1 && value < minCoins )
204+ minCoins = value + 1 ;
155205 }
156206
157- // If we weren't able to find some coins to make our
158- // amount then cache -1 as the answer.
159- return dp [n ] = (minCoins == INF ) ? -1 : minCoins ;
160- }
161-
162- // DP table print function. Used for debugging.
163- private static void p (int [][] dp ) {
164- for (int [] r : dp ) {
165- for (int v : r ) {
166- System .out .printf ("%4d, " , v == INF ? -1 : v );
167- }
168- System .out .println ();
169- }
170- }
171-
172- private static void p (int [] dp ) {
173- for (int v : dp ) {
174- System .out .printf ("%4d, " , v == INF ? -1 : v );
175- }
176- System .out .println ();
207+ // Cache -1 if no combination of coins can make this amount
208+ return dp [n ] = (minCoins == Integer .MAX_VALUE ) ? -1 : minCoins ;
177209 }
178210
179211 public static void main (String [] args ) {
180- // example1();
181- // example2();
182- // example3();
212+ example1 ();
213+ example2 ();
214+ example3 ();
183215 example4 ();
184216 }
185217
186- private static void example4 () {
187- int n = 11 ;
188- int [] coins = {2 , 4 , 1 };
189- // System.out.println(coinChange(coins, n).minCoins);
190- System .out .println (coinChangeSpaceEfficient (coins , n ));
191- // System.out.println(coinChangeRecursive(coins, n));
192- // System.out.println(coinChange(coins, n).selectedCoins);
193- }
194-
195218 private static void example1 () {
196219 int [] coins = {2 , 6 , 1 };
197- System .out .println (coinChange (coins , 17 ).minCoins );
198- System .out .println (coinChange (coins , 17 ).selectedCoins );
199- System .out .println (coinChangeSpaceEfficient (coins , 17 ));
200- System .out .println (coinChangeRecursive (coins , 17 ));
220+ System .out .println ("--- coins={2,6,1}, amount=17 ---" );
221+ System .out .println ("2D DP: " + coinChange (coins , 17 ).minCoins ); // Optional[4]
222+ System .out .println (" selected: " + coinChange (coins , 17 ).selectedCoins ); // [6, 6, 2, 2, 1]
223+ System .out .println ("1D DP: " + coinChangeSpaceEfficient (coins , 17 ).minCoins ); // Optional[4]
224+ System .out .println ("Recursive: " + coinChangeRecursive (coins , 17 )); // 4
201225 }
202226
203227 private static void example2 () {
204228 int [] coins = {2 , 3 , 5 };
205- System .out .println (coinChange (coins , 12 ).minCoins );
206- System .out .println (coinChange (coins , 12 ).selectedCoins );
207- System .out .println (coinChangeSpaceEfficient (coins , 12 ));
208- System .out .println (coinChangeRecursive (coins , 12 ));
229+ System .out .println ("--- coins={2,3,5}, amount=12 ---" );
230+ System .out .println ("2D DP: " + coinChange (coins , 12 ).minCoins ); // Optional[3]
231+ System .out .println (" selected: " + coinChange (coins , 12 ).selectedCoins ); // [5, 5, 2]
232+ System .out .println ("1D DP: " + coinChangeSpaceEfficient (coins , 12 ).minCoins ); // Optional[3]
233+ System .out .println ("Recursive: " + coinChangeRecursive (coins , 12 )); // 3
209234 }
210235
211236 private static void example3 () {
212237 int [] coins = {3 , 4 , 7 };
213- System .out .println (coinChange (coins , 17 ).minCoins );
214- System .out .println (coinChange (coins , 17 ).selectedCoins );
215- System .out .println (coinChangeSpaceEfficient (coins , 17 ));
216- System .out .println (coinChangeRecursive (coins , 17 ));
238+ System .out .println ("--- coins={3,4,7}, amount=17 ---" );
239+ System .out .println ("2D DP: " + coinChange (coins , 17 ).minCoins ); // Optional[3]
240+ System .out .println (" selected: " + coinChange (coins , 17 ).selectedCoins ); // [7, 7, 3]
241+ System .out .println ("1D DP: " + coinChangeSpaceEfficient (coins , 17 ).minCoins ); // Optional[3]
242+ System .out .println ("Recursive: " + coinChangeRecursive (coins , 17 )); // 3
243+ }
244+
245+ private static void example4 () {
246+ int [] coins = {2 , 4 , 1 };
247+ System .out .println ("--- coins={2,4,1}, amount=11 ---" );
248+ System .out .println ("2D DP: " + coinChange (coins , 11 ).minCoins ); // Optional[4]
249+ System .out .println (" selected: " + coinChange (coins , 11 ).selectedCoins ); // [4, 4, 2, 1]
250+ System .out .println ("1D DP: " + coinChangeSpaceEfficient (coins , 11 ).minCoins ); // Optional[4]
251+ System .out .println ("Recursive: " + coinChangeRecursive (coins , 11 )); // 4
217252 }
218253}
0 commit comments