Skip to content

Commit aebfa5b

Browse files
committed
Faster approach that find cycles in each item
1 parent c0ebb02 commit aebfa5b

File tree

2 files changed

+140
-79
lines changed

2 files changed

+140
-79
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Improvements to solutions are always appreciated. Please see the
5858
## Performance
5959

6060
Benchmarks are measured using the built-in `cargo bench` tool run on an [Apple M2 Max][apple-link].
61-
All 250 solutions from 2024 to 2015 complete sequentially in **512 milliseconds**.
61+
All 250 solutions from 2024 to 2015 complete sequentially in **511 milliseconds**.
6262
Interestingly 86% of the total time is spent on just 9 solutions.
6363
Performance is reasonable even on older hardware, for example a 2011 MacBook Pro with an
6464
[Intel i7-2720QM][intel-link] processor takes 3.5 seconds to run the same 250 solutions.
@@ -67,7 +67,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro
6767

6868
| Year | [2015](#2015) | [2016](#2016) | [2017](#2017) | [2018](#2018) | [2019](#2019) | [2020](#2020) | [2021](#2021) | [2022](#2022) | [2023](#2023) | [2024](#2024) |
6969
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
70-
| Benchmark (ms) | 17 | 117 | 82 | 35 | 15 | 220 | 9 | 8 | 5 | 4 |
70+
| Benchmark (ms) | 17 | 117 | 82 | 35 | 15 | 220 | 9 | 7 | 5 | 4 |
7171

7272
## 2024
7373

@@ -149,7 +149,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro
149149
| 8 | [Treetop Tree House](https://adventofcode.com/2022/day/8) | [Source](src/year2022/day08.rs) | 51 |
150150
| 9 | [Rope Bridge](https://adventofcode.com/2022/day/9) | [Source](src/year2022/day09.rs) | 107 |
151151
| 10 | [Cathode-Ray Tube](https://adventofcode.com/2022/day/10) | [Source](src/year2022/day10.rs) | 2 |
152-
| 11 | [Monkey in the Middle](https://adventofcode.com/2022/day/11) | [Source](src/year2022/day11.rs) | 1173 |
152+
| 11 | [Monkey in the Middle](https://adventofcode.com/2022/day/11) | [Source](src/year2022/day11.rs) | 244 |
153153
| 12 | [Hill Climbing Algorithm](https://adventofcode.com/2022/day/12) | [Source](src/year2022/day12.rs) | 57 |
154154
| 13 | [Distress Signal](https://adventofcode.com/2022/day/13) | [Source](src/year2022/day13.rs) | 15 |
155155
| 14 | [Regolith Reservoir](https://adventofcode.com/2022/day/14) | [Source](src/year2022/day14.rs) | 146 |

src/year2022/day11.rs

Lines changed: 137 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -37,132 +37,193 @@
3737
//! 8 % 5 = 3
3838
//! ```
3939
//!
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.
4244
//!
4345
//! [`iter_unsigned`]: ParseOps::iter_unsigned
46+
use crate::util::hash::*;
4447
use crate::util::parse::*;
4548
use crate::util::thread::*;
4649

47-
type Pair = (usize, u64);
50+
type Input = (Vec<Monkey>, Vec<Pair>);
51+
type Pair = (usize, usize);
4852

4953
pub struct Monkey {
50-
items: Vec<u64>,
54+
items: Vec<usize>,
5155
operation: Operation,
52-
test: u64,
56+
test: usize,
5357
yes: usize,
5458
no: usize,
5559
}
5660

57-
pub enum Operation {
61+
enum Operation {
5862
Square,
59-
Multiply(u64),
60-
Add(u64),
63+
Multiply(usize),
64+
Add(usize),
6165
}
6266

63-
type Business = [u64; 8];
67+
#[derive(Clone, Copy)]
68+
struct Business([usize; 8]);
6469

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])
8273
}
83-
input.lines().collect::<Vec<&str>>().chunks(7).map(helper).collect()
84-
}
8574

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+
}
8978

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+
}
9383

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+
}
9788

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
10292
}
10393

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+
}
10798
}
10899

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+
}
112131

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+
}
116160
}
117161

118-
business
162+
business.level()
119163
}
120164

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+
123168
// 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+
});
125172

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()
131175
}
132176

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();
138187

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);
147190

148-
while rounds < max_rounds {
191+
while round < 10_000 {
149192
let worry = match monkeys[from].operation {
150193
Operation::Square => item * item,
151194
Operation::Multiply(y) => item * y,
152195
Operation::Add(y) => item + y,
153196
};
154-
item = adjust(worry);
197+
item = worry % product;
155198

156199
let to = if item.is_multiple_of(monkeys[from].test) {
157200
monkeys[from].yes
158201
} else {
159202
monkeys[from].no
160203
};
161204

205+
business.inc(from);
206+
162207
// Only increase the round when the item is passes to a previous monkey
163208
// 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+
166227
from = to;
167228
}
168229

0 commit comments

Comments
 (0)