1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
//! # Reactor Reboot
//!
//! The key to solving this problem efficiently is the
//! [inclusion-exclusion principle ](https://en.wikipedia.org/wiki/Inclusion%E2%80%93exclusion_principle).
//!
//! Looking at a two dimensional example
//! ```none
//! ┌──────────────┐A Volume of A: 144
//! │ │ Volume of B: 66
//! │ ┌─────────┐B │ Volume of C: 18
//! │ │ │ │
//! │ │ ┌────┐C │ │
//! │ │ │ │ │ │
//! │ │ └────┘ │ │
//! │ └─────────┘ │
//! └──────────────┘
//! ```
//!
//! Using the inclusion-exclusion principle the remaining size of A is:
//!
//! 144 (initial size) - 66 (overlap with B) - 18 (overlap with C) + 18
//! (overlap between B and C) = 78
//!
//! If there were any triple overlaps we would subtract those, add quadruple, and so on until
//! there are no more overlaps remaining.
//!
//! The complexity of this approach depends on how many cubes overlap. In my input most
//! cubes overlapped with zero others, a few with one and rarely with more than one.
use crate::util::iter::*;
use crate::util::parse::*;
/// Wraps a cube with on/off information.
pub struct RebootStep {
on: bool,
cube: Cube,
}
impl RebootStep {
fn from((command, points): (&str, [i32; 6])) -> RebootStep {
let on = command == "on";
let cube = Cube::from(points);
RebootStep { on, cube }
}
}
/// Technically this is actually a [rectangular cuboid](https://en.wikipedia.org/wiki/Cuboid#Rectangular_cuboid)
/// but that was longer to type!
#[derive(Clone, Copy)]
pub struct Cube {
x1: i32,
x2: i32,
y1: i32,
y2: i32,
z1: i32,
z2: i32,
}
impl Cube {
/// Keeping the coordinates in ascending order per axis makes calculating intersections
/// and volume easier.
fn from(points: [i32; 6]) -> Cube {
let [a, b, c, d, e, f] = points;
let x1 = a.min(b);
let x2 = a.max(b);
let y1 = c.min(d);
let y2 = c.max(d);
let z1 = e.min(f);
let z2 = e.max(f);
Cube { x1, x2, y1, y2, z1, z2 }
}
/// Returns a `Some` of the intersection if two cubes overlap or `None` if they don't.
fn intersect(&self, other: &Cube) -> Option<Cube> {
let x1 = self.x1.max(other.x1);
let x2 = self.x2.min(other.x2);
let y1 = self.y1.max(other.y1);
let y2 = self.y2.min(other.y2);
let z1 = self.z1.max(other.z1);
let z2 = self.z2.min(other.z2);
(x1 <= x2 && y1 <= y2 && z1 <= z2).then_some(Cube { x1, x2, y1, y2, z1, z2 })
}
/// Returns the volume of a cube, converting to `i64` to prevent overflow.
fn volume(&self) -> i64 {
let w = (self.x2 - self.x1 + 1) as i64;
let h = (self.y2 - self.y1 + 1) as i64;
let d = (self.z2 - self.z1 + 1) as i64;
w * h * d
}
}
pub fn parse(input: &str) -> Vec<RebootStep> {
let first = input.split_ascii_whitespace().step_by(2);
let second = input.iter_signed().chunk::<6>();
first.zip(second).map(RebootStep::from).collect()
}
/// We re-use the logic between part one and two, by first intersecting all cubes with
/// the specified range. Any cubes that lie completely outside the range will be filtered out.
pub fn part1(input: &[RebootStep]) -> i64 {
let region = Cube { x1: -50, x2: 50, y1: -50, y2: 50, z1: -50, z2: 50 };
let filtered: Vec<_> = input
.iter()
.filter_map(|RebootStep { on, cube }| {
region.intersect(cube).map(|next| RebootStep { on: *on, cube: next })
})
.collect();
part2(&filtered)
}
pub fn part2(input: &[RebootStep]) -> i64 {
let mut total = 0;
let mut candidates = Vec::new();
// Only "on" cubes contribute to volume.
// "off" cubes are considered when subtracting volume
let on_cubes = input.iter().enumerate().filter_map(|(i, rs)| rs.on.then_some((i, rs.cube)));
for (i, cube) in on_cubes {
// Only consider cubes after this one in input order.
// Previous cubes have already had all possible intersections subtracted from their
// volume, so no longer need to be considered.
// We check both "on" and "off" cubes when calculating overlaps to subtract volume.
input[(i + 1)..]
.iter()
.filter_map(|rs| cube.intersect(&rs.cube))
.for_each(|next| candidates.push(next));
// Apply the inclusion/exclusion principle recursively, considering overlaps of
// increasingly higher order until there are no more overlaps remaining.
total += cube.volume() + subsets(&cube, -1, &candidates);
candidates.clear();
}
total
}
// Apply inclusion/exclusion principle. The sign of the result alternates with each level,
// so that we subtract single overlaps, then add double, subtract triple, and so on...
fn subsets(cube: &Cube, sign: i64, candidates: &[Cube]) -> i64 {
let mut total = 0;
for (i, other) in candidates.iter().enumerate() {
if let Some(next) = cube.intersect(other) {
// Subtle nuance here. Similar to the main input we only need to consider higher level
// overlaps of inputs *after* this one, as any overlaps with previous cubes
// have already been considered.
total += sign * next.volume() + subsets(&next, -sign, &candidates[(i + 1)..]);
}
}
total
}