advent-2022-rust/src/days/day15/mod.rs
2023-08-07 05:50:26 +02:00

344 lines
9.5 KiB
Rust

use super::template::{DayTrait, ResultType};
use crate::common::{
parser::{eol_terminated, extract_result, ignore, trim0},
pos::Pos2D,
};
use itertools::Itertools;
use nom::{
bytes::complete::tag,
character::complete::{char, i64},
error::Error,
multi::many0,
sequence::{separated_pair, tuple},
Err, IResult,
};
use std::collections::HashSet;
use thiserror::Error;
const DAY_NUMBER: usize = 15;
pub struct Day;
fn parse_row(input: &str) -> IResult<&str, i64> {
eol_terminated(i64)(input)
}
impl DayTrait for Day {
fn get_day_number(&self) -> usize {
DAY_NUMBER
}
fn part1(&self, lines: &str) -> anyhow::Result<ResultType> {
let (lines, row) = parse_row(lines).map_err(|_| SensorError::RowLineNotFound)?;
let (sensors, beacons) = Day::parse_all(lines)?;
let result = Day::count_coverage_at(&sensors, &beacons, row);
Ok(ResultType::Integer(result))
}
fn part2(&self, lines: &str) -> anyhow::Result<ResultType> {
let (lines, _) = parse_row(lines).map_err(|_| SensorError::RowLineNotFound)?;
let (sensors, _) = Day::parse_all(lines)?;
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::<Vec<_>>();
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: &str) -> Result<(HashSet<Sensor>, HashSet<Pos2D<i64>>), SensorError> {
let data = extract_result(many0(eol_terminated(Sensor::parse)))(lines)?;
let mut sensors = HashSet::new();
let mut beacons = HashSet::new();
for (sensor, beacon) in data {
sensors.insert(sensor);
beacons.insert(beacon);
}
Ok((sensors, beacons))
}
fn count_coverage_at(
sensors: &HashSet<Sensor>,
beacons: &HashSet<Pos2D<i64>>,
row: i64,
) -> i64 {
let ranges = sensors
.iter()
.filter_map(|sensor| sensor.range_at(row))
.collect::<Vec<_>>();
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<Sensor>) -> Vec<Line> {
sensors
.iter()
.tuple_combinations()
.filter_map(|(first, second)| Line::create(first, second))
.collect()
}
}
#[derive(Debug, Error)]
enum SensorError {
#[error("Error Parsing Input: {0}")]
ParsingError(String),
#[error("Did not find exactly one point for the sensor: {0}")]
NotExactlyOneResult(usize),
#[error("Row line not found at start of Input")]
RowLineNotFound,
}
impl From<Err<Error<&str>>> for SensorError {
fn from(error: Err<Error<&str>>) -> Self {
SensorError::ParsingError(error.to_string())
}
}
#[derive(Debug)]
struct Line {
start: Pos2D<i64>,
is_up: bool,
steps: i64,
}
impl Line {
pub fn create(first: &Sensor, second: &Sensor) -> Option<Line> {
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 = Pos2D::new(one.pos.x(), one.pos.y() + one.radius + 1);
} else {
start = Pos2D::new(two.pos.x() - two.radius - 1, two.pos.y());
}
} else {
is_up = false;
if one.pos.y() - one.radius >= two.pos.y() {
start = Pos2D::new(one.pos.x(), one.pos.y() - one.radius - 1);
} else {
start = Pos2D::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<Pos2D<i64>> {
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 + Pos2D::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<Range> {
let mut ranges: Vec<Range> = 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: Pos2D<i64>,
radius: i64,
}
impl Sensor {
fn component<'a>(name: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, i64> {
move |input: &str| {
let input = ignore(tuple((tag(name), char('='))))(input)?;
trim0(i64)(input)
}
}
fn parse_pos(input: &str) -> IResult<&str, Pos2D<i64>> {
let (input, (x, y)) = separated_pair(
Sensor::component("x"),
trim0(char(',')),
Sensor::component("y"),
)(input)?;
Ok((input, Pos2D::new(x, y)))
}
pub fn parse(input: &str) -> IResult<&str, (Sensor, Pos2D<i64>)> {
let input = ignore(tag("Sensor at"))(input)?;
let (input, pos) = trim0(Sensor::parse_pos)(input)?;
let input = ignore(tag(": closest beacon is at"))(input)?;
let (input, beacon) = trim0(Sensor::parse_pos)(input)?;
let radius = pos.taxicab_between(beacon);
Ok((input, (Sensor { pos, radius }, beacon)))
}
pub fn range_at(&self, y: i64) -> Option<Range> {
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_between(other.pos);
distance - self.radius - other.radius
}
pub fn contains(&self, pos: Pos2D<i64>) -> bool {
self.pos.taxicab_between(pos) <= self.radius
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::common::file::read_string;
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: Pos2D::new(2, 18),
radius: 7,
},
Pos2D::new(-2, 15),
);
let result = extract_result(Sensor::parse)(input)?;
assert_eq!(result, expected);
Ok(())
}
#[test]
fn test_width() {
let sensor = Sensor {
pos: Pos2D::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_string(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_string(day.get_day_number(), "example01.txt")?;
let expected = ResultType::Integer(56000011);
let result = day.part2(&lines)?;
assert_eq!(result, expected);
Ok(())
}
}