|
| 1 | +package me.peckb.aoc._2024.calendar.day20 |
| 2 | + |
| 3 | +import javax.inject.Inject |
| 4 | +import me.peckb.aoc.generators.InputGenerator.InputGeneratorFactory |
| 5 | +import me.peckb.aoc.pathing.GenericIntDijkstra |
| 6 | +import kotlin.math.abs |
| 7 | + |
| 8 | +class Day20 @Inject constructor( |
| 9 | + private val generatorFactory: InputGeneratorFactory, |
| 10 | +) { |
| 11 | + fun partOne(filename: String) = generatorFactory.forFile(filename).read { input -> |
| 12 | + findCheats(input, 2) |
| 13 | + } |
| 14 | + |
| 15 | + fun partTwo(filename: String) = generatorFactory.forFile(filename).read { input -> |
| 16 | + findCheats(input, 20) |
| 17 | + } |
| 18 | + |
| 19 | + private fun findCheats(input: Sequence<String>, maxCheatTime: Int): Int { |
| 20 | + lateinit var end: Location |
| 21 | + |
| 22 | + val maze = mutableListOf<MutableList<Space>>() |
| 23 | + input.forEachIndexed { y, line -> |
| 24 | + val row = mutableListOf<Space>() |
| 25 | + line.forEachIndexed { x, c -> |
| 26 | + when (c) { |
| 27 | + '#' -> row.add(Space.FULL) |
| 28 | + '.' -> row.add(Space.EMPTY) |
| 29 | + 'S' -> row.add(Space.EMPTY) |
| 30 | + 'E' -> row.add(Space.EMPTY.also { end = Location(y, x) } ) |
| 31 | + } |
| 32 | + } |
| 33 | + maze.add(row) |
| 34 | + } |
| 35 | + |
| 36 | + val solver = object : GenericIntDijkstra<Location>() {} |
| 37 | + |
| 38 | + val costs = solver.solve(end.withArea(maze)) |
| 39 | + var cheats = 0 |
| 40 | + |
| 41 | + val explorationRange = (-maxCheatTime .. maxCheatTime) |
| 42 | + val explorationDeltas = explorationRange.flatMapIndexed { y, yStep -> |
| 43 | + explorationRange.mapIndexedNotNull { x, xStep -> |
| 44 | + val stepCount = abs(yStep) + abs(xStep) |
| 45 | + if (stepCount <= maxCheatTime) { |
| 46 | + yStep to xStep |
| 47 | + } else { |
| 48 | + null |
| 49 | + } |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + costs.entries.forEach { (cheatStart, _) -> |
| 54 | + val reachableEmptySpaces = mutableSetOf<Location>() |
| 55 | + |
| 56 | + val curY = cheatStart.y |
| 57 | + val curX = cheatStart.x |
| 58 | + |
| 59 | + explorationDeltas.forEach { (yStep, xStep) -> |
| 60 | + val y = curY + yStep |
| 61 | + val x = curX + xStep |
| 62 | + |
| 63 | + if (y in maze.indices && x in maze[y].indices && maze[y][x] == Space.EMPTY) { |
| 64 | + reachableEmptySpaces.add(Location(y, x)) |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + reachableEmptySpaces.forEach { endSpace -> |
| 69 | + val costAtCheatStart = costs[cheatStart]!! |
| 70 | + val costAtCheatEnd = costs[endSpace]!! |
| 71 | + val distanceTravelled = cheatStart.distanceFrom(endSpace) |
| 72 | + |
| 73 | + val timeSaved = costAtCheatStart - costAtCheatEnd - distanceTravelled |
| 74 | + if (timeSaved >= 100) { cheats++ } |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + return cheats |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +data class Location(val y: Int, val x: Int) : GenericIntDijkstra.DijkstraNode<Location> { |
| 83 | + fun distanceFrom(other: Location): Int { |
| 84 | + return abs(other.y - y) + abs(other.x - x) |
| 85 | + } |
| 86 | + |
| 87 | + lateinit var area: MutableList<MutableList<Space>> |
| 88 | + |
| 89 | + fun withArea(area: MutableList<MutableList<Space>>) = apply { this.area = area } |
| 90 | + |
| 91 | + override fun neighbors(): Map<Location, Int> { |
| 92 | + return Direction.entries.mapNotNull { d -> |
| 93 | + val (newY, newX) = d.yDelta + y to d.xDelta + x |
| 94 | + if (newY in area.indices && newX in area[newY].indices && area[newY][newX] == Space.EMPTY) { |
| 95 | + Location(newY, newX).withArea(area) |
| 96 | + } else { null } |
| 97 | + }.associateWith { 1 } |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +enum class Space { FULL, EMPTY } |
| 102 | + |
| 103 | +enum class Direction(val yDelta: Int, val xDelta: Int) { |
| 104 | + N(-1, 0), |
| 105 | + E(0, 1), |
| 106 | + S(1, 0), |
| 107 | + W(0, -1); |
| 108 | +} |
0 commit comments