From 6e3055664b9dadc89c735049c68955d7970a0059 Mon Sep 17 00:00:00 2001 From: Truong Nhan Nguyen Date: Sat, 29 Jun 2024 19:45:52 +0700 Subject: [PATCH 1/4] ref: refactor coin change - Use Rust closures to streamline the implementation - Rewrite tests using macro --- src/dynamic_programming/coin_change.rs | 118 ++++++++++++++----------- 1 file changed, 64 insertions(+), 54 deletions(-) diff --git a/src/dynamic_programming/coin_change.rs b/src/dynamic_programming/coin_change.rs index 84e4ab26323..419baed2441 100644 --- a/src/dynamic_programming/coin_change.rs +++ b/src/dynamic_programming/coin_change.rs @@ -1,70 +1,80 @@ -/// Coin change via Dynamic Programming +//! This module provides a solution to the coin change problem using dynamic programming. +//! The `coin_change` function calculates the fewest number of coins required to make up +//! a given amount using a specified set of coin denominations. +//! +//! The implementation leverages dynamic programming to build up solutions for smaller +//! amounts and combines them to solve for larger amounts. It ensures optimal substructure +//! and overlapping subproblems are efficiently utilized to achieve the solution. -/// coin_change(coins, amount) returns the fewest number of coins that need to make up that amount. -/// If that amount of money cannot be made up by any combination of the coins, return `None`. +//! # Complexity +//! - Time complexity: O(amount * coins.length) +//! - Space complexity: O(amount) + +/// Returns the fewest number of coins needed to make up the given amount using the provided coin denominations. +/// If the amount cannot be made up by any combination of the coins, returns `None`. +/// +/// # Arguments +/// * `coins` - A slice of coin denominations. +/// * `amount` - The total amount of money to be made up. +/// +/// # Returns +/// * `Option` - The minimum number of coins required to make up the amount, or `None` if it's not possible. /// -/// # Arguments: -/// * `coins` - coins of different denominations -/// * `amount` - a total amount of money be made up. /// # Complexity -/// - time complexity: O(amount * coins.length), -/// - space complexity: O(amount), +/// * Time complexity: O(amount * coins.length) +/// * Space complexity: O(amount) pub fn coin_change(coins: &[usize], amount: usize) -> Option { - let mut dp = vec![None; amount + 1]; - dp[0] = Some(0); + let mut min_coins = vec![None; amount + 1]; + min_coins[0] = Some(0); - // Assume dp[i] is the fewest number of coins making up amount i, - // then for every coin in coins, dp[i] = min(dp[i - coin] + 1). - for i in 0..=amount { - for &coin in coins { - if i >= coin { - dp[i] = match dp[i - coin] { - Some(prev_coins) => match dp[i] { - Some(curr_coins) => Some(curr_coins.min(prev_coins + 1)), - None => Some(prev_coins + 1), - }, - None => dp[i], - }; - } - } - } + (0..=amount).for_each(|current_amount| { + coins + .iter() + .filter(|&&coin| current_amount >= coin) + .for_each(|&coin| { + if let Some(previous_min_coins) = min_coins[current_amount - coin] { + min_coins[current_amount] = Some( + min_coins[current_amount] + .map_or(previous_min_coins + 1, |current_min_coins| { + current_min_coins.min(previous_min_coins + 1) + }), + ); + } + }); + }); - dp[amount] + min_coins[amount] } #[cfg(test)] mod tests { use super::*; - #[test] - fn basic() { - // 11 = 5 * 2 + 1 * 1 - let coins = vec![1, 2, 5]; - assert_eq!(Some(3), coin_change(&coins, 11)); - - // 119 = 11 * 10 + 7 * 1 + 2 * 1 - let coins = vec![2, 3, 5, 7, 11]; - assert_eq!(Some(12), coin_change(&coins, 119)); - } - - #[test] - fn coins_empty() { - let coins = vec![]; - assert_eq!(None, coin_change(&coins, 1)); - } - - #[test] - fn amount_zero() { - let coins = vec![1, 2, 3]; - assert_eq!(Some(0), coin_change(&coins, 0)); + macro_rules! coin_change_tests { + ($($name:ident: $test_case:expr,)*) => { + $( + #[test] + fn $name() { + let (coins, amount, expected) = $test_case; + assert_eq!(expected, coin_change(&coins, amount)); + } + )* + } } - #[test] - fn fail_change() { - // 3 can't be change by 2. - let coins = vec![2]; - assert_eq!(None, coin_change(&coins, 3)); - let coins = vec![10, 20, 50, 100]; - assert_eq!(None, coin_change(&coins, 5)); + coin_change_tests! { + test_basic_case: (vec![1, 2, 5], 11, Some(3)), + test_multiple_denominations: (vec![2, 3, 5, 7, 11], 119, Some(12)), + test_empty_coins: (vec![], 1, None), + test_zero_amount: (vec![1, 2, 3], 0, Some(0)), + test_no_solution_small_coin: (vec![2], 3, None), + test_no_solution_large_coin: (vec![10, 20, 50, 100], 5, None), + test_single_coin_large_amount: (vec![1], 100, Some(100)), + test_large_amount_multiple_coins: (vec![1, 2, 5], 10000, Some(2000)), + test_no_combination_possible: (vec![3, 7], 5, None), + test_exact_combination: (vec![1, 3, 4], 6, Some(2)), + test_large_denomination_multiple_coins: (vec![10, 50, 100], 1000, Some(10)), + test_small_amount_not_possible: (vec![5, 10], 1, None), + test_non_divisible_amount: (vec![2], 3, None), } } From ab7e0f407e2a3a204efb6d8e2e32cb6e7815804f Mon Sep 17 00:00:00 2001 From: Truong Nhan Nguyen Date: Thu, 1 Aug 2024 12:39:52 +0700 Subject: [PATCH 2/4] feat(tests): add some edge tests --- src/dynamic_programming/coin_change.rs | 27 ++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/dynamic_programming/coin_change.rs b/src/dynamic_programming/coin_change.rs index 419baed2441..78c9860565b 100644 --- a/src/dynamic_programming/coin_change.rs +++ b/src/dynamic_programming/coin_change.rs @@ -27,17 +27,16 @@ pub fn coin_change(coins: &[usize], amount: usize) -> Option { let mut min_coins = vec![None; amount + 1]; min_coins[0] = Some(0); - (0..=amount).for_each(|current_amount| { + (0..=amount).for_each(|curr_amount| { coins .iter() - .filter(|&&coin| current_amount >= coin) + .filter(|&&coin| curr_amount >= coin) .for_each(|&coin| { - if let Some(previous_min_coins) = min_coins[current_amount - coin] { - min_coins[current_amount] = Some( - min_coins[current_amount] - .map_or(previous_min_coins + 1, |current_min_coins| { - current_min_coins.min(previous_min_coins + 1) - }), + if let Some(prev_min_coins) = min_coins[curr_amount - coin] { + min_coins[curr_amount] = Some( + min_coins[curr_amount].map_or(prev_min_coins + 1, |curr_min_coins| { + curr_min_coins.min(prev_min_coins + 1) + }), ); } }); @@ -76,5 +75,17 @@ mod tests { test_large_denomination_multiple_coins: (vec![10, 50, 100], 1000, Some(10)), test_small_amount_not_possible: (vec![5, 10], 1, None), test_non_divisible_amount: (vec![2], 3, None), + test_all_multiples: (vec![1, 2, 4, 8], 15, Some(4)), + test_large_amount_mixed_coins: (vec![1, 5, 10, 25], 999, Some(45)), + test_prime_coins_and_amount: (vec![2, 3, 5, 7], 17, Some(3)), + test_coins_larger_than_amount: (vec![5, 10, 20], 1, None), + test_repeating_denominations: (vec![1, 1, 1, 5], 8, Some(4)), + test_non_standard_denominations: (vec![1, 4, 6, 9], 15, Some(2)), + test_very_large_denominations: (vec![1000, 2000, 5000], 1, None), + test_large_amount_performance: (vec![1, 5, 10, 25, 50, 100, 200, 500], 9999, Some(29)), + test_powers_of_two: (vec![1, 2, 4, 8, 16, 32, 64], 127, Some(7)), + test_fibonacci_sequence: (vec![1, 2, 3, 5, 8, 13, 21, 34], 55, Some(2)), + test_mixed_small_large: (vec![1, 100, 1000, 10000], 11001, Some(3)), + test_impossible_combinations: (vec![2, 4, 6, 8], 7, None), } } From c512f1e215610e3ec173464691ac1198d78cc248 Mon Sep 17 00:00:00 2001 From: vil02 <65706193+vil02@users.noreply.github.com> Date: Thu, 1 Aug 2024 08:29:09 +0200 Subject: [PATCH 3/4] tests: add `test_greedy_approach_does_not_work` --- src/dynamic_programming/coin_change.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dynamic_programming/coin_change.rs b/src/dynamic_programming/coin_change.rs index 78c9860565b..033ef08ff3d 100644 --- a/src/dynamic_programming/coin_change.rs +++ b/src/dynamic_programming/coin_change.rs @@ -87,5 +87,6 @@ mod tests { test_fibonacci_sequence: (vec![1, 2, 3, 5, 8, 13, 21, 34], 55, Some(2)), test_mixed_small_large: (vec![1, 100, 1000, 10000], 11001, Some(3)), test_impossible_combinations: (vec![2, 4, 6, 8], 7, None), + test_greedy_approach_does_not_work: (vec![1, 12, 20], 24, Some(2)), } } From 8df73cbd57636f0c7aafdf64b396035e4be319f2 Mon Sep 17 00:00:00 2001 From: vil02 <65706193+vil02@users.noreply.github.com> Date: Thu, 1 Aug 2024 08:33:37 +0200 Subject: [PATCH 4/4] tests: add `zero_denominations`-like tests --- src/dynamic_programming/coin_change.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dynamic_programming/coin_change.rs b/src/dynamic_programming/coin_change.rs index 033ef08ff3d..2bfd573a9c0 100644 --- a/src/dynamic_programming/coin_change.rs +++ b/src/dynamic_programming/coin_change.rs @@ -88,5 +88,7 @@ mod tests { test_mixed_small_large: (vec![1, 100, 1000, 10000], 11001, Some(3)), test_impossible_combinations: (vec![2, 4, 6, 8], 7, None), test_greedy_approach_does_not_work: (vec![1, 12, 20], 24, Some(2)), + test_zero_denominations_no_solution: (vec![0], 1, None), + test_zero_denominations_solution: (vec![0], 0, Some(0)), } }