diff --git a/2024/day8/Cargo.lock b/2024/day8/Cargo.lock new file mode 100644 index 0000000..0a523ad --- /dev/null +++ b/2024/day8/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day8" +version = "0.1.0" diff --git a/2024/day8/Cargo.toml b/2024/day8/Cargo.toml new file mode 100644 index 0000000..cc06d1e --- /dev/null +++ b/2024/day8/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "day8" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/2024/day8/README.md b/2024/day8/README.md new file mode 100644 index 0000000..a46dc22 --- /dev/null +++ b/2024/day8/README.md @@ -0,0 +1,51 @@ +# Solution Day 8 + +A bit of geometry for a change. The task was quite ambiguous, I felt. The text says: + +> In particular, an antinode occurs at any point that is perfectly in line with two antennas of the same frequency - but only when one of the antennas is twice as far away as the other + +and then goes on to give exambles, where the antinodes lie outside the two antennas, like this: + +``` +..#....a....a....#.... +``` + +Either I'm not getting the exact meaning of "antinode" (English is not my native language) or there's another set of antinodes _between_ the two antennas: + +``` +..#....a.##.a....#.... +``` + +Those should also fulfill the requirement of one antenna being twice as far away as the other. +Anyway, going with the examples, this is basically just calculating an equation of the form +`p = mn + b`, with `p`, and `b` being points in 2D space, `m = a - b` (the vector between +antennas `a` and `b`), and `n` being in the natural numbers **N**. + +Implementing this as infrastructure (along with loading and parsing the data), makes the actual tasks rather simple. + +## Task 1 + +The main point in this task is to only include points of order 1 formed by 2 different antennas, hence +why the inner nested loop in `get_antinodes` has to always start one index _above_ the current outer loop, +to never pair an antenna with itself (I got this wrong in the first try). Those nested loops will form all possible pairs +of the antennas: + +``` +[1,2,3] => [[1,2], [1,3], [2,3]] +``` + +## Task 2 + +The change to the code are actually miniscule: antennas can now interfere with themselves (presumably if there's +another antenna of the same frequency somewhere on the map. No idea how this makes physical sense, but there you are...) and +we need to continue our antinodes beyond order 1 until we're outside the map. + +Since we already had a "outside the map"-filter in place from task 1, the lazy solution was to calculate the antinodes +up to an order, that is _definitely_ outside the map in order to capture all possible nodes (hence why calculating the the 50th +node, even if that could never be on the map). + +This also pairs antennas with themselves, so the inner loop in `get_antinodes` now starts at the index of the outer node. + +``` +[1,2,3] => [[1,1], [1,2], [1,3], [2,2], [2,3], [3,3]] +``` \ No newline at end of file diff --git a/2024/day8/src/main.rs b/2024/day8/src/main.rs new file mode 100644 index 0000000..7c925a7 --- /dev/null +++ b/2024/day8/src/main.rs @@ -0,0 +1,114 @@ +use std::collections::{HashMap, HashSet}; + +struct Antenna { + x: usize, + y: usize, + marker: char, +} + +struct AntennaMap { + width: usize, + height: usize, + antennas: Vec, +} + +#[derive(Hash, PartialEq, Eq)] +struct Antinode { + x: usize, + y: usize, +} + +fn load_input() -> AntennaMap { + let input = std::fs::read_to_string("./input.txt").expect("Should be able to read input file"); + let lines: Vec<&str> = input.lines().collect(); + let antennas: Vec = lines + .iter() + .enumerate() + .flat_map(|(y, line)| { + line.chars().enumerate().filter_map(move |(x, ch)| { + if ch != '.' { + Some(Antenna { x, y, marker: ch }) + } else { + None + } + }) + }) + .collect(); + AntennaMap { + width: lines.iter().next().unwrap().len(), + height: lines.len(), + antennas, + } +} + +fn group_antennas(antennas: &Vec) -> HashMap> { + let mut map = HashMap::new(); + for antenna in antennas.into_iter() { + if !map.contains_key(&antenna.marker) { + map.insert(antenna.marker, vec![]); + } + map.get_mut(&antenna.marker).unwrap().push(antenna); + } + map +} + +fn get_nth_antinode(antenna_a: &Antenna, antenna_b: &Antenna, n: usize) -> Option { + if (n + 1) * antenna_a.x >= n * antenna_b.x && (n + 1) * antenna_a.y >= n * antenna_b.y { + Some(Antinode { + x: (n + 1) * antenna_a.x - n * antenna_b.x, + y: (n + 1) * antenna_a.y - n * antenna_b.y, + }) + } else { + None + } +} + +fn get_antinodes( + antennas: &Vec<&Antenna>, + width: usize, + height: usize, + max_n: usize, + include_self: bool, +) -> Vec { + let mut antinodes = vec![]; + for (idx, antenna_a) in antennas.iter().enumerate() { + for antenna_b in antennas[idx + (if include_self { 0 } else { 1 })..].iter() { + for n in 1..=max_n { + antinodes.push(get_nth_antinode(antenna_a, antenna_b, n)); + antinodes.push(get_nth_antinode(antenna_b, antenna_a, n)); + } + } + } + antinodes + .into_iter() + .filter_map(|node_opt| node_opt) + .filter(|node| node.x < width && node.y < height) + .collect() +} + +fn task1() { + let antenna_map = load_input(); + let by_frequency = group_antennas(&antenna_map.antennas); + let all_antinodes = by_frequency + .iter() + .flat_map(|entry| get_antinodes(entry.1, antenna_map.width, antenna_map.height, 1, false)); + let unique_antinodes: HashSet = all_antinodes.collect(); + + println!("Task 1: Unique nodes: {}", unique_antinodes.len()); +} + +fn task2() { + let antenna_map = load_input(); + let by_frequency = group_antennas(&antenna_map.antennas); + let all_antinodes = by_frequency + .iter() + .flat_map(|entry| get_antinodes(entry.1, antenna_map.width, antenna_map.height, 50, true)); // arbitrarily high n to make sure we get everything inside the map + let unique_antinodes: HashSet = all_antinodes.collect(); + + println!("Task 2: Unique nodes: {}", unique_antinodes.len()); +} + +fn main() { + task1(); + task2(); +}