|
37 | 37 | //! 8 % 5 = 3 |
38 | 38 | //! ``` |
39 | 39 | //! |
40 | | -//! Each item can be treated individually. This allows the processing to be parallelized over |
41 | | -//! many threads, speeding things up in part two. |
| 40 | +//! A neat trick is that each item can be treated individually. This allows the processing to be |
| 41 | +//! parallelized over many threads. To speed things up even more, we notice that items form cycles, |
| 42 | +//! repeating the same path through the monkeys. Once we find a cycle for an item, then we short |
| 43 | +//! circuit the calculation early without having to calculate the entire 10,000 rounds. |
42 | 44 | //! |
43 | 45 | //! [`iter_unsigned`]: ParseOps::iter_unsigned |
| 46 | +use crate::util::hash::*; |
44 | 47 | use crate::util::parse::*; |
45 | 48 | use crate::util::thread::*; |
46 | 49 |
|
47 | | -type Pair = (usize, u64); |
| 50 | +type Input = (Vec<Monkey>, Vec<Pair>); |
| 51 | +type Pair = (usize, usize); |
48 | 52 |
|
49 | 53 | pub struct Monkey { |
50 | | - items: Vec<u64>, |
| 54 | + items: Vec<usize>, |
51 | 55 | operation: Operation, |
52 | | - test: u64, |
| 56 | + test: usize, |
53 | 57 | yes: usize, |
54 | 58 | no: usize, |
55 | 59 | } |
56 | 60 |
|
57 | | -pub enum Operation { |
| 61 | +enum Operation { |
58 | 62 | Square, |
59 | | - Multiply(u64), |
60 | | - Add(u64), |
| 63 | + Multiply(usize), |
| 64 | + Add(usize), |
61 | 65 | } |
62 | 66 |
|
63 | | -type Business = [u64; 8]; |
| 67 | +#[derive(Clone, Copy)] |
| 68 | +struct Business([usize; 8]); |
64 | 69 |
|
65 | | -/// Extract each Monkey's info from the flavor text. With the exception of the lines starting |
66 | | -/// `Operation` we are only interested in the numbers on each line. |
67 | | -pub fn parse(input: &str) -> Vec<Monkey> { |
68 | | - /// Inner helper function to keep the parsing logic readable. |
69 | | - fn helper(chunk: &[&str]) -> Monkey { |
70 | | - let items = chunk[1].iter_unsigned().collect(); |
71 | | - let tokens: Vec<_> = chunk[2].split(' ').rev().take(2).collect(); |
72 | | - let operation = match tokens[..] { |
73 | | - ["old", _] => Operation::Square, |
74 | | - [y, "*"] => Operation::Multiply(y.unsigned()), |
75 | | - [y, "+"] => Operation::Add(y.unsigned()), |
76 | | - _ => unreachable!(), |
77 | | - }; |
78 | | - let test = chunk[3].unsigned(); |
79 | | - let yes = chunk[4].unsigned(); |
80 | | - let no = chunk[5].unsigned(); |
81 | | - Monkey { items, operation, test, yes, no } |
| 70 | +impl Business { |
| 71 | + fn zero() -> Self { |
| 72 | + Business([0; 8]) |
82 | 73 | } |
83 | | - input.lines().collect::<Vec<&str>>().chunks(7).map(helper).collect() |
84 | | -} |
85 | 74 |
|
86 | | -pub fn part1(input: &[Monkey]) -> u64 { |
87 | | - solve(input, sequential) |
88 | | -} |
| 75 | + fn inc(&mut self, from: usize) { |
| 76 | + self.0[from] += 1; |
| 77 | + } |
89 | 78 |
|
90 | | -pub fn part2(input: &[Monkey]) -> u64 { |
91 | | - solve(input, parallel) |
92 | | -} |
| 79 | + fn level(mut self) -> usize { |
| 80 | + self.0.sort_unstable(); |
| 81 | + self.0.iter().rev().take(2).product() |
| 82 | + } |
93 | 83 |
|
94 | | -/// Convenience wrapper to reuse common logic between part one and two. |
95 | | -fn solve(monkeys: &[Monkey], play: impl Fn(&[Monkey], &[Pair]) -> Business) -> u64 { |
96 | | - let mut pairs = Vec::new(); |
| 84 | + fn add(mut self, rhs: Self) -> Self { |
| 85 | + self.0.iter_mut().zip(rhs.0).for_each(|(a, b)| *a += b); |
| 86 | + self |
| 87 | + } |
97 | 88 |
|
98 | | - for (from, monkey) in monkeys.iter().enumerate() { |
99 | | - for &item in &monkey.items { |
100 | | - pairs.push((from, item)); |
101 | | - } |
| 89 | + fn sub(mut self, rhs: Self) -> Self { |
| 90 | + self.0.iter_mut().zip(rhs.0).for_each(|(a, b)| *a -= b); |
| 91 | + self |
102 | 92 | } |
103 | 93 |
|
104 | | - let mut business = play(monkeys, &pairs); |
105 | | - business.sort_unstable(); |
106 | | - business.iter().rev().take(2).product() |
| 94 | + fn mul(mut self, rhs: usize) -> Self { |
| 95 | + self.0.iter_mut().for_each(|a| *a *= rhs); |
| 96 | + self |
| 97 | + } |
107 | 98 | } |
108 | 99 |
|
109 | | -/// Play 20 rounds dividing the worry level by 3 each inspection. |
110 | | -fn sequential(monkeys: &[Monkey], pairs: &[Pair]) -> Business { |
111 | | - let mut business = [0; 8]; |
| 100 | +/// Extract each Monkey's info from the flavor text. With the exception of the lines starting |
| 101 | +/// `Operation` we are only interested in the numbers on each line. |
| 102 | +pub fn parse(input: &str) -> Input { |
| 103 | + let lines: Vec<_> = input.lines().collect(); |
| 104 | + |
| 105 | + let monkeys: Vec<_> = lines |
| 106 | + .chunks(7) |
| 107 | + .map(|chunk: &[&str]| { |
| 108 | + let items = chunk[1].iter_unsigned().collect(); |
| 109 | + let tokens: Vec<_> = chunk[2].split(' ').rev().take(2).collect(); |
| 110 | + let operation = match tokens[..] { |
| 111 | + ["old", _] => Operation::Square, |
| 112 | + [y, "*"] => Operation::Multiply(y.unsigned()), |
| 113 | + [y, "+"] => Operation::Add(y.unsigned()), |
| 114 | + _ => unreachable!(), |
| 115 | + }; |
| 116 | + let test = chunk[3].unsigned(); |
| 117 | + let yes = chunk[4].unsigned(); |
| 118 | + let no = chunk[5].unsigned(); |
| 119 | + Monkey { items, operation, test, yes, no } |
| 120 | + }) |
| 121 | + .collect(); |
| 122 | + |
| 123 | + let pairs: Vec<_> = monkeys |
| 124 | + .iter() |
| 125 | + .enumerate() |
| 126 | + .flat_map(|(from, monkey)| monkey.items.iter().map(move |&item| (from, item))) |
| 127 | + .collect(); |
| 128 | + |
| 129 | + (monkeys, pairs) |
| 130 | +} |
112 | 131 |
|
113 | | - for &pair in pairs { |
114 | | - let extra = play(monkeys, 20, |x| x / 3, pair); |
115 | | - business.iter_mut().enumerate().for_each(|(i, b)| *b += extra[i]); |
| 132 | +pub fn part1(input: &Input) -> usize { |
| 133 | + let (monkeys, pairs) = input; |
| 134 | + let mut business = Business::zero(); |
| 135 | + |
| 136 | + for &(mut from, mut item) in pairs { |
| 137 | + let mut rounds = 0; |
| 138 | + |
| 139 | + while rounds < 20 { |
| 140 | + let worry = match monkeys[from].operation { |
| 141 | + Operation::Square => item * item, |
| 142 | + Operation::Multiply(y) => item * y, |
| 143 | + Operation::Add(y) => item + y, |
| 144 | + }; |
| 145 | + item = worry / 3; |
| 146 | + |
| 147 | + let to = if item.is_multiple_of(monkeys[from].test) { |
| 148 | + monkeys[from].yes |
| 149 | + } else { |
| 150 | + monkeys[from].no |
| 151 | + }; |
| 152 | + |
| 153 | + business.inc(from); |
| 154 | + |
| 155 | + // Only increase the round when the item is passes to a previous monkey |
| 156 | + // which will have to be processed in the next turn. |
| 157 | + rounds += usize::from(to < from); |
| 158 | + from = to; |
| 159 | + } |
116 | 160 | } |
117 | 161 |
|
118 | | - business |
| 162 | + business.level() |
119 | 163 | } |
120 | 164 |
|
121 | | -/// Play 10,000 rounds adjusting the worry level modulo the product of all the monkey's test values. |
122 | | -fn parallel(monkeys: &[Monkey], pairs: &[Pair]) -> Business { |
| 165 | +pub fn part2(input: &Input) -> usize { |
| 166 | + let (monkeys, pairs) = input; |
| 167 | + |
123 | 168 | // Use as many cores as possible to parallelize the calculation. |
124 | | - let result = spawn_parallel_iterator(pairs, |iter| worker(monkeys, iter)); |
| 169 | + let result = spawn_parallel_iterator(pairs, |iter| { |
| 170 | + iter.map(|&(from, item)| play(monkeys, from, item)).collect::<Vec<_>>() |
| 171 | + }); |
125 | 172 |
|
126 | | - let mut business = [0; 8]; |
127 | | - for extra in result.into_iter().flatten() { |
128 | | - business.iter_mut().zip(extra).for_each(|(b, e)| *b += e); |
129 | | - } |
130 | | - business |
| 173 | + // Merge results. |
| 174 | + result.into_iter().flatten().fold(Business::zero(), Business::add).level() |
131 | 175 | } |
132 | 176 |
|
133 | | -/// Multiple worker functions are executed in parallel, one per thread. |
134 | | -fn worker(monkeys: &[Monkey], iter: ParIter<'_, Pair>) -> Vec<Business> { |
135 | | - let product: u64 = monkeys.iter().map(|m| m.test).product(); |
136 | | - iter.map(|&pair| play(monkeys, 10000, |x| x % product, pair)).collect() |
137 | | -} |
| 177 | +/// Play 10,000 rounds adjusting the worry level modulo the product of all the monkey's test values. |
| 178 | +/// Look for cycles in each path so that we don't have to process the entire 10,000 rounds. |
| 179 | +fn play(monkeys: &[Monkey], mut from: usize, mut item: usize) -> Business { |
| 180 | + let product: usize = monkeys.iter().map(|m| m.test).product(); |
| 181 | + |
| 182 | + let mut round = 0; |
| 183 | + let mut business = Business::zero(); |
| 184 | + |
| 185 | + let mut path = Vec::new(); |
| 186 | + let mut seen = FastMap::new(); |
138 | 187 |
|
139 | | -/// Play an arbitrary number of rounds for a single item. |
140 | | -/// |
141 | | -/// The logic to adjust the worry level is passed via a closure |
142 | | -/// so that we can re-use the bulk of the same logic between part 1 and 2. |
143 | | -fn play(monkeys: &[Monkey], max_rounds: u32, adjust: impl Fn(u64) -> u64, pair: Pair) -> Business { |
144 | | - let (mut from, mut item) = pair; |
145 | | - let mut rounds = 0; |
146 | | - let mut business = [0; 8]; |
| 188 | + path.push(business); |
| 189 | + seen.insert((from, item), path.len() - 1); |
147 | 190 |
|
148 | | - while rounds < max_rounds { |
| 191 | + while round < 10_000 { |
149 | 192 | let worry = match monkeys[from].operation { |
150 | 193 | Operation::Square => item * item, |
151 | 194 | Operation::Multiply(y) => item * y, |
152 | 195 | Operation::Add(y) => item + y, |
153 | 196 | }; |
154 | | - item = adjust(worry); |
| 197 | + item = worry % product; |
155 | 198 |
|
156 | 199 | let to = if item.is_multiple_of(monkeys[from].test) { |
157 | 200 | monkeys[from].yes |
158 | 201 | } else { |
159 | 202 | monkeys[from].no |
160 | 203 | }; |
161 | 204 |
|
| 205 | + business.inc(from); |
| 206 | + |
162 | 207 | // Only increase the round when the item is passes to a previous monkey |
163 | 208 | // which will have to be processed in the next turn. |
164 | | - rounds += (to < from) as u32; |
165 | | - business[from] += 1; |
| 209 | + if to < from { |
| 210 | + round += 1; |
| 211 | + path.push(business); |
| 212 | + |
| 213 | + // If we have found a cycle, then short ciruit and return the final result. |
| 214 | + if let Some(previous) = seen.insert((to, item), path.len() - 1) { |
| 215 | + let cycle_width = round - previous; |
| 216 | + |
| 217 | + let offset = 10_000 - round; |
| 218 | + let quotient = offset / cycle_width; |
| 219 | + let remainder = offset % cycle_width; |
| 220 | + |
| 221 | + let full = business.sub(path[previous]).mul(quotient); |
| 222 | + let partial = path[previous + remainder].sub(path[previous]); |
| 223 | + return business.add(full).add(partial); |
| 224 | + } |
| 225 | + } |
| 226 | + |
166 | 227 | from = to; |
167 | 228 | } |
168 | 229 |
|
|
0 commit comments