Skip to content

Commit 1258223

Browse files
committed
Document Year 2021 Day 22
1 parent 4371240 commit 1258223

File tree

1 file changed

+50
-0
lines changed

1 file changed

+50
-0
lines changed

src/year2021/day22.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
1+
//! # Reactor Reboot
2+
//!
3+
//! The key to solving this problem efficiently is the
4+
//! [inclusion-exclusion principle ](https://en.wikipedia.org/wiki/Inclusion%E2%80%93exclusion_principle).
5+
//!
6+
//! Looking at a two dimensional example
7+
//! ```none
8+
//! ┌──────────────┐A Volume of A: 144
9+
//! │ │ Volume of B: 66
10+
//! │ ┌─────────┐B │ Volume of C: 18
11+
//! │ │ │ │
12+
//! │ │ ┌────┐C │ │
13+
//! │ │ │ │ │ │
14+
//! │ │ └────┘ │ │
15+
//! │ └─────────┘ │
16+
//! └──────────────┘
17+
//! ```
18+
//!
19+
//! Using the inclusion-exclusion principle the remaining size of A is:
20+
//!
21+
//! 144 (initial size) - 66 (overlap with B) - 18 (overlap with C) + 18
22+
//! (overlap between B and C) = 78
23+
//!
24+
//! If there were any triple overlaps we would subtract those, add quadruple, and so on until
25+
//! there are no more overlaps remaining.
26+
//!
27+
//! The complexity of this approach depends on how many cubes overlap. In my input most
28+
//! cubes overlapped with zero others, a few with one and rarely with more than one.
129
use crate::util::iter::*;
230
use crate::util::parse::*;
331

32+
/// Wraps a cube with on/off information.
433
pub struct RebootStep {
534
on: bool,
635
cube: Cube,
@@ -14,6 +43,8 @@ impl RebootStep {
1443
}
1544
}
1645

46+
/// Technically this is actually a [rectangular cuboid](https://en.wikipedia.org/wiki/Cuboid#Rectangular_cuboid)
47+
/// but that was longer to type!
1748
#[derive(Clone, Copy)]
1849
pub struct Cube {
1950
x1: i32,
@@ -25,6 +56,8 @@ pub struct Cube {
2556
}
2657

2758
impl Cube {
59+
/// Keeping the coordinates in ascending order per axis makes calculating intersections
60+
/// and volume easier.
2861
fn from(points: [i32; 6]) -> Cube {
2962
let [a, b, c, d, e, f] = points;
3063
let x1 = a.min(b);
@@ -36,6 +69,7 @@ impl Cube {
3669
Cube { x1, x2, y1, y2, z1, z2 }
3770
}
3871

72+
/// Returns a `Some` of the intersection if two cubes overlap or `None` if they don't.
3973
fn intersect(&self, other: &Cube) -> Option<Cube> {
4074
let x1 = self.x1.max(other.x1);
4175
let x2 = self.x2.min(other.x2);
@@ -46,6 +80,7 @@ impl Cube {
4680
(x1 <= x2 && y1 <= y2 && z1 <= z2).then_some(Cube { x1, x2, y1, y2, z1, z2 })
4781
}
4882

83+
/// Returns the volume of a cube, converting to `i64` to prevent overflow.
4984
fn volume(&self) -> i64 {
5085
let w = (self.x2 - self.x1 + 1) as i64;
5186
let h = (self.y2 - self.y1 + 1) as i64;
@@ -60,6 +95,8 @@ pub fn parse(input: &str) -> Vec<RebootStep> {
6095
first.zip(second).map(RebootStep::from).collect()
6196
}
6297

98+
/// We re-use the logic between part one and two, by first intersecting all cubes with
99+
/// the specified range. Any cubes that lie completely outside the range will be filtered out.
63100
pub fn part1(input: &[RebootStep]) -> i64 {
64101
let region = Cube { x1: -50, x2: 50, y1: -50, y2: 50, z1: -50, z2: 50 };
65102

@@ -76,26 +113,39 @@ pub fn part1(input: &[RebootStep]) -> i64 {
76113
pub fn part2(input: &[RebootStep]) -> i64 {
77114
let mut total = 0;
78115
let mut candidates = Vec::new();
116+
// Only "on" cubes contribute to volume.
117+
// "off" cubes are considered when subtracting volume
79118
let on_cubes = input.iter().enumerate().filter_map(|(i, rs)| rs.on.then_some((i, rs.cube)));
80119

81120
for (i, cube) in on_cubes {
121+
// Only consider cubes after this one in input order.
122+
// Previous cubes have already had all possible intersections subtracted from their
123+
// volume, so no longer need to be considered.
124+
// We check both "on" and "off" cubes when calculating overlaps to subtract volume.
82125
input[(i + 1)..]
83126
.iter()
84127
.filter_map(|rs| cube.intersect(&rs.cube))
85128
.for_each(|next| candidates.push(next));
86129

130+
// Apply the inclusion/exclusion principle recursively, considering overlaps of
131+
// increasingly higher order until there are no more overlaps remaining.
87132
total += cube.volume() + subsets(&cube, -1, &candidates);
88133
candidates.clear();
89134
}
90135

91136
total
92137
}
93138

139+
// Apply inclusion/exclusion principle. The sign of the result alternates with each level,
140+
// so that we subtract single overlaps, then add double, subtract triple, and so on...
94141
fn subsets(cube: &Cube, sign: i64, candidates: &[Cube]) -> i64 {
95142
let mut total = 0;
96143

97144
for (i, other) in candidates.iter().enumerate() {
98145
if let Some(next) = cube.intersect(other) {
146+
// Subtle nuance here. Similar to the main input we only need to consider higher level
147+
// overlaps of inputs *after* this one, as any overlaps with previous cubes
148+
// have already been considered.
99149
total += sign * next.volume() + subsets(&next, -sign, &candidates[(i + 1)..]);
100150
}
101151
}

0 commit comments

Comments
 (0)