From bec5ad6c2f511f7e655861e2c531680ff94f2658 Mon Sep 17 00:00:00 2001 From: Vishal Pankaj Chandratreya <19171016+tfpf@users.noreply.github.com> Date: Mon, 12 Feb 2024 22:07:59 +0530 Subject: [PATCH] Further optimisation of dynamic programming. (#138) * Optimised P31 solution further. * Optimised P76 solution further. * Optimised P77 solution further. See a80653e0ab4094163503dea38411be3778609504 for the previous optimisation. --- src/solutions/coin_sums.rs | 41 ++++++++++++++++------------ src/solutions/counting_summations.rs | 34 ++++++++++------------- src/solutions/prime_summations.rs | 32 ++++++++++------------ 3 files changed, 51 insertions(+), 56 deletions(-) diff --git a/src/solutions/coin_sums.rs b/src/solutions/coin_sums.rs index 0a9419f..6a5d4fa 100644 --- a/src/solutions/coin_sums.rs +++ b/src/solutions/coin_sums.rs @@ -1,34 +1,39 @@ pub fn solve() -> i64 { let denominations = [1, 2, 5, 10, 20, 50, 100, 200]; - // Any element in this matrix shall be the number of ways to obtain a sum - // equal to the column index using coins at indices less than or equal to - // the row index. Note that there is only one way to obtain a sum of 0. - // Hence, as an optimisation, set the first element of each row of the - // matrix to 1 during initialisation. - let (rows, cols) = (denominations.len(), denominations.iter().max().unwrap() + 1); - let mut ways = vec![ - std::iter::once(1) - .chain(std::iter::repeat(0).take(cols - 1)) - .collect::>(); - rows - ]; + // Imagine a matrix in which the element at row index `row` and column + // index `col` is the number of ways to obtain a sum equal to `col` using + // some/all of the coins up to `denominations[row]`. We need a sum of 200. + // What should be the size of this matrix? + let (rows, cols) = (denominations.len(), 201); + + // We don't have to store the whole matrix. At any point in time, we need + // only two consecutive rows. The current row can be constructed from the + // previous row. Since the number of ways to obtain a sum of 0 is always 1, + // the element at index 0 is 1 for any row. For the first row, the + // remaining elements are also 1 (which is the number of ways to obtain any + // sum using the first coin). + let mut curr = vec![1; cols]; + + // Initialise the previous row with dummy values. The first element must be + // 1, as mentioned above. + let mut prev = vec![1; cols]; // Bottom-up dynamic programming. - for sum in 1..cols { - ways[0][sum] = if sum % denominations[0] == 0 { 1 } else { 0 }; - } for idx in 1..rows { + (prev, curr) = (curr, prev); for sum in 1..cols { - ways[idx][sum] = ways[idx - 1][sum] + curr[sum] = prev[sum] + if sum >= denominations[idx] { - ways[idx][sum - denominations[idx]] + curr[sum - denominations[idx]] } else { 0 }; } + // At this point, `curr` is the `idx`th row of the matrix, and `prev` + // is the previous. } - let result = ways[rows - 1][cols - 1]; + let result = curr[cols - 1]; assert_eq!(result, 73682); result as i64 diff --git a/src/solutions/counting_summations.rs b/src/solutions/counting_summations.rs index ba27285..55d4110 100644 --- a/src/solutions/counting_summations.rs +++ b/src/solutions/counting_summations.rs @@ -1,30 +1,24 @@ pub fn solve() -> i64 { // Approach is similar to that used for P31. The difference is that the - // maximum denomination (99) is not the same as the target sum (100). - let denominations: Vec = (1..100).collect(); - let (rows, cols) = (denominations.len(), denominations.iter().max().unwrap() + 2); - let mut ways = vec![ - std::iter::once(1) - .chain(std::iter::repeat(0).take(cols - 1)) - .collect::>(); - rows - ]; + // denominations are the numbers from 1 to 99 (so we don't need a separate + // array to store them) and that the target sum is 100. The matrix is now + // such that the element at row index `row` and column index `col` is the + // number of ways to obtain a sum equal to `col` using some/all of the + // numbers up to `row`. Obviously, we can skip `row == 0`. + let (rows, cols) = (100, 101); + + // As noted above, we start from `row == 1`. + let mut curr = vec![1; cols]; + let mut prev = vec![1; cols]; // Bottom-up dynamic programming. - for sum in 1..cols { - ways[0][sum] = if sum % denominations[0] == 0 { 1 } else { 0 }; - } - for idx in 1..rows { + for idx in 2..rows { + (prev, curr) = (curr, prev); for sum in 1..cols { - ways[idx][sum] = ways[idx - 1][sum] - + if sum >= denominations[idx] { - ways[idx][sum - denominations[idx]] - } else { - 0 - }; + curr[sum] = prev[sum] + if sum >= idx { curr[sum - idx] } else { 0 }; } } - let result = ways[rows - 1][cols - 1]; + let result = curr[cols - 1]; assert_eq!(result, 190569291); result as i64 diff --git a/src/solutions/prime_summations.rs b/src/solutions/prime_summations.rs index f3af2be..03ada24 100644 --- a/src/solutions/prime_summations.rs +++ b/src/solutions/prime_summations.rs @@ -2,37 +2,33 @@ use crate::utils; pub fn solve() -> i64 { // Approach is similar to that used for P31. The difference is that the - // maximum denomination is guessed, and that there is no target sum. + // denominations are prime numbers (how many of them to use is guessed), + // and that there is no target sum. const LIMIT: usize = 100; let primes = utils::SieveOfAtkin::new(LIMIT).iter().collect::>(); let (rows, cols) = (primes.len(), LIMIT + 1); - let mut ways = vec![ - std::iter::once(1) - .chain(std::iter::repeat(0).take(cols - 1)) - .collect::>(); - rows - ]; + + // This will no longer be all ones, because the number of ways to obtain a + // particular sum using only the first prime number depends on the parity + // of the sum! + let mut curr = (0..cols) + .map(|sum| if sum % primes[0] as usize == 0 { 1 } else { 0 }) + .collect(); + let mut prev = vec![1; cols]; // Bottom-up dynamic programming. - for sum in 1..cols { - ways[0][sum] = if sum % primes[0] as usize == 0 { 1 } else { 0 }; - } for idx in 1..rows { + (prev, curr) = (curr, prev); for sum in 1..cols { - ways[idx][sum] = ways[idx - 1][sum] + curr[sum] = prev[sum] + if sum >= primes[idx] as usize { - ways[idx][sum - primes[idx] as usize] + curr[sum - primes[idx] as usize] } else { 0 }; } } - let result = ways[rows - 1] - .iter() - .enumerate() - .find(|(_, &count)| count >= 5000) - .unwrap() - .0; + let result = curr.iter().enumerate().find(|(_, &count)| count >= 5000).unwrap().0; result as i64 }