Skip to content

Commit 76249e7

Browse files
committed
Optimize 2024 day 11
Precompute all stone transformations
1 parent b24b95c commit 76249e7

File tree

1 file changed

+110
-27
lines changed

1 file changed

+110
-27
lines changed

crates/year2024/src/day11.rs

Lines changed: 110 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,137 @@
1-
use std::collections::HashMap;
1+
use std::collections::{HashMap, VecDeque};
22
use utils::prelude::*;
33

44
/// Counting dividing stones.
55
#[derive(Clone, Debug)]
66
pub struct Day11 {
7-
stones: HashMap<u64, u64>,
7+
pub counts: Vec<u64>,
8+
pub next: Vec<(usize, usize)>,
9+
pub max_idx: Vec<usize>,
810
}
911

12+
// Placeholder stone number, used as the second stone when a stone only splits into one
13+
const PLACEHOLDER: u64 = u64::MAX;
14+
1015
impl Day11 {
1116
pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
12-
let mut stones = HashMap::new();
13-
for v in parser::u64().repeat(b' ', 1).parse_complete(input)? {
14-
*stones.entry(v).or_insert(0) += 1;
17+
let mut builder = Builder::new(75);
18+
19+
let mut counts = Vec::new();
20+
for n in parser::u64().repeat(b' ', 1).parse_complete(input)? {
21+
let idx = builder.index(n, 0);
22+
if idx >= counts.len() {
23+
counts.resize(idx + 1, 0);
24+
}
25+
counts[idx] += 1;
1526
}
16-
Ok(Self { stones })
27+
28+
// Precompute all stone divisions
29+
let (next, max_idx) = builder.finish();
30+
31+
Ok(Self {
32+
counts,
33+
next,
34+
max_idx,
35+
})
1736
}
1837

1938
#[must_use]
2039
pub fn part1(&self) -> u64 {
21-
self.blink(25)
40+
self.stones(25)
2241
}
2342

2443
#[must_use]
2544
pub fn part2(&self) -> u64 {
26-
self.blink(75)
45+
self.stones(75)
2746
}
2847

29-
fn blink(&self, times: u32) -> u64 {
30-
let mut stones = self.stones.clone();
31-
let mut next = HashMap::new();
32-
for _ in 0..times {
33-
for (&x, &c) in stones.iter() {
34-
if x == 0 {
35-
next.entry(1).and_modify(|v| *v += c).or_insert(c);
36-
} else {
37-
let log = x.ilog10() + 1;
38-
if log % 2 == 0 {
39-
let pow = 10u64.pow(log / 2);
40-
next.entry(x / pow).and_modify(|v| *v += c).or_insert(c);
41-
next.entry(x % pow).and_modify(|v| *v += c).or_insert(c);
42-
} else {
43-
next.entry(x * 2024).and_modify(|v| *v += c).or_insert(c);
44-
}
48+
fn stones(&self, blinks: usize) -> u64 {
49+
let mut counts = vec![0; self.next.len()];
50+
let mut next = vec![0; self.next.len()];
51+
counts[..self.counts.len()].copy_from_slice(&self.counts);
52+
53+
for blink in 0..blinks {
54+
for (c, &(a, b)) in counts[..=self.max_idx[blink]].iter_mut().zip(&self.next) {
55+
if *c > 0 {
56+
next[a] += *c;
57+
next[b] += *c;
58+
*c = 0;
4559
}
4660
}
4761

48-
(stones, next) = (next, stones);
49-
next.clear();
62+
// Clear placeholder stones
63+
next[0] = 0;
64+
65+
(counts, next) = (next, counts);
66+
}
67+
68+
counts.iter().sum()
69+
}
70+
}
71+
72+
struct Builder {
73+
num_map: HashMap<u64, usize>,
74+
next: Vec<(usize, usize)>,
75+
max_idx: Vec<usize>,
76+
todo: VecDeque<(u64, usize, u32)>,
77+
}
78+
79+
impl Builder {
80+
fn new(blinks: u32) -> Self {
81+
let mut num_map = HashMap::with_capacity(5000);
82+
let mut next = Vec::with_capacity(5000);
83+
84+
// Always insert placeholder stone as index 0
85+
num_map.insert(PLACEHOLDER, 0);
86+
next.push((0, 0));
87+
88+
Self {
89+
num_map,
90+
next,
91+
todo: VecDeque::with_capacity(500),
92+
max_idx: vec![0; blinks as usize],
93+
}
94+
}
95+
96+
fn index(&mut self, n: u64, blinks: u32) -> usize {
97+
let next_idx = self.num_map.len();
98+
*self.num_map.entry(n).or_insert_with(|| {
99+
self.next.push((0, 0));
100+
self.max_idx[blinks as usize] = self.max_idx[blinks as usize].max(next_idx);
101+
if (blinks as usize) < self.max_idx.len() {
102+
self.todo.push_back((n, next_idx, blinks));
103+
}
104+
next_idx
105+
})
106+
}
107+
108+
fn finish(mut self) -> (Vec<(usize, usize)>, Vec<usize>) {
109+
while let Some((n, idx, blink)) = self.todo.pop_front() {
110+
self.next[idx] = if n == 0 {
111+
(self.index(1, blink + 1), 0)
112+
} else {
113+
let log = n.ilog10() + 1;
114+
if log % 2 == 0 {
115+
let pow = 10u64.pow(log / 2);
116+
(
117+
self.index(n / pow, blink + 1),
118+
self.index(n % pow, blink + 1),
119+
)
120+
} else {
121+
(self.index(n * 2024, blink + 1), 0)
122+
}
123+
};
50124
}
51-
stones.values().sum()
125+
126+
// Max index is an optimization to reduce the number of indexes iterated over in the first
127+
// blinks. Ensure it is always increasing, as the insert function only updates it when
128+
// adding a new number, which means that blinks with no new numbers will have max_idx = 0
129+
// without this.
130+
for i in 1..self.max_idx.len() {
131+
self.max_idx[i] = self.max_idx[i].max(self.max_idx[i - 1]);
132+
}
133+
134+
(self.next, self.max_idx)
52135
}
53136
}
54137

0 commit comments

Comments
 (0)