diff --git a/src/main/kotlin/y2024/day16/Day16.kt b/src/main/kotlin/y2024/day16/Day16.kt new file mode 100644 index 0000000..65850d9 --- /dev/null +++ b/src/main/kotlin/y2024/day16/Day16.kt @@ -0,0 +1,159 @@ +package y2024.day16 + +import utils.Point +import utils.getInputFile +import utils.toPointGrid +import java.util.* + +fun main() { + println("Part one: " + Day16.solvePartOne()) + println("Part two: " + Day16.solvePartTwo()) +} + +object Day16 { + + private val input = getInputFile(this::class.java.packageName, example = false) + .readLines() + .toPointGrid() + + fun solvePartOne(): Int { + val start = input.entries.find { it.value == 'S' }!!.key + val end = input.entries.find { it.value == 'E' }!!.key + val movementQueue: PriorityQueue> = PriorityQueue(compareBy { it.third }) + movementQueue += Triple(start, Direction.EAST, 0) + + val visited = mutableSetOf(start to Direction.EAST) + val scores = mutableListOf() + + while (movementQueue.isNotEmpty()) { + val (point, direction, score) = movementQueue.poll() + visited += point to direction + + val n = point.n + if (n == end) { + scores += score + 1 + direction.rotationScore(Direction.NORTH) + } else if ((n to Direction.NORTH) !in visited && !input.isWall(n)) { + movementQueue += Triple(n, Direction.NORTH, score + 1 + direction.rotationScore(Direction.NORTH)) + } + + val e = point.e + if (e == end) { + scores += score + 1 + direction.rotationScore(Direction.EAST) + } else if ((e to Direction.EAST) !in visited && !input.isWall(e)) { + movementQueue += Triple(e, Direction.EAST, score + 1 + direction.rotationScore(Direction.EAST)) + } + + val s = point.s + if (s == end) { + scores += score + 1 + direction.rotationScore(Direction.SOUTH) + } else if ((s to Direction.SOUTH) !in visited && !input.isWall(s)) { + movementQueue += Triple(s, Direction.SOUTH, score + 1 + direction.rotationScore(Direction.SOUTH)) + } + + val w = point.w + if (w == end) { + scores += score + 1 + direction.rotationScore(Direction.WEST) + } else if ((w to Direction.WEST) !in visited && !input.isWall(w)) { + movementQueue += Triple(w, Direction.WEST, score + 1 + direction.rotationScore(Direction.WEST)) + } + } + + return scores.min() + } + + data class Path( + val currentPoint: Point, + val currentDirection: Direction, + val score: Int, + val visited: Set, + ) + + data class GoodPath( + val score: Int, + val path: Set, + ) + + fun solvePartTwo(): Int { + val start = input.entries.find { it.value == 'S' }!!.key + val end = input.entries.find { it.value == 'E' }!!.key + val movementQueue: PriorityQueue = PriorityQueue(compareBy { it.score }) + movementQueue += Path(start, Direction.EAST, 0, setOf(start, end)) + + val visited = mutableSetOf(start to Direction.EAST) + val scores = mutableListOf() + + while (movementQueue.isNotEmpty()) { + val (point, direction, score, path) = movementQueue.poll() + visited += point to direction + + val n = point.n + if (n == end) { + scores += GoodPath(score + 1 + direction.rotationScore(Direction.NORTH), path + n) + } else if ((n to Direction.NORTH) !in visited && !input.isWall(n)) { + movementQueue += Path(n, Direction.NORTH, score + 1 + direction.rotationScore(Direction.NORTH), path + n) + } + + val e = point.e + if (e == end) { + scores += GoodPath(score + 1 + direction.rotationScore(Direction.EAST), path + e) + } else if ((e to Direction.EAST) !in visited && !input.isWall(e)) { + movementQueue += Path(e, Direction.EAST, score + 1 + direction.rotationScore(Direction.EAST), path + e) + } + + val s = point.s + if (s == end) { + scores += GoodPath(score + 1 + direction.rotationScore(Direction.SOUTH), path + s) + } else if ((s to Direction.SOUTH) !in visited && !input.isWall(s)) { + movementQueue += Path(s, Direction.SOUTH, score + 1 + direction.rotationScore(Direction.SOUTH), path + s) + } + + val w = point.w + if (w == end) { + scores += GoodPath(score + 1 + direction.rotationScore(Direction.WEST), path + w) + } else if ((w to Direction.WEST) !in visited && !input.isWall(w)) { + movementQueue += Path(w, Direction.WEST, score + 1 + direction.rotationScore(Direction.WEST), path + w) + } + } + + val lowestScore = scores.minOfOrNull { it.score } ?: Integer.MAX_VALUE + + return scores + .filter { it.score == lowestScore } + .flatMap { it.path } + .toSet() + .size + } + + enum class Direction { + NORTH, EAST, SOUTH, WEST; + + fun rotationScore(other: Direction): Int = when (this) { + NORTH -> when (other) { + NORTH -> 0 + EAST -> 1000 + SOUTH -> 2000 + WEST -> 1000 + } + EAST -> when (other) { + NORTH -> 1000 + EAST -> 0 + SOUTH -> 1000 + WEST -> 2000 + } + SOUTH -> when (other) { + NORTH -> 2000 + EAST -> 1000 + SOUTH -> 0 + WEST -> 1000 + } + WEST -> when (other) { + NORTH -> 1000 + EAST -> 2000 + SOUTH -> 1000 + WEST -> 0 + } + } + } + + private fun Map.isWall(point: Point) = get(point) == '#' +} diff --git a/src/main/kotlin/y2024/day16/example.txt b/src/main/kotlin/y2024/day16/example.txt new file mode 100644 index 0000000..6a5bb85 --- /dev/null +++ b/src/main/kotlin/y2024/day16/example.txt @@ -0,0 +1,15 @@ +############### +#.......#....E# +#.#.###.#.###.# +#.....#.#...#.# +#.###.#####.#.# +#.#.#.......#.# +#.#.#####.###.# +#...........#.# +###.#.#####.#.# +#...#.....#.#.# +#.#.#.###.#.#.# +#.....#...#.#.# +#.###.#.#.#.#.# +#S..#.....#...# +############### \ No newline at end of file diff --git a/src/main/kotlin/y2024/day16/input.txt b/src/main/kotlin/y2024/day16/input.txt new file mode 100644 index 0000000..d423686 Binary files /dev/null and b/src/main/kotlin/y2024/day16/input.txt differ diff --git a/src/test/kotlin/y2024/day16/Day16Test.kt b/src/test/kotlin/y2024/day16/Day16Test.kt new file mode 100644 index 0000000..98cfba6 --- /dev/null +++ b/src/test/kotlin/y2024/day16/Day16Test.kt @@ -0,0 +1,16 @@ +package y2024.day16 + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class Day16Test { + @Test + fun solvePartOne() { + assertEquals(108504, Day16.solvePartOne()) + } + + @Test + fun solvePartTwo() { + assertEquals(538, Day16.solvePartTwo()) + } +}