2023-12-05 18:49:36 -08:00
|
|
|
use num::traits;
|
2023-12-05 16:01:40 -08:00
|
|
|
use std::fs::File;
|
|
|
|
use std::io::{BufRead, BufReader, Lines};
|
2023-12-05 18:49:36 -08:00
|
|
|
use std::ops::Range;
|
2023-12-05 16:01:40 -08:00
|
|
|
|
|
|
|
// --- Day 3: Gear Ratios ---
|
|
|
|
|
|
|
|
// You and the Elf eventually reach a gondola lift station; he says the gondola lift
|
|
|
|
// will take you up to the water source, but this is as far as he can bring you. You go
|
|
|
|
// inside.
|
|
|
|
|
|
|
|
// It doesn't take long to find the gondolas, but there seems to be a problem: they're
|
|
|
|
// not moving.
|
|
|
|
|
|
|
|
// "Aaah!"
|
|
|
|
|
|
|
|
// You turn around to see a slightly-greasy Elf with a wrench and a look of surprise.
|
|
|
|
// "Sorry, I wasn't expecting anyone! The gondola lift isn't working right now; it'll
|
|
|
|
// still be a while before I can fix it." You offer to help.
|
|
|
|
|
|
|
|
// The engineer explains that an engine part seems to be missing from the engine, but
|
|
|
|
// nobody can figure out which one. If you can add up all the part numbers in the engine
|
|
|
|
// schematic, it should be easy to work out which part is missing.
|
|
|
|
|
|
|
|
// The engine schematic (your puzzle input) consists of a visual representation of the
|
|
|
|
// engine. There are lots of numbers and symbols you don't really understand, but
|
|
|
|
// apparently any number adjacent to a symbol, even diagonally, is a "part number" and
|
|
|
|
// should be included in your sum. (Periods (.) do not count as a symbol.)
|
|
|
|
|
|
|
|
// Here is an example engine schematic:
|
|
|
|
|
|
|
|
// 467..114.. ...*...... ..35..633. ......#... 617*...... .....+.58. ..592.....
|
|
|
|
// ......755. ...$.*.... .664.598..
|
|
|
|
|
|
|
|
// In this schematic, two numbers are not part numbers because they are not adjacent to
|
|
|
|
// a symbol: 114 (top right) and 58 (middle right). Every other number is adjacent to a
|
|
|
|
// symbol and so is a part number; their sum is 4361.
|
|
|
|
|
|
|
|
// BOILERPLATE
|
|
|
|
type InputIter = Lines<BufReader<File>>;
|
|
|
|
|
|
|
|
fn get_input() -> InputIter {
|
|
|
|
let f = File::open("input").unwrap();
|
|
|
|
let br = BufReader::new(f);
|
|
|
|
br.lines()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
println!("Problem 1 solution: {}", problem1(get_input()));
|
|
|
|
println!("Problem 2 solution: {}", problem2(get_input()));
|
|
|
|
}
|
|
|
|
|
|
|
|
// PARSING
|
|
|
|
|
|
|
|
// We will store the schematic as a 2D Vector of char
|
|
|
|
|
2023-12-05 18:49:36 -08:00
|
|
|
#[derive(Debug)]
|
2023-12-05 16:01:40 -08:00
|
|
|
struct Pos(usize, usize);
|
|
|
|
|
|
|
|
// Outer Vec holds rows, so addressing is column then row
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct SchematicRow(Vec<char>);
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct Schematic(Vec<SchematicRow>);
|
|
|
|
|
|
|
|
impl From<&str> for SchematicRow {
|
|
|
|
fn from(s: &str) -> Self {
|
|
|
|
SchematicRow(s.chars().collect())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<InputIter> for Schematic {
|
|
|
|
fn from(input: InputIter) -> Self {
|
|
|
|
Self(input.map(|line| line.unwrap().as_str().into()).collect())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const ADJACENCY_OFFSETS: &[(i64, i64)] = &[
|
|
|
|
(-1, -1),
|
|
|
|
(0, -1),
|
|
|
|
(1, -1),
|
|
|
|
(-1, 0),
|
|
|
|
(1, 0),
|
|
|
|
(-1, 1),
|
|
|
|
(0, 1),
|
|
|
|
(1, 1),
|
|
|
|
];
|
|
|
|
|
|
|
|
// We assume the schematic is composed of equal length lines
|
|
|
|
impl Schematic {
|
2023-12-05 18:49:36 -08:00
|
|
|
fn in_bounds<T: PartialOrd + num::traits::Zero + TryFrom<usize>>(&self, x: T, y: T) -> bool {
|
|
|
|
let x_bound: T = self.0[0]
|
|
|
|
.0
|
|
|
|
.len()
|
|
|
|
.try_into()
|
|
|
|
.unwrap_or_else(|_| panic!("bad bounds"));
|
|
|
|
let y_bound: T = self
|
|
|
|
.0
|
|
|
|
.len()
|
|
|
|
.try_into()
|
|
|
|
.unwrap_or_else(|_| panic!("bad bounds"));
|
|
|
|
x >= T::zero() && x < x_bound && y >= T::zero() && y < y_bound
|
|
|
|
}
|
2023-12-05 16:01:40 -08:00
|
|
|
fn at(&self, pos: &Pos) -> char {
|
|
|
|
self.0[pos.1].0[pos.0]
|
|
|
|
}
|
|
|
|
|
|
|
|
fn adjacent_to_symbol(&self, pos: &Pos) -> bool {
|
|
|
|
ADJACENCY_OFFSETS.iter().any(|(x_ofs, y_ofs)| {
|
|
|
|
let adj_x = (pos.0 as i64) + x_ofs;
|
|
|
|
let adj_y = (pos.1 as i64) + y_ofs;
|
|
|
|
|
2023-12-05 18:49:36 -08:00
|
|
|
if !self.in_bounds(adj_x, adj_y) {
|
2023-12-05 16:01:40 -08:00
|
|
|
false
|
|
|
|
} else {
|
|
|
|
let adj_c = self.at(&Pos(adj_x as usize, adj_y as usize));
|
|
|
|
adj_c != '.' && adj_c.is_ascii_punctuation()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2023-12-05 18:49:36 -08:00
|
|
|
|
|
|
|
fn is_gear(&self, pos: &Pos) -> bool {
|
|
|
|
if self.at(pos) != '*' {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
ADJACENCY_OFFSETS
|
|
|
|
.iter()
|
|
|
|
.filter(|(x_ofs, y_ofs)| {
|
|
|
|
let adj_x = (pos.0 as i64) + x_ofs;
|
|
|
|
let adj_y = (pos.1 as i64) + y_ofs;
|
|
|
|
|
|
|
|
if adj_x < 0
|
|
|
|
|| adj_x >= self.0[pos.1].0.len() as i64
|
|
|
|
|| adj_y < 0
|
|
|
|
|| adj_y >= self.0.len() as i64
|
|
|
|
{
|
|
|
|
false
|
|
|
|
} else {
|
|
|
|
let adj_c = self.at(&Pos(adj_x as usize, adj_y as usize));
|
|
|
|
adj_c.is_digit(10)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.count()
|
|
|
|
>= 2
|
|
|
|
}
|
|
|
|
|
|
|
|
fn gear_ratio(&self, pos: &Pos) -> Option<u64> {
|
|
|
|
if self.at(pos) != '*' {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut exclusions: Vec<(Range<usize>, i64)> = Vec::new();
|
|
|
|
let mut nums = (0u64, 0u64);
|
|
|
|
|
|
|
|
for (x_ofs, y_ofs) in ADJACENCY_OFFSETS {
|
|
|
|
let adj_x = (pos.0 as i64) + x_ofs;
|
|
|
|
let adj_y = (pos.1 as i64) + y_ofs;
|
|
|
|
if !self.in_bounds(adj_x, adj_y)
|
|
|
|
|| exclusions
|
|
|
|
.iter()
|
|
|
|
.any(|(ex_x_range, ex_y)| *ex_y == adj_y && ex_x_range.contains(&(adj_x as usize)))
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let adj_c = self.at(&Pos(adj_x as usize, adj_y as usize));
|
|
|
|
if adj_c.is_digit(10) {
|
|
|
|
let mut num_start_x = adj_x as usize;
|
|
|
|
loop {
|
|
|
|
if num_start_x > 0
|
|
|
|
&& self
|
|
|
|
.at(&Pos((num_start_x - 1) as usize, adj_y as usize))
|
|
|
|
.is_digit(10)
|
|
|
|
{
|
|
|
|
num_start_x -= 1;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let mut num_end_x = adj_x as usize;
|
|
|
|
loop {
|
|
|
|
if self.in_bounds(num_end_x + 1, adj_y as usize)
|
|
|
|
&& self
|
|
|
|
.at(&Pos((num_end_x + 1) as usize, adj_y as usize))
|
|
|
|
.is_digit(10)
|
|
|
|
{
|
|
|
|
num_end_x += 1;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// we have the bounds for the number, extract a slice
|
|
|
|
let num_slice = &self.0[adj_y as usize].0[num_start_x..num_end_x + 1];
|
|
|
|
let mut num = 0u64;
|
|
|
|
for c in num_slice {
|
|
|
|
num = num * 10 + c.to_digit(10).unwrap() as u64;
|
|
|
|
}
|
|
|
|
if nums.0 == 0 {
|
|
|
|
nums.0 = num
|
|
|
|
} else {
|
|
|
|
nums.1 = num;
|
|
|
|
return Some(nums.0 * nums.1);
|
|
|
|
}
|
|
|
|
exclusions.push((num_start_x..num_end_x + 1, adj_y));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
2023-12-05 16:01:40 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// PROBLEM 1 solution
|
|
|
|
|
|
|
|
// Of course, the actual engine schematic is much larger. What is the sum of all of the
|
|
|
|
// part numbers in the engine schematic?
|
|
|
|
|
|
|
|
fn problem1_all_valid_part_numbers(schematic: &Schematic) -> Vec<u64> {
|
|
|
|
let mut results = Vec::new();
|
|
|
|
|
|
|
|
let mut cur_pos = Pos(0usize, 0usize);
|
|
|
|
|
|
|
|
while cur_pos.1 < schematic.0.len() {
|
|
|
|
while cur_pos.0 < schematic.0[0].0.len() {
|
|
|
|
let c = schematic.at(&cur_pos);
|
|
|
|
if c.is_digit(10) {
|
|
|
|
let mut valid: bool = schematic.adjacent_to_symbol(&cur_pos);
|
|
|
|
let mut num = c.to_digit(10).unwrap() as u64;
|
|
|
|
loop {
|
|
|
|
if cur_pos.0 + 1 >= schematic.0[0].0.len() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
cur_pos.0 += 1;
|
|
|
|
if schematic.at(&cur_pos).is_digit(10) {
|
|
|
|
num = num * 10 + schematic.at(&cur_pos).to_digit(10).unwrap() as u64;
|
|
|
|
valid = valid || schematic.adjacent_to_symbol(&cur_pos);
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if valid {
|
|
|
|
results.push(num);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cur_pos.0 += 1;
|
|
|
|
}
|
|
|
|
cur_pos.1 += 1;
|
|
|
|
cur_pos.0 = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
results
|
|
|
|
}
|
|
|
|
|
|
|
|
fn problem1(input: InputIter) -> u64 {
|
|
|
|
let schematic: Schematic = input.into();
|
|
|
|
problem1_all_valid_part_numbers(&schematic).iter().sum()
|
|
|
|
}
|
|
|
|
|
|
|
|
// PROBLEM 2 solution
|
|
|
|
|
2023-12-05 18:49:36 -08:00
|
|
|
// --- Part Two ---
|
|
|
|
|
|
|
|
// The engineer finds the missing part and installs it in the engine! As the engine
|
|
|
|
// springs to life, you jump in the closest gondola, finally ready to ascend to the
|
|
|
|
// water source.
|
|
|
|
|
|
|
|
// You don't seem to be going very fast, though. Maybe something is still wrong?
|
|
|
|
// Fortunately, the gondola has a phone labeled "help", so you pick it up and the
|
|
|
|
// engineer answers.
|
|
|
|
|
|
|
|
// Before you can explain the situation, she suggests that you look out the window.
|
|
|
|
// There stands the engineer, holding a phone in one hand and waving with the other.
|
|
|
|
// You're going so slowly that you haven't even left the station. You exit the gondola.
|
|
|
|
|
|
|
|
// The missing part wasn't the only issue - one of the gears in the engine is wrong. A
|
|
|
|
// gear is any * symbol that is adjacent to exactly two part numbers. Its gear ratio is
|
|
|
|
// the result of multiplying those two numbers together.
|
|
|
|
|
|
|
|
// This time, you need to find the gear ratio of every gear and add them all up so that
|
|
|
|
// the engineer can figure out which gear needs to be replaced.
|
|
|
|
|
|
|
|
// Consider the same engine schematic again:
|
|
|
|
|
|
|
|
// 467..114.. ...*...... ..35..633. ......#... 617*...... .....+.58. ..592.....
|
|
|
|
// ......755. ...$.*.... .664.598..
|
|
|
|
|
|
|
|
// In this schematic, there are two gears. The first is in the top left; it has part
|
|
|
|
// numbers 467 and 35, so its gear ratio is 16345. The second gear is in the lower
|
|
|
|
// right; its gear ratio is 451490. (The * adjacent to 617 is not a gear because it is
|
|
|
|
// only adjacent to one part number.) Adding up all of the gear ratios produces 467835.
|
|
|
|
|
|
|
|
// What is the sum of all of the gear ratios in your engine schematic?
|
|
|
|
|
|
|
|
fn problem2_all_gear_ratios(schematic: &Schematic) -> Vec<u64> {
|
|
|
|
let mut gears = Vec::new();
|
|
|
|
for y in 0..schematic.0.len() {
|
|
|
|
for x in 0..schematic.0[0].0.len() {
|
|
|
|
match schematic.gear_ratio(&Pos(x, y)) {
|
|
|
|
Some(ratio) => gears.push(ratio),
|
|
|
|
_ => ()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
gears
|
|
|
|
}
|
|
|
|
|
2023-12-05 16:01:40 -08:00
|
|
|
fn problem2(input: InputIter) -> u64 {
|
2023-12-05 18:49:36 -08:00
|
|
|
let schematic: Schematic = input.into();
|
|
|
|
problem2_all_gear_ratios(&schematic).iter().sum()
|
2023-12-05 16:01:40 -08:00
|
|
|
}
|