|
| 1 | +package sschr15.aocsolutions |
| 2 | + |
| 3 | +import com.sschr15.aoc.annotations.Memoize |
| 4 | +import sschr15.aocsolutions.util.* |
| 5 | + |
| 6 | +/** |
| 7 | + * AOC 2024 [Day 21](https://adventofcode.com/2024/day/21) |
| 8 | + * Challenge: |
| 9 | + */ |
| 10 | +object Day21 : Challenge { |
| 11 | + override fun solve() = challenge(2024, 21) { |
| 12 | + test() |
| 13 | + |
| 14 | + // Adapted from lukebemish's solution - this puzzle completely destroyed my hopes and dreams |
| 15 | + |
| 16 | + val keypad = """ |
| 17 | + 789 |
| 18 | + 456 |
| 19 | + 123 |
| 20 | + 0A |
| 21 | + """.trimIndent().lines().toGrid() |
| 22 | + val keyMap = keypad.toPointMap().map { (k, v) -> v to k }.toMap() |
| 23 | + |
| 24 | + val directions = """ |
| 25 | + | ^A |
| 26 | + |<v> |
| 27 | + """.trimMargin().lines().toGrid() |
| 28 | + val dirMap = directions.toPointMap().map { (k, v) -> v to k }.toMap() |
| 29 | + |
| 30 | + fun getWays(start: Point, value: Char, map: Map<Char, Point>): List<String> { |
| 31 | + var reverse = true |
| 32 | + val s = buildString { |
| 33 | + val end = map[value]!! |
| 34 | + var startX = start.x |
| 35 | + if (startX == 0 && end.y == map[' ']!!.y) { |
| 36 | + if (0 < end.x) { |
| 37 | + append(">".repeat(end.x)) |
| 38 | + startX = end.x |
| 39 | + } |
| 40 | + reverse = false |
| 41 | + } else if (start.y == map[' ']!!.y && end.x == 0) { |
| 42 | + reverse = false |
| 43 | + } |
| 44 | + |
| 45 | + if (start.y > end.y) append("^".repeat(start.y - end.y)) |
| 46 | + if (start.y < end.y) append("v".repeat(end.y - start.y)) |
| 47 | + if (startX > end.x) append("<".repeat(startX - end.x)) |
| 48 | + if (startX < end.x) append(">".repeat(end.x - startX)) |
| 49 | + } |
| 50 | + |
| 51 | + if (s == s.reversed() || !reverse) return listOf(s) |
| 52 | + return listOf(s, s.reversed()) |
| 53 | + } |
| 54 | + |
| 55 | + fun findWays(sequence: String, map: Map<Char, Point>): List<String> { |
| 56 | + var results = listOf(emptyList<Char>() to map['A']!!) |
| 57 | + sequence.forEach { c -> |
| 58 | + results = results.flatMap { (path, start) -> |
| 59 | + getWays(start, c, map).map { s -> (path + s.toList() + 'A') to map[c]!! } |
| 60 | + } |
| 61 | + } |
| 62 | + val min = results.minOf { (path) -> path.size } |
| 63 | + return results.mapNotNull { (path) -> path.takeIf { it.size == min }?.joinToString("") } |
| 64 | + } |
| 65 | + |
| 66 | + @Memoize |
| 67 | + fun shortestDirectionalPath(iterations: Int, goal: String): Long { |
| 68 | + if (iterations == -1) return goal.length.toLong() |
| 69 | + val sections = goal.split('A').dropLastWhile { it.isEmpty() }.map { it + "A" } |
| 70 | + return sections.sumOf { section -> findWays(section, dirMap).minOf { shortestDirectionalPath(iterations - 1, it) } } |
| 71 | + } |
| 72 | + |
| 73 | + fun shortestKeypadPath(iterations: Int, goal: String): Long { |
| 74 | + // Separate function to keep from memoization |
| 75 | + return findWays(goal, keyMap).minOf { shortestDirectionalPath(iterations - 1, it) } |
| 76 | + } |
| 77 | + |
| 78 | + part1 { |
| 79 | + inputLines.sumOf { shortestKeypadPath(2, it) * it.takeWhile(Char::isDigit).toLong() } |
| 80 | + } |
| 81 | + part2 { |
| 82 | + inputLines.sumOf { shortestKeypadPath(25, it).also(::println) * it.takeWhile(Char::isDigit).toLong() } |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + @JvmStatic |
| 87 | + fun main(args: Array<String>) = println(solve()) |
| 88 | +} |
0 commit comments