From 3b12de497099b9d3dc18904ad76c50e95135583e Mon Sep 17 00:00:00 2001 From: Ruediger Ludwig Date: Mon, 13 Feb 2023 19:30:59 +0100 Subject: [PATCH] day 15 finished --- data/day15/example01.txt | 15 ++ data/day15/input.txt | 32 ++++ src/common/pos.rs | 17 +++ src/days/day15/mod.rs | 317 +++++++++++++++++++++++++++++++++++++++ src/days/mod.rs | 4 +- 5 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 data/day15/example01.txt create mode 100644 data/day15/input.txt create mode 100644 src/days/day15/mod.rs diff --git a/data/day15/example01.txt b/data/day15/example01.txt new file mode 100644 index 0000000..6d6c325 --- /dev/null +++ b/data/day15/example01.txt @@ -0,0 +1,15 @@ +10 +Sensor at x=2, y=18: closest beacon is at x=-2, y=15 +Sensor at x=9, y=16: closest beacon is at x=10, y=16 +Sensor at x=13, y=2: closest beacon is at x=15, y=3 +Sensor at x=12, y=14: closest beacon is at x=10, y=16 +Sensor at x=10, y=20: closest beacon is at x=10, y=16 +Sensor at x=14, y=17: closest beacon is at x=10, y=16 +Sensor at x=8, y=7: closest beacon is at x=2, y=10 +Sensor at x=2, y=0: closest beacon is at x=2, y=10 +Sensor at x=0, y=11: closest beacon is at x=2, y=10 +Sensor at x=20, y=14: closest beacon is at x=25, y=17 +Sensor at x=17, y=20: closest beacon is at x=21, y=22 +Sensor at x=16, y=7: closest beacon is at x=15, y=3 +Sensor at x=14, y=3: closest beacon is at x=15, y=3 +Sensor at x=20, y=1: closest beacon is at x=15, y=3 \ No newline at end of file diff --git a/data/day15/input.txt b/data/day15/input.txt new file mode 100644 index 0000000..e1e288b --- /dev/null +++ b/data/day15/input.txt @@ -0,0 +1,32 @@ +2000000 +Sensor at x=3859432, y=2304903: closest beacon is at x=3677247, y=3140958 +Sensor at x=2488890, y=2695345: closest beacon is at x=1934788, y=2667279 +Sensor at x=3901948, y=701878: closest beacon is at x=4095477, y=368031 +Sensor at x=2422190, y=1775708: closest beacon is at x=1765036, y=2000000 +Sensor at x=2703846, y=3282799: closest beacon is at x=2121069, y=3230302 +Sensor at x=172003, y=2579074: closest beacon is at x=-77667, y=3197309 +Sensor at x=1813149, y=1311283: closest beacon is at x=1765036, y=2000000 +Sensor at x=1704453, y=2468117: closest beacon is at x=1934788, y=2667279 +Sensor at x=1927725, y=2976002: closest beacon is at x=1934788, y=2667279 +Sensor at x=3176646, y=1254463: closest beacon is at x=2946873, y=2167634 +Sensor at x=2149510, y=3722117: closest beacon is at x=2121069, y=3230302 +Sensor at x=3804434, y=251015: closest beacon is at x=4095477, y=368031 +Sensor at x=2613561, y=3932220: closest beacon is at x=2121069, y=3230302 +Sensor at x=3997794, y=3291220: closest beacon is at x=3677247, y=3140958 +Sensor at x=98328, y=3675176: closest beacon is at x=-77667, y=3197309 +Sensor at x=2006541, y=2259601: closest beacon is at x=1934788, y=2667279 +Sensor at x=663904, y=122919: closest beacon is at x=1618552, y=-433244 +Sensor at x=1116472, y=3349728: closest beacon is at x=2121069, y=3230302 +Sensor at x=2810797, y=2300748: closest beacon is at x=2946873, y=2167634 +Sensor at x=1760767, y=2024355: closest beacon is at x=1765036, y=2000000 +Sensor at x=3098487, y=2529092: closest beacon is at x=2946873, y=2167634 +Sensor at x=1716839, y=634872: closest beacon is at x=1618552, y=-433244 +Sensor at x=9323, y=979154: closest beacon is at x=-245599, y=778791 +Sensor at x=1737623, y=2032367: closest beacon is at x=1765036, y=2000000 +Sensor at x=26695, y=3049071: closest beacon is at x=-77667, y=3197309 +Sensor at x=3691492, y=3766350: closest beacon is at x=3677247, y=3140958 +Sensor at x=730556, y=1657010: closest beacon is at x=1765036, y=2000000 +Sensor at x=506169, y=3958647: closest beacon is at x=-77667, y=3197309 +Sensor at x=2728744, y=23398: closest beacon is at x=1618552, y=-433244 +Sensor at x=3215227, y=3077078: closest beacon is at x=3677247, y=3140958 +Sensor at x=2209379, y=3030851: closest beacon is at x=2121069, y=3230302 \ No newline at end of file diff --git a/src/common/pos.rs b/src/common/pos.rs index d5ffb07..5e692a2 100644 --- a/src/common/pos.rs +++ b/src/common/pos.rs @@ -22,6 +22,10 @@ impl Pos where T: Num + Copy, { + pub fn splat(v: T) -> Pos { + Pos(v, v) + } + pub fn x(&self) -> T { self.0 } @@ -259,3 +263,16 @@ where Pos::mul(*self, *rhs) } } + +impl Pos +where + T: Num + Signed + Copy, +{ + pub fn taxicab_origin(&self) -> T { + self.0.abs() + self.1.abs() + } + + pub fn taxicab(&self, other: &Pos) -> T { + (self.0 - other.0).abs() + (self.1 - other.1).abs() + } +} diff --git a/src/days/day15/mod.rs b/src/days/day15/mod.rs new file mode 100644 index 0000000..0614b9e --- /dev/null +++ b/src/days/day15/mod.rs @@ -0,0 +1,317 @@ +use super::template::{DayTrait, ResultType}; +use crate::common::pos::Pos; +use itertools::Itertools; +use lazy_static::lazy_static; +use regex::Regex; +use std::{collections::HashSet, num::ParseIntError}; +use thiserror::Error; + +const DAY_NUMBER: usize = 15; + +pub struct Day; + +impl DayTrait for Day { + fn get_day_number(&self) -> usize { + DAY_NUMBER + } + + fn part1(&self, lines: &[String]) -> anyhow::Result { + let row = lines[0].parse()?; + let (sensors, beacons) = Day::parse_all(&lines[1..])?; + let result = Day::count_coverage_at(&sensors, &beacons, row); + Ok(ResultType::Integer(result)) + } + + fn part2(&self, lines: &[String]) -> anyhow::Result { + let (sensors, _) = Day::parse_all(&lines[1..])?; + let mid_lines = Day::dividing_lines(&sensors); + let points = mid_lines + .iter() + .tuple_combinations() + .filter_map(|(first, second)| first.cross(second)) + .filter(|pos| sensors.iter().all(|sensor| !sensor.contains(pos))) + .dedup() + .collect::>(); + + if points.len() != 1 { + Err(SensorError::NotExactlyOneResult(points.len()))?; + } + + let point = points.first().unwrap(); + Ok(ResultType::Integer(point.x() * 4000000 + point.y())) + } +} + +impl Day { + fn parse_all(lines: &[String]) -> Result<(HashSet, HashSet>), SensorError> { + let mut sensors = HashSet::new(); + let mut beacons = HashSet::new(); + for line in lines { + let (sensor, beacon) = Sensor::parse(line)?; + sensors.insert(sensor); + beacons.insert(beacon); + } + Ok((sensors, beacons)) + } + + fn count_coverage_at(sensors: &HashSet, beacons: &HashSet>, row: i64) -> i64 { + let ranges = sensors + .iter() + .filter_map(|sensor| sensor.range_at(row)) + .collect::>(); + let merged: i64 = Range::merge_all(&ranges) + .iter() + .map(|range| range.width()) + .sum(); + let sensors = sensors + .iter() + .filter(|sensor| sensor.pos.y() == row) + .count() as i64; + let beacons = beacons.iter().filter(|beacons| beacons.y() == row).count() as i64; + + merged - (sensors + beacons) + } + + fn dividing_lines(sensors: &HashSet) -> Vec { + sensors + .iter() + .tuple_combinations() + .filter_map(|(first, second)| Line::create(first, second)) + .collect() + } +} + +#[derive(Debug, Error)] +enum SensorError { + #[error("Not an Integer")] + NotAnInt(#[from] ParseIntError), + + #[error("Unknown line: {0}")] + UnknownLine(String), + + #[error("Did not find exactly one point for the sensor: {0}")] + NotExactlyOneResult(usize), +} + +#[derive(Debug)] +struct Line { + start: Pos, + is_up: bool, + steps: i64, +} + +impl Line { + pub fn create(first: &Sensor, second: &Sensor) -> Option { + if first.border_distance(second) != 2 { + return None; + } + + let (one, two) = if first.pos.x() < second.pos.x() { + (first, second) + } else { + (second, first) + }; + + let start; + let is_up; + if one.pos.y() < two.pos.y() { + is_up = true; + if one.pos.y() + one.radius <= two.pos.y() { + start = Pos::new(one.pos.x(), one.pos.y() + one.radius + 1); + } else { + start = Pos::new(two.pos.x() - two.radius - 1, two.pos.y()); + } + } else { + is_up = false; + if one.pos.y() - one.radius >= two.pos.y() { + start = Pos::new(one.pos.x(), one.pos.y() - one.radius - 1); + } else { + start = Pos::new(two.pos.x() - two.radius - 1, two.pos.y()); + } + } + let steps = two.pos.x().min(one.pos.x() + one.radius) - start.x(); + + Some(Line { + start, + is_up, + steps, + }) + } + + fn cross(&self, other: &Line) -> Option> { + if self.is_up == other.is_up { + return None; + } + let (bottom_up, top_down) = if self.is_up { + (self, other) + } else { + (other, self) + }; + + let r2 = + bottom_up.start.x() + bottom_up.start.y() - (top_down.start.x() + top_down.start.y()); + if r2 % 2 != 0 { + return None; + } + + let r = r2 / 2; + if r < 0 || r > top_down.steps { + return None; + } + + let pos = top_down.start + Pos::splat(r); + Some(pos) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Range(i64, i64); + +impl Range { + fn overlaps(&self, other: &Range) -> bool { + self.0 <= other.1 && other.0 <= self.1 + } + + pub fn width(&self) -> i64 { + self.1 - self.0 + 1 + } + + pub fn merge_all(ranges: &[Range]) -> Vec { + let mut ranges: Vec = ranges.iter().sorted().copied().collect(); + assert!(!ranges.is_empty()); + loop { + let mut next = vec![]; + let mut merged = false; + let mut current = ranges[0]; + for range in &ranges[1..] { + if current.overlaps(range) { + current = Range(current.0, current.1.max(range.1)); + merged = true; + } else { + next.push(current); + current = *range; + } + } + next.push(current); + + if merged { + ranges = next; + } else { + return next; + } + } + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] +struct Sensor { + pos: Pos, + radius: i64, +} + +lazy_static! { + static ref SENSOR: Regex = Regex::new(r"x=(-?\d+), y=(-?\d+).*x=(-?\d+), y=(-?\d+)").unwrap(); +} + +impl Sensor { + pub fn parse(line: &str) -> Result<(Sensor, Pos), SensorError> { + let caps = SENSOR + .captures(line) + .ok_or(SensorError::UnknownLine(line.to_owned()))?; + let sensor_x = caps.get(1).unwrap().as_str().parse()?; + let sensor_y = caps.get(2).unwrap().as_str().parse()?; + let beacon_x = caps.get(3).unwrap().as_str().parse()?; + let beacon_y = caps.get(4).unwrap().as_str().parse()?; + let sensor = Pos::new(sensor_x, sensor_y); + let beacon = Pos::new(beacon_x, beacon_y); + let radius = sensor.taxicab(&beacon); + Ok(( + Sensor { + pos: sensor, + radius, + }, + beacon, + )) + } + + pub fn range_at(&self, y: i64) -> Option { + let distance = (self.pos.y() - y).abs(); + if distance > self.radius { + None + } else { + let extent = self.radius - distance; + Some(Range(self.pos.x() - extent, self.pos.x() + extent)) + } + } + + pub fn border_distance(&self, other: &Sensor) -> i64 { + let distance = self.pos.taxicab(&other.pos); + distance - self.radius - other.radius + } + + pub fn contains(&self, pos: &Pos) -> bool { + self.pos.taxicab(pos) <= self.radius + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::common::file::read_lines; + use anyhow::Result; + #[test] + fn test_parse() -> Result<()> { + let input = "Sensor at x=2, y=18: closest beacon is at x=-2, y=15"; + let expected = ( + Sensor { + pos: Pos::new(2, 18), + radius: 7, + }, + Pos::new(-2, 15), + ); + let result = Sensor::parse(input)?; + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn test_width() { + let sensor = Sensor { + pos: Pos::new(8, 7), + radius: 9, + }; + assert_eq!(sensor.range_at(17), None); + assert_eq!(sensor.range_at(7), Some(Range(-1, 17))); + assert_eq!(sensor.range_at(10), Some(Range(2, 14))); + } + + #[test] + fn test_merge() { + let input = vec![Range(4, 10), Range(1, 3), Range(5, 9), Range(8, 12)]; + let expected = vec![Range(1, 3), Range(4, 12)]; + assert_eq!(Range::merge_all(&input), expected); + } + + #[test] + fn test_part1() -> Result<()> { + let day = Day {}; + let lines = read_lines(day.get_day_number(), "example01.txt")?; + let expected = ResultType::Integer(26); + let result = day.part1(&lines)?; + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn test_part2() -> Result<()> { + let day = Day {}; + let lines = read_lines(day.get_day_number(), "example01.txt")?; + let expected = ResultType::Integer(56000011); + let result = day.part2(&lines)?; + assert_eq!(result, expected); + + Ok(()) + } +} diff --git a/src/days/mod.rs b/src/days/mod.rs index ce2ea67..e93a410 100644 --- a/src/days/mod.rs +++ b/src/days/mod.rs @@ -12,6 +12,7 @@ mod day11; mod day12; mod day13; mod day14; +mod day15; mod template; pub use template::DayTrait; @@ -21,7 +22,7 @@ pub mod day_provider { use super::*; use thiserror::Error; - const MAX_DAY: usize = 14; + const MAX_DAY: usize = 15; pub fn get_day(day_num: usize) -> Result, ProviderError> { match day_num { @@ -39,6 +40,7 @@ pub mod day_provider { 12 => Ok(Box::new(day12::Day)), 13 => Ok(Box::new(day13::Day)), 14 => Ok(Box::new(day14::Day)), + 15 => Ok(Box::new(day15::Day)), _ => Err(ProviderError::InvalidNumber(day_num)), } }