aoc/year2015/day20.rs
1//! # Infinite Elves and Infinite Houses
2//!
3//! The amount of presents that each house receives in part one is 10 times the
4//! [divisor function](https://en.wikipedia.org/wiki/Divisor_function) `σ`.
5//! For example the divisors of 6 are 1, 2, 3 and 6, so house 6 receives
6//! 10 + 20 + 30 + 60 = 120 presents.
7//!
8//! It's highly likely that the answer will be a
9//! [superabundant number](https://en.wikipedia.org/wiki/Superabundant_number) but there's no way
10//! to easily *prove* that so we brute force check every possible solution. The approach is similar
11//! to a reverse [Sieve of Eratosthenes](https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes),
12//! iterating first over each elf, then over each house adding the presents.
13//! Somewhat unintuitively the `ln(x)` asymptotic complexity of this approach is much lower
14//! than the `n√n` complexity of finding the factors of each number.
15//!
16//! To speed things up we make one high level optimization and a few low tweaks.
17//!
18//! The high level optimization is an observation from user `warbaque` on the
19//! [Day 20 solution thread](https://www.reddit.com/r/adventofcode/comments/3xjpp2/day_20_solutions/)
20//! that [Robin's inequality](https://en.wikipedia.org/wiki/Divisor_function#Growth_rate)
21//! shows that the `σ(n)` function is lower than `eᵞ * n * ln(ln(n))` for all `n` greater than 5040.
22//! This means that we can determine a starting index close to the final result, skipping over a
23//! huge chunk of numbers.
24//!
25//! The low level tweaks reduce the amount of work that needs to be done.
26//! We break the search range into fixed size blocks from `start` to `end`,
27//! for example 200000 to 299999 with a block size of 100000. Then we can make some observations:
28//!
29//! * Elf 1 visits each house once.
30//! * Elves from `start` to `end` each visit a different house exactly once,
31//! bringing start, start + 1, ... presents.
32//! * Elves from `end / 2` to `start` skip over all the houses.
33//! * Elves from `block size` to `end / 2` visit *at most* one house as the increment is
34//! greater than the size of the block.
35//! * Elves from `2` to `block size` may visit any number of times.
36
37// More explicit syntax fits in with surrounding code better.
38#![allow(clippy::needless_range_loop)]
39
40use crate::util::parse::*;
41
42const BLOCK: usize = 100_000;
43
44type Input = (u32, usize);
45
46pub fn parse(input: &str) -> Input {
47 let robins_inequality = [
48 (100000, 4352000),
49 (200000, 8912250),
50 (300000, 13542990),
51 (400000, 18218000),
52 (500000, 22925240),
53 (600000, 27657740),
54 (700000, 32410980),
55 (800000, 37181790),
56 (900000, 41967820),
57 (1000000, 46767260),
58 (1100000, 51578680),
59 (1200000, 56400920),
60 (1300000, 61233020),
61 (1400000, 66074170),
62 (1500000, 70923680),
63 (1600000, 75780960),
64 (1700000, 80645490),
65 (1800000, 85516820),
66 (1900000, 90394550),
67 (2000000, 95278320),
68 ];
69
70 // Find a starting block closer to the answer. Part two presents overall are lower than
71 // part one so the answer will also be after this block.
72 let (target, mut start) = (input.unsigned(), 0);
73
74 for (key, value) in robins_inequality {
75 if target >= value {
76 start = key;
77 } else {
78 break;
79 }
80 }
81
82 assert!(target > 5040);
83 assert!(start > 100000);
84
85 (target, start)
86}
87
88pub fn part1(input: &Input) -> usize {
89 let (target, mut start) = *input;
90 let mut end = start + BLOCK;
91 let mut houses = vec![0; BLOCK];
92
93 loop {
94 // Elves that visit exactly once.
95 for i in 0..BLOCK {
96 houses[i] = 10 * (1 + start + i) as u32;
97 }
98
99 // Elves that visit at most once.
100 for i in BLOCK..end / 2 {
101 let presents = 10 * i as u32;
102 let j = start.next_multiple_of(i) - start;
103
104 if j < BLOCK {
105 houses[j] += presents;
106 }
107 }
108
109 // All remaining elves.
110 for i in 2..BLOCK {
111 let presents = 10 * i as u32;
112 let mut j = start.next_multiple_of(i) - start;
113
114 while j < BLOCK {
115 houses[j] += presents;
116 j += i;
117 }
118 }
119
120 if let Some(found) = houses.iter().position(|&p| p >= target) {
121 break start + found;
122 }
123
124 start += BLOCK;
125 end += BLOCK;
126 }
127}
128
129pub fn part2(input: &Input) -> usize {
130 let (target, mut start) = *input;
131 let mut end = start + BLOCK;
132 let mut houses = vec![0; BLOCK];
133
134 loop {
135 // Elves that visit exactly once (not including elf 1 anymore).
136 for i in 0..BLOCK {
137 houses[i] = 11 * (start + i) as u32;
138 }
139
140 // Elves that visit at most once.
141 for i in BLOCK..end / 2 {
142 let presents = 11 * i as u32;
143 let j = start.next_multiple_of(i) - start;
144
145 if j < BLOCK {
146 houses[j] += presents;
147 }
148 }
149
150 // All remaining elves. We can start higher than 2.
151 for i in start / 50..BLOCK {
152 let presents = 11 * i as u32;
153 let mut j = start.next_multiple_of(i) - start;
154 let mut remaining = 51 - start.div_ceil(i);
155
156 while j < BLOCK && remaining > 0 {
157 houses[j] += presents;
158 j += i;
159 remaining -= 1;
160 }
161 }
162
163 if let Some(found) = houses.iter().position(|&p| p >= target) {
164 break start + found;
165 }
166
167 start += BLOCK;
168 end += BLOCK;
169 }
170}