Skip to content

Commit

Permalink
Further optimisation of dynamic programming. (#138)
Browse files Browse the repository at this point in the history
* Optimised P31 solution further.
* Optimised P76 solution further.
* Optimised P77 solution further.

See a80653e for the previous optimisation.
  • Loading branch information
tfpf authored Feb 12, 2024
1 parent 51558d8 commit bec5ad6
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 56 deletions.
41 changes: 23 additions & 18 deletions src/solutions/coin_sums.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<i32>>();
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
Expand Down
34 changes: 14 additions & 20 deletions src/solutions/counting_summations.rs
Original file line number Diff line number Diff line change
@@ -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<usize> = (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::<Vec<i32>>();
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
Expand Down
32 changes: 14 additions & 18 deletions src/solutions/prime_summations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<i64>>();
let (rows, cols) = (primes.len(), LIMIT + 1);
let mut ways = vec![
std::iter::once(1)
.chain(std::iter::repeat(0).take(cols - 1))
.collect::<Vec<i64>>();
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
}

0 comments on commit bec5ad6

Please sign in to comment.