From d4d3bb90df9e0dc2ef28b2c62c17b2f438ae6874 Mon Sep 17 00:00:00 2001 From: jurisk <4895679+jurisk@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:38:44 +0200 Subject: [PATCH] 2024-21 (#37) --- ReadMe.md | 1 + rust/y2024/resources/21-test-00.txt | 5 + rust/y2024/resources/21.txt | 5 + rust/y2024/src/bin/solution_2024_21.rs | 284 ++++++++++++++++++ scala2/src/main/resources/2024/21-test-00.txt | 6 +- scala2/src/main/resources/2024/21.txt | 6 +- scala2/src/main/resources/2024/22-test-00.txt | 1 + scala2/src/main/resources/2024/22.txt | 1 + .../jurisk/adventofcode/y2024/Advent21.scala | 239 +++++++++++++-- .../jurisk/adventofcode/y2024/Advent22.scala | 48 +++ .../adventofcode/y2024/Advent21Spec.scala | 29 +- .../adventofcode/y2024/Advent22Spec.scala | 30 ++ 12 files changed, 627 insertions(+), 28 deletions(-) create mode 100644 rust/y2024/resources/21-test-00.txt create mode 100644 rust/y2024/resources/21.txt create mode 100644 rust/y2024/src/bin/solution_2024_21.rs create mode 100644 scala2/src/main/resources/2024/22-test-00.txt create mode 100644 scala2/src/main/resources/2024/22.txt create mode 100644 scala2/src/main/scala/jurisk/adventofcode/y2024/Advent22.scala create mode 100644 scala2/src/test/scala/jurisk/adventofcode/y2024/Advent22Spec.scala diff --git a/ReadMe.md b/ReadMe.md index bdfab720..cd15d7d6 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -6,6 +6,7 @@ | Year-Day | Task | Scala | Rust | Others | |----------|:-------------------------------------------------------------------------------|:-----------------------------------------------------------------------:|:----------------------------------------------:|:----------------------------------------------------------------------:| +| 2024-21 | [Keypad Conundrum](https://adventofcode.com/2024/day/21) | [Scala](scala2/src/main/scala/jurisk/adventofcode/y2024/Advent21.scala) | [Rust](rust/y2024/src/bin/solution_2024_21.rs) | | 2024-20 | [Race Condition](https://adventofcode.com/2024/day/20) | [Scala](scala2/src/main/scala/jurisk/adventofcode/y2024/Advent20.scala) | [Rust](rust/y2024/src/bin/solution_2024_20.rs) | | | 2024-19 | [Linen Layout](https://adventofcode.com/2024/day/19) | [Scala](scala2/src/main/scala/jurisk/adventofcode/y2024/Advent19.scala) | [Rust](rust/y2024/src/bin/solution_2024_19.rs) | | | 2024-18 | [RAM Run](https://adventofcode.com/2024/day/18) | [Scala](scala2/src/main/scala/jurisk/adventofcode/y2024/Advent18.scala) | [Rust](rust/y2024/src/bin/solution_2024_18.rs) | | diff --git a/rust/y2024/resources/21-test-00.txt b/rust/y2024/resources/21-test-00.txt new file mode 100644 index 00000000..dd73dfda --- /dev/null +++ b/rust/y2024/resources/21-test-00.txt @@ -0,0 +1,5 @@ +029A +980A +179A +456A +379A \ No newline at end of file diff --git a/rust/y2024/resources/21.txt b/rust/y2024/resources/21.txt new file mode 100644 index 00000000..6db26551 --- /dev/null +++ b/rust/y2024/resources/21.txt @@ -0,0 +1,5 @@ +973A +836A +780A +985A +413A \ No newline at end of file diff --git a/rust/y2024/src/bin/solution_2024_21.rs b/rust/y2024/src/bin/solution_2024_21.rs new file mode 100644 index 00000000..c2f9e77a --- /dev/null +++ b/rust/y2024/src/bin/solution_2024_21.rs @@ -0,0 +1,284 @@ +use std::cmp::Ordering; +use std::str::FromStr; + +use advent_of_code_common::direction::Direction; +use advent_of_code_common::grid2d::Coords; +use advent_of_code_common::parsing::{Error, parse_lines_to_vec}; +use itertools::Itertools; +use memoize::memoize; + +const DATA: &str = include_str!("../../resources/21.txt"); + +type R = usize; + +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +enum NumericButton { + Number(u8), + Activate, + Invalid, +} + +impl NumericButton { + fn coords(self) -> Coords { + match self { + NumericButton::Number(0) => Coords::new(1, 3), + NumericButton::Number(1) => Coords::new(0, 2), + NumericButton::Number(2) => Coords::new(1, 2), + NumericButton::Number(3) => Coords::new(2, 2), + NumericButton::Number(4) => Coords::new(0, 1), + NumericButton::Number(5) => Coords::new(1, 1), + NumericButton::Number(6) => Coords::new(2, 1), + NumericButton::Number(7) => Coords::new(0, 0), + NumericButton::Number(8) => Coords::new(1, 0), + NumericButton::Number(9) => Coords::new(2, 0), + NumericButton::Number(n) => { + panic!("Invalid number {n}") + }, + NumericButton::Activate => Coords::new(2, 3), + NumericButton::Invalid => Coords::new(0, 3), + } + } + + fn path_to(self, b: Self) -> impl Iterator> { + path_between_coords(self.coords(), b.coords(), Self::Invalid.coords()) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +enum DirectionalButton { + Arrow(Direction), + Activate, + Invalid, +} + +impl DirectionalButton { + fn coords(self) -> Coords { + match self { + DirectionalButton::Arrow(Direction::North) => Coords::new(1, 0), + DirectionalButton::Arrow(Direction::East) => Coords::new(2, 1), + DirectionalButton::Arrow(Direction::South) => Coords::new(1, 1), + DirectionalButton::Arrow(Direction::West) => Coords::new(0, 1), + DirectionalButton::Activate => Coords::new(2, 0), + DirectionalButton::Invalid => Coords::new(0, 0), + } + } + + fn path_to(self, b: Self) -> impl Iterator> { + path_between_coords(self.coords(), b.coords(), Self::Invalid.coords()) + } +} + +#[expect(clippy::cast_sign_loss)] +fn path_between_coords( + a: Coords, + b: Coords, + invalid: Coords, +) -> impl Iterator> { + let diff = b - a; + let for_x = match diff.x.cmp(&0) { + Ordering::Less => { + vec![DirectionalButton::Arrow(Direction::West); -diff.x as usize] + }, + Ordering::Equal => { + vec![] + }, + Ordering::Greater => { + vec![DirectionalButton::Arrow(Direction::East); diff.x as usize] + }, + }; + + let for_y = match diff.y.cmp(&0) { + Ordering::Less => { + vec![DirectionalButton::Arrow(Direction::North); -diff.y as usize] + }, + Ordering::Equal => { + vec![] + }, + Ordering::Greater => { + vec![DirectionalButton::Arrow(Direction::South); diff.y as usize] + }, + }; + + let valid_path = move |path: &Vec| -> bool { + let mut current = a; + for b in path { + if let DirectionalButton::Arrow(d) = b { + current += d.diff(); + if current == invalid { + return false; + } + } + } + true + }; + + vec![ + [for_x.clone(), for_y.clone(), vec![ + DirectionalButton::Activate, + ]] + .concat(), + [for_y, for_x, vec![DirectionalButton::Activate]].concat(), + ] + .into_iter() + .filter(valid_path) +} + +#[memoize] +fn move_between_directional_buttons( + a: DirectionalButton, + b: DirectionalButton, + robot_directional_keyboards: usize, +) -> R { + move_between_buttons(a.path_to(b), robot_directional_keyboards) +} + +fn move_between_buttons( + iter: impl IntoIterator>, + robot_directional_keyboards: usize, +) -> R { + iter.into_iter() + .map(|path| move_cost(path, robot_directional_keyboards)) + .min() + .expect("Expected at least one path") +} + +fn move_cost(path: Vec, robot_directional_keyboards: usize) -> R { + if robot_directional_keyboards == 0 { + path.len() + } else { + [vec![DirectionalButton::Activate], path] + .concat() + .into_iter() + .tuple_windows() + .map(|(a, b)| move_between_directional_buttons(a, b, robot_directional_keyboards - 1)) + .sum() + } +} + +#[memoize] +fn move_between_numeric_buttons( + a: NumericButton, + b: NumericButton, + robot_directional_keyboards: usize, +) -> R { + move_between_buttons(a.path_to(b), robot_directional_keyboards) +} + +type Input = Vec; + +#[derive(Clone)] +struct Code { + buttons: Vec, +} + +impl Code { + fn numeric_part(&self) -> R { + self.buttons + .iter() + .filter_map(|button| { + match button { + NumericButton::Number(n) => Some(*n as R), + _ => None, + } + }) + .fold(0, |acc, n| acc * 10 + n) + } + + fn best_human_presses_length(self, robot_directional_keyboards: usize) -> R { + [vec![NumericButton::Activate], self.buttons] + .concat() + .into_iter() + .tuple_windows() + .map(|(a, b)| move_between_numeric_buttons(a, b, robot_directional_keyboards)) + .sum() + } + + fn complexity(self, robot_directional_keyboards: usize) -> R { + self.numeric_part() * self.best_human_presses_length(robot_directional_keyboards) + } +} + +impl FromStr for Code { + type Err = Error; + + fn from_str(s: &str) -> Result { + let buttons: Result, Self::Err> = s + .chars() + .map(|c| { + match c { + x if x.is_ascii_digit() => Ok(NumericButton::Number(x as u8 - b'0')), + 'A' => Ok(NumericButton::Activate), + _ => Err(format!("Invalid button: {c}")), + } + }) + .collect(); + + let buttons = buttons?; + Ok(Code { buttons }) + } +} + +fn parse(input: &str) -> Result { + parse_lines_to_vec(input) +} + +fn solve(data: Input, robot_directional_keyboards: usize) -> R { + data.into_iter() + .map(|code| code.complexity(robot_directional_keyboards)) + .sum() +} + +fn solve_1(data: Input) -> R { + solve(data, 2) +} + +fn solve_2(data: Input) -> R { + solve(data, 25) +} + +fn main() -> Result<(), Error> { + let data = parse(DATA)?; + + let result_1 = solve_1(data.clone()); + println!("Part 1: {result_1}"); + + let result_2 = solve_2(data); + println!("Part 2: {result_2}"); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST_DATA: &str = include_str!("../../resources/21-test-00.txt"); + + fn test_data() -> Input { + parse(TEST_DATA).unwrap() + } + + fn real_data() -> Input { + parse(DATA).unwrap() + } + + #[test] + fn test_solve_1_test() { + assert_eq!(solve_1(test_data()), 126_384); + } + + #[test] + fn test_solve_1_real() { + assert_eq!(solve_1(real_data()), 270_084); + } + + #[test] + fn test_solve_2_test() { + assert_eq!(solve_2(test_data()), 154_115_708_116_294); + } + + #[test] + fn test_solve_2_real() { + assert_eq!(solve_2(real_data()), 329_431_019_997_766); + } +} diff --git a/scala2/src/main/resources/2024/21-test-00.txt b/scala2/src/main/resources/2024/21-test-00.txt index a8b6c947..dd73dfda 100644 --- a/scala2/src/main/resources/2024/21-test-00.txt +++ b/scala2/src/main/resources/2024/21-test-00.txt @@ -1 +1,5 @@ -noop \ No newline at end of file +029A +980A +179A +456A +379A \ No newline at end of file diff --git a/scala2/src/main/resources/2024/21.txt b/scala2/src/main/resources/2024/21.txt index a8b6c947..6db26551 100644 --- a/scala2/src/main/resources/2024/21.txt +++ b/scala2/src/main/resources/2024/21.txt @@ -1 +1,5 @@ -noop \ No newline at end of file +973A +836A +780A +985A +413A \ No newline at end of file diff --git a/scala2/src/main/resources/2024/22-test-00.txt b/scala2/src/main/resources/2024/22-test-00.txt new file mode 100644 index 00000000..a8b6c947 --- /dev/null +++ b/scala2/src/main/resources/2024/22-test-00.txt @@ -0,0 +1 @@ +noop \ No newline at end of file diff --git a/scala2/src/main/resources/2024/22.txt b/scala2/src/main/resources/2024/22.txt new file mode 100644 index 00000000..a8b6c947 --- /dev/null +++ b/scala2/src/main/resources/2024/22.txt @@ -0,0 +1 @@ +noop \ No newline at end of file diff --git a/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent21.scala b/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent21.scala index 75a527ff..9d8438fd 100644 --- a/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent21.scala +++ b/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent21.scala @@ -1,43 +1,238 @@ package jurisk.adventofcode.y2024 +import cats.implicits.catsSyntaxFoldableOps0 +import cats.implicits.catsSyntaxOptionId +import cats.implicits.none +import jurisk.adventofcode.y2024.Advent21.DirectionalButton._ +import jurisk.adventofcode.y2024.Advent21.NumericButton.InvalidNumericCoords +import jurisk.geometry.Coords2D import jurisk.utils.FileInput._ +import jurisk.utils.Memoize import jurisk.utils.Parsing.StringOps object Advent21 { - type Input = List[Command] - type N = Long - - sealed trait Command extends Product with Serializable - object Command { - case object Noop extends Command - final case class Something( - values: List[N] - ) extends Command - final case class Other(value: String) extends Command - - def parse(s: String): Command = - s match { - case "noop" => Noop - case s"something $rem" => Something(rem.extractLongList) - case s if s.nonEmpty => Other(s) - case _ => s.failedToParse - } + type N = Long + + private def pathBetweenCoords( + current: Coords2D, + toPress: Coords2D, + invalid: Coords2D, + ): Set[List[DirectionalButton]] = { + def validDirections(directions: List[DirectionalButton]): Boolean = + !directions + .scanLeft(current) { (coords, d) => + coords + d.actionDiff + } + .contains(invalid) + + val diff = toPress - current + val forX = if (diff.x > 0) { + List.fill(diff.x)(Right) + } else if (diff.x < 0) { + List.fill(-diff.x)(Left) + } else { + Nil + } + val forY = if (diff.y > 0) { + List.fill(diff.y)(Down) + } else if (diff.y < 0) { + List.fill(-diff.y)(Up) + } else { + Nil + } + + Set( + forX ++ forY ++ List(DirectionalButton.Activate), + forY ++ forX ++ List(DirectionalButton.Activate), + ).filter(validDirections) + } + + sealed trait DirectionalButton extends Product with Serializable { + def actionDiff: Coords2D + def coords: Coords2D + + def pathTo(toPress: DirectionalButton): Set[List[DirectionalButton]] = + pathBetweenCoords(this.coords, toPress.coords, InvalidDirectionalCoords) + } + + object DirectionalButton { + private val InvalidDirectionalCoords: Coords2D = Coords2D(0, 0) + + case object Up extends DirectionalButton { + override def actionDiff: Coords2D = Coords2D(0, -1) + override def coords: Coords2D = Coords2D(1, 0) + } + case object Down extends DirectionalButton { + override def actionDiff: Coords2D = Coords2D(0, 1) + override def coords: Coords2D = Coords2D(1, 1) + } + case object Left extends DirectionalButton { + override def actionDiff: Coords2D = Coords2D(-1, 0) + override def coords: Coords2D = Coords2D(0, 1) + } + case object Right extends DirectionalButton { + override def actionDiff: Coords2D = Coords2D(1, 0) + override def coords: Coords2D = Coords2D(2, 1) + } + case object Activate extends DirectionalButton { + override def actionDiff: Coords2D = Coords2D(0, 0) + override def coords: Coords2D = Coords2D(2, 0) + } + + def parseList(s: String): List[DirectionalButton] = s.map { + case '^' => Up + case 'v' => Down + case '<' => Left + case '>' => Right + case 'A' => Activate + }.toList + } + + sealed trait NumericButton extends Product with Serializable { + def coords: Coords2D + def digit: Option[Int] + + def pathTo(toPress: NumericButton): Set[List[DirectionalButton]] = + pathBetweenCoords(this.coords, toPress.coords, InvalidNumericCoords) + } + + object NumericButton { + private val InvalidNumericCoords: Coords2D = Coords2D(0, 3) + + case object Zero extends NumericButton { + override def coords: Coords2D = Coords2D(1, 3) + override def digit: Option[Int] = 0.some + } + case object One extends NumericButton { + override def coords: Coords2D = Coords2D(0, 2) + override def digit: Option[Int] = 1.some + } + case object Two extends NumericButton { + override def coords: Coords2D = Coords2D(1, 2) + override def digit: Option[Int] = 2.some + } + case object Three extends NumericButton { + override def coords: Coords2D = Coords2D(2, 2) + override def digit: Option[Int] = 3.some + } + case object Four extends NumericButton { + override def coords: Coords2D = Coords2D(0, 1) + override def digit: Option[Int] = 4.some + } + case object Five extends NumericButton { + override def coords: Coords2D = Coords2D(1, 1) + override def digit: Option[Int] = 5.some + } + case object Six extends NumericButton { + override def coords: Coords2D = Coords2D(2, 1) + override def digit: Option[Int] = 6.some + } + case object Seven extends NumericButton { + override def coords: Coords2D = Coords2D(0, 0) + override def digit: Option[Int] = 7.some + } + case object Eight extends NumericButton { + override def coords: Coords2D = Coords2D(1, 0) + override def digit: Option[Int] = 8.some + } + case object Nine extends NumericButton { + override def coords: Coords2D = Coords2D(2, 0) + override def digit: Option[Int] = 9.some + } + case object Activate extends NumericButton { + override def coords: Coords2D = Coords2D(2, 3) + override def digit: Option[Int] = none + } + + def parse(ch: Char): NumericButton = ch match { + case '0' => Zero + case '1' => One + case '2' => Two + case '3' => Three + case '4' => Four + case '5' => Five + case '6' => Six + case '7' => Seven + case '8' => Eight + case '9' => Nine + case 'A' => Activate + case _ => "Invalid".fail + } + } + + private val countPairMemo: (DirectionalButton, DirectionalButton, Int) => N = + Memoize.memoize3(countPair) + + private def countPair( + a: DirectionalButton, + b: DirectionalButton, + level: Int, + ): N = + countPaths(a pathTo b, level - 1) + + private def countPaths( + paths: Set[List[DirectionalButton]], + level: Int, + ): N = + paths.map { path => + countPath(path, level) + }.min + + private def countPath( + presses: List[DirectionalButton], + level: Int, + ): N = { + assert(presses.last == DirectionalButton.Activate) + if (level == 0) { + presses.length + } else { + (DirectionalButton.Activate :: presses).sliding2.map { case (a, b) => + countPairMemo(a, b, level) + }.sum + } } + final case class Code(numericButtons: List[NumericButton]) { + val numericPart: N = + numericButtons.flatMap(_.digit).map(_.toString).mkString.toLong + + def complexity(robotDirectionalKeyboards: Int): N = { + println(s"Processing for ${this.numericButtons}") + bestHumanPressesLength(robotDirectionalKeyboards) * numericPart + } + + def bestHumanPressesLength( + robotDirectionalKeyboards: Int + ): N = + (NumericButton.Activate :: numericButtons).sliding2.map { case (a, b) => + countPaths(a pathTo b, robotDirectionalKeyboards) + }.sum + } + + object Code { + def apply(s: String): Code = + new Code(s.map(NumericButton.parse).toList) + } + + type Input = List[Code] + def parse(input: String): Input = - input.parseLines(Command.parse) + input.parseLines(Code(_)) + + def solve(data: Input, robotDirectionalKeyboards: Int): N = + data.map(_.complexity(robotDirectionalKeyboards)).sum def part1(data: Input): N = - 0 + solve(data, 2) def part2(data: Input): N = - 0 + solve(data, 25) def parseFile(fileName: String): Input = parse(readFileText(fileName)) def fileName(suffix: String): String = - s"2024/00$suffix.txt" + s"2024/21$suffix.txt" def main(args: Array[String]): Unit = { val realData: Input = parseFile(fileName("")) diff --git a/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent22.scala b/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent22.scala new file mode 100644 index 00000000..827950fb --- /dev/null +++ b/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent22.scala @@ -0,0 +1,48 @@ +package jurisk.adventofcode.y2024 + +import jurisk.utils.FileInput._ +import jurisk.utils.Parsing.StringOps + +object Advent22 { + type Input = List[Command] + type N = Long + + sealed trait Command extends Product with Serializable + object Command { + case object Noop extends Command + final case class Something( + values: List[N] + ) extends Command + final case class Other(value: String) extends Command + + def parse(s: String): Command = + s match { + case "noop" => Noop + case s"something $rem" => Something(rem.extractLongList) + case s if s.nonEmpty => Other(s) + case _ => s.failedToParse + } + } + + def parse(input: String): Input = + input.parseLines(Command.parse) + + def part1(data: Input): N = + 0 + + def part2(data: Input): N = + 0 + + def parseFile(fileName: String): Input = + parse(readFileText(fileName)) + + def fileName(suffix: String): String = + s"2024/22$suffix.txt" + + def main(args: Array[String]): Unit = { + val realData: Input = parseFile(fileName("")) + + println(s"Part 1: ${part1(realData)}") + println(s"Part 2: ${part2(realData)}") + } +} diff --git a/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent21Spec.scala b/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent21Spec.scala index 4dce4b7f..073429f0 100644 --- a/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent21Spec.scala +++ b/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent21Spec.scala @@ -8,23 +8,44 @@ class Advent21Spec extends AnyFreeSpec { private def testData = parseFile(fileName("-test-00")) private def realData = parseFile(fileName("")) + "utility" - { + "toPressNumericButton" in { + (NumericButton.Activate pathTo NumericButton.Zero) shouldEqual Set( + DirectionalButton.parseList("A"), + DirectionalButton.parseList(">^^A"), + ) + } + } + "part 1" - { + "test 029A" in { + val code = Code("029A") + code.bestHumanPressesLength(2) shouldEqual 68 + code.numericPart shouldEqual 29 + } + "test" in { - part1(testData) shouldEqual 0 + part1(testData) shouldEqual 126384 } "real" in { - part1(realData) shouldEqual 0 + part1(realData) shouldEqual 270084 } } "part 2" - { "test" in { - part2(testData) shouldEqual 0 + part2(testData) shouldEqual 154115708116294L } "real" in { - part2(realData) shouldEqual 0 + part2(realData) shouldEqual 329431019997766L } } } diff --git a/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent22Spec.scala b/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent22Spec.scala new file mode 100644 index 00000000..6474210c --- /dev/null +++ b/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent22Spec.scala @@ -0,0 +1,30 @@ +package jurisk.adventofcode.y2024 + +import Advent22._ +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers._ + +class Advent22Spec extends AnyFreeSpec { + private def testData = parseFile(fileName("-test-00")) + private def realData = parseFile(fileName("")) + + "part 1" - { + "test" in { + part1(testData) shouldEqual 0 + } + + "real" in { + part1(realData) shouldEqual 0 + } + } + + "part 2" - { + "test" in { + part2(testData) shouldEqual 0 + } + + "real" in { + part2(realData) shouldEqual 0 + } + } +}