Skip to content

Commit 219d864

Browse files
committed
Pre-compute initial explode actions
During part two, we are computing a pairing between every two lines. Any line that had one or more depth 4 number pairs will cause the same set of explode actions to be taken for each of the other 99 lines it is paired with, before we get into the main effort of add() in processing split actions. Rather than repeat early work that will produce the same answer, we can instead pre-compute these initial explode calls during parse time. On my input, adding a simple counter shows that this reduces the number of calls to explode() from 180k down to 131k. parse() and part1() runtimes are about the same, while part2() speeds up from 600us to 560us.
1 parent 4c8375f commit 219d864

1 file changed

Lines changed: 74 additions & 19 deletions

File tree

src/year2021/day18.rs

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,25 @@
2424
//!
2525
//! This means that we can store each snailfish number as an implicit data structure in a fixed-size
2626
//! array. This is faster, smaller and more convenient than using a traditional struct with pointers.
27-
//! The root node is stored at index 1 (index 0 is unused). For a node at index `i` its left child
28-
//! is at index `2i`, right child at index `2i + 1` and parent at index `i / 2`. As leaf nodes are
29-
//! always greater than or equal to zero, `-1` is used as a special sentinel value for non-leaf nodes.
27+
//! The root node is stored at index 1 (index 0 is unused by the tree, but see below). For a
28+
//! node at index `i` its left child is at index `2i`, right child at index `2i + 1` and parent
29+
//! at index `i / 2`. As leaf nodes are always greater than or equal to zero, `-1` is used as a
30+
//! special sentinel value for non-leaf nodes.
31+
//!
32+
//! Another optimization is realizing that all of the explode actions before the first split can
33+
//! be pre-computed. Instead of parsing a line `[1,[2,[3,[4,5]]]]` as written, we instead parse it
34+
//! it as if it had been `[[1,[2,[3,[4,5]]]],0]`, then perform the initial explode actions for that
35+
//! line up front, such that node 3 contains the value that must be added to the first leaf of a
36+
//! right-hand value in summation. Node 0 is not used by the implicit tree structure, so we instead
37+
//! use it as a tri-state value:
38+
//!
39+
//! - -2 means this Snailfish number is the result of a sum, for the left side during part one.
40+
//! When passed to `add()`, we must shuffle contents one level lower.
41+
//! - -1 means this Snailfish number was just parsed, but did not explode left. When used on the
42+
//! right side of add, any spillover from the left is added to the first leaf on the right.
43+
//! - Non-negative means this Snailfish number was just parsed, and had an explode that spilled
44+
//! left. When used as the right side of an add, any spill from the left is combined with
45+
//! this value, then added to the last leaf on the left.
3046
use crate::util::parse::*;
3147
use crate::util::thread::*;
3248

@@ -49,8 +65,12 @@ pub fn parse(input: &str) -> Vec<Snailfish> {
4965
input
5066
.lines()
5167
.map(|line: &str| {
68+
// Treat the line as if it had been `[line,0]`, then perform explode until it is back
69+
// at depth 4. This allows later add() operations to do less work. Index 0 and 3
70+
// then track the amount spilled left or right from those explodes.
5271
let mut tree = [-1; 64];
53-
let mut i = 1;
72+
tree[3] = 0;
73+
let mut i = 2;
5474

5575
for b in line.bytes() {
5676
match b {
@@ -60,6 +80,11 @@ pub fn parse(input: &str) -> Vec<Snailfish> {
6080
b => tree[i] = b.to_decimal() as i32,
6181
}
6282
}
83+
for pair in (32..48).step_by(2) {
84+
if tree[pair] >= 0 {
85+
explode(&mut tree, pair);
86+
}
87+
}
6388

6489
tree
6590
})
@@ -100,29 +125,56 @@ fn worker(iter: ParIter<'_, (&Snailfish, &Snailfish)>) -> Option<i32> {
100125
/// The initial step creates a new root node then makes the numbers the left and right children
101126
/// of this new root node, by copying the respective ranges of the implicit trees.
102127
///
103-
/// We can optimize the rules a little. This initial combination is the only time that more than one
104-
/// pair will be 4 levels deep simultaneously, so we can sweep from left to right on all possible
105-
/// leaf nodes in one pass.
128+
/// We can optimize the rules a little. The parse step already ensured that there are no pairs
129+
/// deeper than 4 levels, and precomputed any explode values to spill between the two halves
130+
/// of the joined value. All that remains is checking for splits, where each split also takes
131+
/// care of any additional explodes needed.
106132
fn add(left: &Snailfish, right: &Snailfish) -> Snailfish {
107133
let mut tree = [-1; 64];
108134

109-
tree[4..6].copy_from_slice(&left[2..4]);
110-
tree[8..12].copy_from_slice(&left[4..8]);
111-
tree[16..24].copy_from_slice(&left[8..16]);
112-
tree[32..48].copy_from_slice(&left[16..32]);
113-
114-
tree[6..8].copy_from_slice(&right[2..4]);
115-
tree[12..16].copy_from_slice(&right[4..8]);
116-
tree[24..32].copy_from_slice(&right[8..16]);
117-
tree[48..64].copy_from_slice(&right[16..32]);
135+
if left[0] == -2 {
136+
// Left comes from a running sum during part one. We need to increase the depth, which
137+
// in turn might cause some depth 5 leaves that need explode.
138+
tree[3] = 0;
139+
tree[4..6].copy_from_slice(&left[2..4]);
140+
tree[8..12].copy_from_slice(&left[4..8]);
141+
tree[16..24].copy_from_slice(&left[8..16]);
142+
tree[32..48].copy_from_slice(&left[16..32]);
143+
144+
for pair in (32..48).step_by(2) {
145+
if tree[pair] >= 0 {
146+
explode(&mut tree, pair);
147+
}
148+
}
149+
} else {
150+
// We are adding two just-parsed numbers; the left is already rooted at 2 and has no depth 5
151+
// leaves, making it ready to copy into place.
152+
tree[3..24].copy_from_slice(&left[3..24]);
153+
}
118154

119-
for pair in (32..64).step_by(2) {
120-
if tree[pair] >= 0 {
121-
explode(&mut tree, pair);
155+
// Copy the right into place. This value is always just-parsed, with no depth 5 leaves.
156+
tree[6..8].copy_from_slice(&right[4..6]);
157+
tree[12..16].copy_from_slice(&right[8..12]);
158+
tree[24..32].copy_from_slice(&right[16..24]);
159+
160+
// Adjust by the explode spillover between sides. We ensured that tree[3] contains any
161+
// value to spill right, but must check right[0] to see if that sum then spills back left.
162+
let (mut i, spill) = if right[0] == -1 { (24, tree[3]) } else { (23, tree[3] + right[0]) };
163+
loop {
164+
if tree[i] >= 0 {
165+
tree[i] += spill;
166+
break;
122167
}
168+
i /= 2;
123169
}
170+
tree[3] = -1;
124171

172+
// Now we process all split operations; any further explode actions are done during the split
173+
// that creates a temporary depth 5.
125174
while split(&mut tree) {}
175+
176+
// Mark this tree as a sum before returning it.
177+
tree[0] = -2;
126178
tree
127179
}
128180

@@ -145,6 +197,9 @@ fn explode(tree: &mut Snailfish, pair: usize) {
145197
}
146198
i /= 2;
147199
}
200+
} else {
201+
// Store the left spill-out for later use by add().
202+
tree[0] = tree[pair];
148203
}
149204

150205
if pair < 62 {

0 commit comments

Comments
 (0)