From c087e388ac189e96c01ee7ee2e24d9cbb93b4011 Mon Sep 17 00:00:00 2001 From: Matej Focko Date: Tue, 3 Jan 2023 22:15:50 +0100 Subject: [PATCH] day(15): add solution Signed-off-by: Matej Focko --- Cargo.lock | 106 ++++++++++++++++++ Cargo.toml | 2 + samples/day15.txt | 14 +++ src/bin/day15.rs | 279 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 401 insertions(+) create mode 100644 samples/day15.txt create mode 100644 src/bin/day15.rs diff --git a/Cargo.lock b/Cargo.lock index 39aca1e..1c069fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,11 +33,18 @@ dependencies = [ "color-eyre", "itertools", "lazy_static", + "rayon", "regex", "tracing", "tracing-subscriber", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "backtrace" version = "0.3.66" @@ -92,6 +99,49 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + [[package]] name = "either" version = "1.8.0" @@ -114,6 +164,15 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "indenter" version = "0.3.3" @@ -171,6 +230,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.5.4" @@ -190,6 +258,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num_cpus" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_threads" version = "0.1.6" @@ -250,6 +328,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "regex" version = "1.7.0" @@ -282,6 +382,12 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.149" diff --git a/Cargo.toml b/Cargo.toml index a8dd5c7..6a9cd53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,8 @@ color-eyre = "0.6.2" tracing-subscriber = { version = "0.3.16", default-features = true, features = ["env-filter", "local-time"] } regex = "1.7.0" lazy_static = "1.4.0" +rayon = "1.6.1" +# bitvec = "1.0.1" [profile.dev.package.backtrace] opt-level = 3 diff --git a/samples/day15.txt b/samples/day15.txt new file mode 100644 index 0000000..652e631 --- /dev/null +++ b/samples/day15.txt @@ -0,0 +1,14 @@ +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/src/bin/day15.rs b/src/bin/day15.rs new file mode 100644 index 0000000..aae30fb --- /dev/null +++ b/src/bin/day15.rs @@ -0,0 +1,279 @@ +use std::{ + cmp::{max, min}, + collections::BTreeMap, + str::FromStr, +}; + +use aoc_2022::*; + +use color_eyre::Report; +use itertools::Itertools; +use lazy_static::lazy_static; +use rayon::prelude::{IntoParallelIterator, ParallelIterator}; +use regex::Regex; +// use tracing::debug; + +type Input = Vec; +type Output = usize; + +type Coord = Vector2D; + +fn manhattan(u: &Coord, v: &Coord) -> i32 { + let direction = *v - *u; + (*direction.x()).abs() + (*direction.y()).abs() +} + +#[derive(Debug, PartialEq, Eq, Clone)] +struct Sensor { + position: Coord, + beacon: Coord, +} + +impl Sensor { + fn bounds_at_y(&self, y: i32) -> Option<(Coord, Coord)> { + let m = manhattan(&self.position, &self.beacon); + if y < *self.position.y() - m || y > *self.position.y() + m { + return None; + } + + let x = *self.position.x(); + let side_d = m - (*self.position.y() - y).abs(); + + Some((Coord::new(x - side_d, y), Coord::new(x + side_d, y))) + } +} + +lazy_static! { + static ref REGEX: Regex = Regex::new(concat!( + r"Sensor at x=(?P-?\d+), y=(?P-?\d+): ", + r"closest beacon is at x=(?P-?\d+), y=(?P-?\d+)" + )) + .unwrap(); +} + +impl FromStr for Sensor { + type Err = Report; + + fn from_str(s: &str) -> Result { + let caps = REGEX.captures(s).unwrap(); + let (sx, sy, bx, by) = vec!["sx", "sy", "bx", "by"] + .iter() + .map(|&p| caps[p].parse::().unwrap()) + .collect_tuple() + .unwrap(); + + Ok(Sensor { + position: Coord::new(sx, sy), + beacon: Coord::new(bx, by), + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +enum Status { + Sensor, + Beacon, + Cannot, +} + +fn cannot_contain(sensors: &Input, watched_y: i32) -> Output { + let mut positions = BTreeMap::::new(); + + // mark unusable positions + sensors + .iter() + .filter_map(|s| s.bounds_at_y(watched_y)) + .for_each(|(l, r)| { + (*l.x()..=*r.x()).for_each(|x| { + positions.insert(x, Status::Cannot); + }) + }); + + // rewrite beacons and sensors if needed + sensors + .iter() + .filter(|s| *s.position.y() == watched_y || *s.beacon.y() == watched_y) + .for_each(|s| { + if *s.beacon.y() == watched_y { + positions.insert(*s.beacon.x(), Status::Beacon); + } + + if *s.position.y() == watched_y { + positions.insert(*s.beacon.x(), Status::Sensor); + } + }); + + // count unusable + positions + .iter() + .filter(|(_, s)| **s == Status::Cannot) + .count() +} + +fn check_row(sensors: &Input, watched_y: i32, upper_bound: i32) -> Vec<(i32, i32)> { + let mut positions: Vec<(i32, i32)> = Vec::new(); + + // mark unusable positions + positions.extend( + sensors + .iter() + .filter_map(|s| s.bounds_at_y(watched_y)) + .map(|(l, r)| (max(0, *l.x()), min(*r.x(), upper_bound))), + ); + positions.sort(); + + positions +} + +fn find_first_empty(positions: &Vec<(i32, i32)>, upper_bound: i32) -> Option { + let mut x = 0; + for &(from, to) in positions { + if x >= from && x <= to { + x = to + 1; + } + } + + if x <= upper_bound { + return Some(x); + } + + None +} + +fn find_distress(sensors: &Input, upper_bound: usize) -> Output { + let (x, y) = (0..=upper_bound) + .into_par_iter() + .find_map_any(|y| { + let positions = check_row(sensors, y as i32, upper_bound as i32); + + find_first_empty(&positions, upper_bound as i32).map(|x| (x, y)) + }) + .unwrap(); + + x as usize * 4000000 + y +} + +struct Day15; +impl Solution for Day15 { + fn parse_input>(pathname: P) -> Input { + file_to_structs(pathname) + } + + fn part_1(input: &Input) -> Output { + cannot_contain(input, 2000000) + } + + fn part_2(input: &Input) -> Output { + find_distress(input, 4000000) + } +} + +fn main() -> Result<()> { + // Day15::run("sample") + Day15::main() +} + +#[cfg(test)] +mod day_15 { + use super::*; + + #[test] + fn test_part_1() { + let sample = Day15::parse_input(&format!("samples/{}.txt", Day15::day())); + assert_eq!(cannot_contain(&sample, 10), 26); + } + + #[test] + fn test_part_2() { + let sample = Day15::parse_input(&format!("samples/{}.txt", Day15::day())); + assert_eq!(find_distress(&sample, 20), 56000011); + } +} + +#[cfg(test)] +mod day_15_unit { + use super::*; + + #[test] + fn test_manhattan_basic_example() { + let s = Sensor { + position: Coord::new(8, 7), + beacon: Coord::new(2, 10), + }; + assert_eq!(manhattan(&s.position, &s.beacon), 9); + } + + #[test] + fn test_manhattan_close() { + let s = Sensor { + position: Coord::new(0, 11), + beacon: Coord::new(2, 10), + }; + assert_eq!(manhattan(&s.position, &s.beacon), 3); + } + + #[test] + fn test_manhattan_next() { + let s = Sensor { + position: Coord::new(2, 11), + beacon: Coord::new(2, 10), + }; + assert_eq!(manhattan(&s.position, &s.beacon), 1); + } + + #[test] + fn bounds_at_y_basic_example() { + let s = Sensor { + position: Coord::new(8, 7), + beacon: Coord::new(2, 10), + }; + + let bounds = s.bounds_at_y(10); + assert!(bounds.is_some()); + + let (left, right) = bounds.unwrap(); + assert_eq!(left, Coord::new(2, 10)); + assert_eq!(right, Coord::new(14, 10)); + } + + #[test] + fn bounds_at_y_none() { + let s = Sensor { + position: Coord::new(8, 7), + beacon: Coord::new(2, 10), + }; + + let bounds = s.bounds_at_y(20); + assert!(bounds.is_none()); + } + + #[test] + fn bounds_at_y_close_but_not_same() { + let s = Sensor { + position: Coord::new(8, 7), + beacon: Coord::new(9, 7), + }; + + let bounds = s.bounds_at_y(7); + assert!(bounds.is_some()); + + let (left, right) = bounds.unwrap(); + assert_eq!(left, Coord::new(7, 7)); + assert_eq!(right, Coord::new(9, 7)); + } + + #[test] + fn bounds_at_y_close_and_same() { + let s = Sensor { + position: Coord::new(8, 7), + beacon: Coord::new(9, 7), + }; + + let bounds = s.bounds_at_y(6); + assert!(bounds.is_some()); + + let (left, right) = bounds.unwrap(); + assert_eq!(left, Coord::new(8, 6)); + assert_eq!(right, Coord::new(8, 6)); + } +}