diff --git a/src/main/kotlin/me/peckb/aoc/_2024/calendar/day21/Day21.kt b/src/main/kotlin/me/peckb/aoc/_2024/calendar/day21/Day21.kt new file mode 100644 index 00000000..bdded484 --- /dev/null +++ b/src/main/kotlin/me/peckb/aoc/_2024/calendar/day21/Day21.kt @@ -0,0 +1,112 @@ +package me.peckb.aoc._2024.calendar.day21 + +import javax.inject.Inject +import me.peckb.aoc.generators.InputGenerator.InputGeneratorFactory +import java.util.LinkedList +import kotlin.math.sign + +class Day21 @Inject constructor( + private val generatorFactory: InputGeneratorFactory, +) { + fun partOne(filename: String) = generatorFactory.forFile(filename).read { input -> + solve(input, indirections = 3) + } + + fun partTwo(filename: String) = generatorFactory.forFile(filename).read { input -> + solve(input, indirections = 26) + } + + private fun solve(input: Sequence, indirections: Int) : Long { + return input.sumOf { code -> + val length = getLength(NUMERIC_PAD, code, indirections) + val size = code.take(3).toLong() + + length * size + } + } + + private fun getLength(keyPad: Map>, code: String, indirections: Int): Long { + val lengthKey = LengthKey(keyPad.size, code, indirections) + + if (LENGTH_CACHE.containsKey(lengthKey)) { + return LENGTH_CACHE[lengthKey]!! + } + + if (indirections == 0) { + return code.length.toLong().also { LENGTH_CACHE[lengthKey] = it } + } + + val minLength = "A${code}".toList() + .windowed(2) + .sumOf { (s, e) -> + paths(keyPad, s, e).minOf { getLength(DIRECTION_PAD, "${it}A", indirections - 1) } + } + + return minLength.also { LENGTH_CACHE[lengthKey] = it } + } + + private fun paths(keyPad: Map>, start: Char, end: Char): List { + val pathKey = PathKey(keyPad.size, start, end) + + if (PATHS_CACHE.containsKey(pathKey)) { + return PATHS_CACHE[pathKey]!! + } + + val paths = mutableListOf() + val partialPaths = LinkedList, String>>().apply { + add(keyPad[start]!! to "") + } + + pathGeneration@ while(partialPaths.isNotEmpty()) { + val (position, path) = partialPaths.poll() + + val target = keyPad[end]!! + if (position == target) { + paths.add(path) + continue@pathGeneration + } + + val yDelta = target.second - position.second + if (yDelta != 0) { + val newPoint = position.first to position.second + yDelta.sign + if (newPoint in keyPad.values) { + val dir = if (yDelta > 0) { ">" } else { "<" } + partialPaths.add(newPoint to "$path$dir") + } + } + + val xDelta = target.first - position.first + if (xDelta != 0) { + val newPoint = position.first + xDelta.sign to position.second + if (newPoint in keyPad.values) { + val dir = if (xDelta > 0) { "v" } else { "^" } + partialPaths.add(newPoint to "$path$dir") + } + } + } + + return paths.also { PATHS_CACHE[pathKey] = it } + } + + companion object { + val NUMERIC_PAD = mapOf( + '7' to (0 to 0), '8' to (0 to 1), '9' to (0 to 2), + '4' to (1 to 0), '5' to (1 to 1), '6' to (1 to 2), + '1' to (2 to 0), '2' to (2 to 1), '3' to (2 to 2), + '0' to (3 to 1), 'A' to (3 to 2), + ) + + val DIRECTION_PAD = mapOf( + '^' to (0 to 1), 'A' to (0 to 2), + '<' to (1 to 0), 'v' to (1 to 1), '>' to (1 to 2) + ) + + val LENGTH_CACHE = mutableMapOf() + + val PATHS_CACHE = mutableMapOf>() + } +} + +data class LengthKey(val padIndicator: Int, val pattern: String, val robots: Int) + +data class PathKey(val padIndicator: Int, val start: Char, val end: Char) diff --git a/src/main/kotlin/me/peckb/aoc/_2024/calendar/day21/README.md b/src/main/kotlin/me/peckb/aoc/_2024/calendar/day21/README.md new file mode 100644 index 00000000..d1e98914 --- /dev/null +++ b/src/main/kotlin/me/peckb/aoc/_2024/calendar/day21/README.md @@ -0,0 +1 @@ +## [Day 21: Keypad Conundrum](https://adventofcode.com/2024/day/21) \ No newline at end of file diff --git a/src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt b/src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt index f98577b5..e9a907ea 100644 --- a/src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt +++ b/src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt @@ -20,6 +20,7 @@ import me.peckb.aoc._2024.calendar.day17.Day17Test import me.peckb.aoc._2024.calendar.day18.Day18Test import me.peckb.aoc._2024.calendar.day19.Day19Test import me.peckb.aoc._2024.calendar.day20.Day20Test +import me.peckb.aoc._2024.calendar.day21.Day21Test import javax.inject.Singleton import me.peckb.aoc.DayComponent import me.peckb.aoc.InputModule @@ -48,4 +49,5 @@ internal interface TestDayComponent : DayComponent { fun inject(day18Test: Day18Test) fun inject(day19Test: Day19Test) fun inject(day20Test: Day20Test) + fun inject(day21Test: Day21Test) } diff --git a/src/test/kotlin/me/peckb/aoc/_2024/calendar/day21/Day21Test.kt b/src/test/kotlin/me/peckb/aoc/_2024/calendar/day21/Day21Test.kt new file mode 100644 index 00000000..7bc2d70a --- /dev/null +++ b/src/test/kotlin/me/peckb/aoc/_2024/calendar/day21/Day21Test.kt @@ -0,0 +1,32 @@ +package me.peckb.aoc._2024.calendar.day21 + +import javax.inject.Inject + +import me.peckb.aoc._2024.DaggerTestDayComponent +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class Day21Test { + @Inject + lateinit var day21: Day21 + + @BeforeEach + fun setup() { + DaggerTestDayComponent.create().inject(this) + } + + @Test + fun testDay21PartOne() { + assertEquals(246_990, day21.partOne(DAY_21)) + } + + @Test + fun testDay21PartTwo() { + assertEquals(306_335_137_543_664, day21.partTwo(DAY_21)) + } + + companion object { + private const val DAY_21: String = "advent-of-code-input/2024/day21.input" + } +}