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.
1
29
use crate :: util:: iter:: * ;
2
30
use crate :: util:: parse:: * ;
3
31
32
+ /// Wraps a cube with on/off information.
4
33
pub struct RebootStep {
5
34
on : bool ,
6
35
cube : Cube ,
@@ -14,6 +43,8 @@ impl RebootStep {
14
43
}
15
44
}
16
45
46
+ /// Technically this is actually a [rectangular cuboid](https://en.wikipedia.org/wiki/Cuboid#Rectangular_cuboid)
47
+ /// but that was longer to type!
17
48
#[ derive( Clone , Copy ) ]
18
49
pub struct Cube {
19
50
x1 : i32 ,
@@ -25,6 +56,8 @@ pub struct Cube {
25
56
}
26
57
27
58
impl Cube {
59
+ /// Keeping the coordinates in ascending order per axis makes calculating intersections
60
+ /// and volume easier.
28
61
fn from ( points : [ i32 ; 6 ] ) -> Cube {
29
62
let [ a, b, c, d, e, f] = points;
30
63
let x1 = a. min ( b) ;
@@ -36,6 +69,7 @@ impl Cube {
36
69
Cube { x1, x2, y1, y2, z1, z2 }
37
70
}
38
71
72
+ /// Returns a `Some` of the intersection if two cubes overlap or `None` if they don't.
39
73
fn intersect ( & self , other : & Cube ) -> Option < Cube > {
40
74
let x1 = self . x1 . max ( other. x1 ) ;
41
75
let x2 = self . x2 . min ( other. x2 ) ;
@@ -46,6 +80,7 @@ impl Cube {
46
80
( x1 <= x2 && y1 <= y2 && z1 <= z2) . then_some ( Cube { x1, x2, y1, y2, z1, z2 } )
47
81
}
48
82
83
+ /// Returns the volume of a cube, converting to `i64` to prevent overflow.
49
84
fn volume ( & self ) -> i64 {
50
85
let w = ( self . x2 - self . x1 + 1 ) as i64 ;
51
86
let h = ( self . y2 - self . y1 + 1 ) as i64 ;
@@ -60,6 +95,8 @@ pub fn parse(input: &str) -> Vec<RebootStep> {
60
95
first. zip ( second) . map ( RebootStep :: from) . collect ( )
61
96
}
62
97
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.
63
100
pub fn part1 ( input : & [ RebootStep ] ) -> i64 {
64
101
let region = Cube { x1 : -50 , x2 : 50 , y1 : -50 , y2 : 50 , z1 : -50 , z2 : 50 } ;
65
102
@@ -76,26 +113,39 @@ pub fn part1(input: &[RebootStep]) -> i64 {
76
113
pub fn part2 ( input : & [ RebootStep ] ) -> i64 {
77
114
let mut total = 0 ;
78
115
let mut candidates = Vec :: new ( ) ;
116
+ // Only "on" cubes contribute to volume.
117
+ // "off" cubes are considered when subtracting volume
79
118
let on_cubes = input. iter ( ) . enumerate ( ) . filter_map ( |( i, rs) | rs. on . then_some ( ( i, rs. cube ) ) ) ;
80
119
81
120
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.
82
125
input[ ( i + 1 ) ..]
83
126
. iter ( )
84
127
. filter_map ( |rs| cube. intersect ( & rs. cube ) )
85
128
. for_each ( |next| candidates. push ( next) ) ;
86
129
130
+ // Apply the inclusion/exclusion principle recursively, considering overlaps of
131
+ // increasingly higher order until there are no more overlaps remaining.
87
132
total += cube. volume ( ) + subsets ( & cube, -1 , & candidates) ;
88
133
candidates. clear ( ) ;
89
134
}
90
135
91
136
total
92
137
}
93
138
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...
94
141
fn subsets ( cube : & Cube , sign : i64 , candidates : & [ Cube ] ) -> i64 {
95
142
let mut total = 0 ;
96
143
97
144
for ( i, other) in candidates. iter ( ) . enumerate ( ) {
98
145
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.
99
149
total += sign * next. volume ( ) + subsets ( & next, -sign, & candidates[ ( i + 1 ) ..] ) ;
100
150
}
101
151
}
0 commit comments