|
| 1 | +# Introduction |
| 2 | + |
| 3 | +<!-- TODO write a proper introduction --> |
| 4 | + |
| 5 | +Several possible approaches to this exercise: |
| 6 | + |
| 7 | +- filter for multiples |
| 8 | +- generate multiples and |
| 9 | + - gather & de-duplicate, e.g. using `set().union` |
| 10 | + - merge the multiple-generators into one |
| 11 | +- spot the repeating pattern |
| 12 | + |
| 13 | + |
| 14 | +## Approach: `filter` for multiples |
| 15 | + |
| 16 | +```python |
| 17 | +def sum_of_multiples(limit, factors): |
| 18 | + is_multiple = lambda n: any(n % f == 0 for f in factors if f != 0) |
| 19 | + return sum(filter(is_multiple, range(limit))) |
| 20 | +``` |
| 21 | + |
| 22 | +Egregious performance when multiples are few. |
| 23 | + |
| 24 | +... |
| 25 | + |
| 26 | + |
| 27 | +<!-- TODO improve section title --> |
| 28 | +## Approach: generate & gather multiples |
| 29 | + |
| 30 | +```python |
| 31 | +def sum_of_multiples(limit, factors): |
| 32 | + multiples = (range(0, limit, f) for f in factors if f != 0) |
| 33 | + return sum(set().union(*multiples)) |
| 34 | +``` |
| 35 | + |
| 36 | +Egregious memory occupancy when multiples are many. |
| 37 | + |
| 38 | +... |
| 39 | + |
| 40 | + |
| 41 | +<!-- TODO improve section title --> |
| 42 | +## Approach: merge the multiple-generators into one |
| 43 | + |
| 44 | +```python |
| 45 | +# NOTE This is a sketch (but it does work) |
| 46 | +def sum_of_multiples(limit, factors): |
| 47 | + generators = [range(0, limit, f) for f in factors if f != 0] |
| 48 | + while len(generators) > 1: |
| 49 | + generators = [ |
| 50 | + merge(g, g_) |
| 51 | + for g, g_ in zip_longest(generators[0::2], generators[1::2], fillvalue=()) |
| 52 | + ] |
| 53 | + all_multiples, *_ = generators + [()] |
| 54 | + return sum(all_multiples) |
| 55 | + |
| 56 | + |
| 57 | +def merge(gen1, gen2): |
| 58 | + """Merge two sorted-without-duplicates iterables |
| 59 | + into a single sorted-without-duplicates generator. |
| 60 | + """ |
| 61 | + return sorted({*gen1, *gen2}) # FIXME this is CHEATING |
| 62 | +``` |
| 63 | + |
| 64 | +This is supposed to use very little memory. |
| 65 | + |
| 66 | +... |
| 67 | + |
| 68 | + |
| 69 | +<!-- TODO: improve section title --> |
| 70 | +## Approach: spot the repeating pattern |
| 71 | + |
| 72 | +```python |
| 73 | +# NOTE this too is but a sketch (that nevertheless works) |
| 74 | +def sum_of_multiples(limit, factors): |
| 75 | + (*factors,) = filter(lambda f: f != 0, factors) |
| 76 | + N = lcm(*factors) |
| 77 | + is_multiple = lambda n: any(n % f == 0 for f in factors) |
| 78 | + multiples_up_to_lcm = [n for n in range(1, N + 1) if is_multiple(n)] |
| 79 | + q, r = divmod(limit - 1, N) |
| 80 | + return ( |
| 81 | + q * (q - 1) // 2 * N * len(multiples_up_to_lcm) |
| 82 | + + q * sum(multiples_up_to_lcm) |
| 83 | + + sum(q * N + m for m in takewhile(lambda m: m <= r, multiples_up_to_lcm)) |
| 84 | + ) |
| 85 | +``` |
| 86 | + |
| 87 | +```text |
| 88 | +assuming: limit = 22 multiples = [2, 3] |
| 89 | +the task is to sum the lower 4 below rows |
| 90 | +
|
| 91 | +| 1 2 3 4 5 6| 7 8 9 10 11 12|13 14 15 16 17 18|19 20 21 |
| 92 | +| 2 3 4 6| 6 6 6 6| 6 6 6 6| 6 6 |
| 93 | +| | 2 3 4 6| 6 6 6 6| 6 6 |
| 94 | +| | | 2 3 4 6| 6 6 |
| 95 | +| | | | 2 3 |
| 96 | +
|
| 97 | +We see |
| 98 | + 3 copies of - 2 3 4 - 6 |
| 99 | + 0+1+2 = 3×(3-1)/2 = 3 copies of - 6 6 6 - 6 |
| 100 | + 3 copies of - 6 6 |
| 101 | + 1 copy of - 2 3 |
| 102 | +``` |
| 103 | + |
| 104 | +<!-- TODO properly explain this stuff --> |
| 105 | + |
| 106 | +This approach saves on a lot of iteration, but is still vulnerable to excessive memory use. |
| 107 | +Fortunately it can be combined with the generator merging approach. |
| 108 | + |
| 109 | +... |
0 commit comments