Skip to content

Commit

Permalink
sum-of-multiples: Add approaches (#1858)
Browse files Browse the repository at this point in the history
  • Loading branch information
clechasseur authored Jan 21, 2024
1 parent 43226e1 commit 5d4a40e
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 0 deletions.
27 changes: 27 additions & 0 deletions exercises/practice/sum-of-multiples/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"introduction": {
"authors": [
"clechasseur"
]
},
"approaches": [
{
"uuid": "c0e599bf-a0b4-4eb3-af73-ab6c5b04dec8",
"slug": "from-factors",
"title": "Calculating sum from factors",
"blurb": "Calculate the sum by scanning the factors and computing their multiples.",
"authors": [
"clechasseur"
]
},
{
"uuid": "305246ad-c36a-48ae-8047-cd00a4e7a3e4",
"slug": "from-range",
"title": "Calculating sum by iterating the whole range",
"blurb": "Calculate the sum by scanning the whole range and identifying any multiple via factors.",
"authors": [
"clechasseur"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Calculating sum from factors

```rust
pub fn sum_of_multiples_from_factors(limit: u32, factors: &[u32]) -> u32 {
let mut multiples: Vec<_> = factors
.iter()
.filter(|&&factor| factor != 0)
.flat_map(|&factor| (factor..limit).step_by(factor as usize))
.collect();
multiples.sort();
multiples.dedup();
multiples.iter().sum()
}
```

This approach implements the exact steps outlined in the exercise description:

1. For each non-zero factor, find all multiples of that factor that are less than the `limit`
2. Collect all multiples in a [`Vec`][vec]
3. Remove duplicate multiples
3. Calculate the sum of all unique multiples

In order to compute the list of multiples for a factor, we create a [`Range`][range] from the factor (inclusive) to the `limit` (exclusive), then use [`step_by`][iterator-step_by] with the same factor.

To combine the multiples of all factors, we iterate the list of factors and use [`flat_map`][iterator-flat_map] on each factor's multiples.
[`flat_map`][iterator-flat_map] is a combination of [`map`][iterator-map] and [`flatten`][iterator-flatten]; it maps each factor into its multiples, then flattens them all in a single sequence.

Since we need to have unique multiples to compute the proper sum, we [`collect`][iterator-collect] the multiples into a [`Vec`][vec], which allows us to then [`sort`][slice-sort][^1] them and use [`dedup`][vec-dedup] to remove the duplicates.
[`collect`][iterator-collect] is a powerful function that can collect the data in a sequence and store it in any kind of collection - however, because of this, the compiler is not able to infer the type of collection you want as the output.
To solve this problem, we type the `multiples` variable explicitly.

Finally, calculating the sum of the remaining unique multiples in the set is easy: we can simply call [`sum`][iterator-sum].

[^1]: There is another method available to sort a slice: [`sort_unstable`][slice-sort_unstable]. Usually, using [`sort_unstable`][slice-sort_unstable] is recommended if we do not need to keep the ordering of duplicate elements (which is our case). However, [`sort`][slice-sort] has the advantage because of its implementation. From the documentation:

> Current implementation
>
> The current algorithm is an adaptive, iterative merge sort inspired by timsort. It is designed to be very fast in cases where the slice is nearly sorted, or consists of two or more sorted sequences concatenated one after another.
The last part is key, because this is exactly our use case: we concatenate sequences of _sorted_ multiples.

Running a benchmark using the two methods shows that in our scenario, [`sort`][slice-sort] is about twice as fast as [`sort_unstable`][slice-sort_unstable].

[vec]: https://doc.rust-lang.org/std/vec/struct.Vec.html
[range]: https://doc.rust-lang.org/std/ops/struct.Range.html
[iterator-step_by]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.step_by
[iterator-flat_map]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flat_map
[iterator-map]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map
[iterator-flatten]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flatten
[iterator-collect]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect
[slice-sort]: https://doc.rust-lang.org/std/primitive.slice.html#method.sort
[vec-dedup]: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.dedup
[iterator-sum]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.sum
[slice-sort_unstable]: https://doc.rust-lang.org/std/primitive.slice.html#method.sort_unstable
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub fn sum_of_multiples_from_factors(limit: u32, factors: &[u32]) -> u32 {
let mut multiples: Vec<_> = factors.iter()
.filter(|&&factor| factor != 0)
.flat_map(|&factor| (factor..limit).step_by(factor as usize))
.collect();
multiples.sort();
multiples.dedup();
multiples.iter().sum()
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Calculating sum by iterating the whole range

```rust
pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 {
(1..limit)
.filter(|&n| factors.iter().any(|&factor| factor != 0 && n % factor == 0))
.sum()
}
```

Instead of implementing the steps in the exercise description, this approach uses another angle:

1. Iterate all numbers between 1 (inclusive) and `limit` (exclusive)
2. Keep only numbers which have at least one factor in `factors` (automatically avoiding any duplicates)
3. Calculate the sum of all numbers kept

After creating our range, we use [`filter`][iterator-filter] to keep only matching multiples.
To detect the multiples, we scan the `factors` and use [`any`][iterator-any] to check if at least one is a factor of the number we're checking.
([`any`][iterator-any] is short-circuiting: it stops as soon as it finds one compatible factor.)

Finally, once we have the multiples, we can compute the sum easily using [`sum`][iterator-sum].

[iterator-filter]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter
[iterator-any]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.any
[iterator-sum]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 {
(1..limit)
.filter(|&n| factors.iter().any(|&factor| factor != 0 && n % factor == 0))
.sum()
}
55 changes: 55 additions & 0 deletions exercises/practice/sum-of-multiples/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Introduction

There are a couple of different approaches available to solve Sum of Multiples.
One is to follow the algorithm [outlined in the exercise description][approach-from-factors],
but there are other ways, including [scanning the entire range][approach-from-range].

## General guidance

The key to solving Sum of Multiples is to find the unique multiples of all provided factors.
To determine if `f` is a factor of a given number `n`, we can use the [remainder operator][rem].
It is also possible to find the multiples by simple addition, starting from the factor.

## Approach: Calculating sum from factors

```rust
pub fn sum_of_multiples_from_factors(limit: u32, factors: &[u32]) -> u32 {
let mut multiples: Vec<_> = factors
.iter()
.filter(|&&factor| factor != 0)
.flat_map(|&factor| (factor..limit).step_by(factor as usize))
.collect();
multiples.sort();
multiples.dedup();
multiples.iter().sum()
}
```

For more information, check the [Sum from factors approach][approach-from-factors].

## Approach: Calculating sum by iterating the whole range

```rust
pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 {
(1..limit)
.filter(|&n| factors.iter().any(|&factor| factor != 0 && n % factor == 0))
.sum()
}
```

For more information, check the [Sum by iterating the whole range approach][approach-from-range].

## Which approach to use?

- Computing the sum from factors can be efficient if we have a small number of factors and/or if they are large compared to the limit, because this will result in a small number of multiples to deduplicate.
However, as the number of multiples grows, this approach can result in a lot of work to deduplicate them.
- Computing the sum by iterating the whole range is less efficient for large ranges when the number of factors is small and/or when they are large.
However, this approach has the advantage of having stable complexity that is only dependent on the limit and the number of factors, since there is no deduplication involved.
It also avoids any additional memory allocation.

Without proper benchmarks, the second approach may be preferred since it offers a more stable level of complexity (e.g. its performances varies less when the size of the input changes).
However, if you have some knowledge of the size and shape of the input, then benchmarking might reveal that one approach is better than the other for your specific use case.

[approach-from-factors]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-factors
[approach-from-range]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-range
[rem]: https://doc.rust-lang.org/core/ops/trait.Rem.html

0 comments on commit 5d4a40e

Please sign in to comment.