Skip to content

Commit 67266e3

Browse files
committed
Optimizing Day 16 part 2
1 parent d9704ac commit 67266e3

File tree

5 files changed

+95
-125
lines changed

5 files changed

+95
-125
lines changed

src/main/kotlin/adventofcode/day16/ReindeerMaze.kt

Lines changed: 89 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -19,81 +19,106 @@ package adventofcode.day16
1919
import adventofcode.util.geom.plane.Direction
2020
import adventofcode.util.geom.plane.Point2D
2121
import adventofcode.util.graph.Graphs
22+
import adventofcode.util.graph.ShortestPaths
2223
import adventofcode.util.grid.TextGrid
2324

24-
data class State(val position: Point2D, val orientation: Direction) {
25-
fun moveForward() = State(position + orientation, orientation)
26-
fun turnLeft() = State(position, orientation.turnLeft())
27-
fun turnRight() = State(position, orientation.turnRight())
28-
}
25+
fun String.countBestTiles(): Int = ReindeerMaze(this).countBestTiles()
26+
27+
fun String.findLowestScore() = ReindeerMaze(this).findLowestScore()
2928

30-
private fun Direction.turnRight() = when (this) {
31-
Direction.NORTH -> Direction.EAST
32-
Direction.EAST -> Direction.SOUTH
33-
Direction.SOUTH -> Direction.WEST
34-
else -> Direction.NORTH
29+
data class Pose(val position: Point2D, val orientation: Direction) {
30+
fun goForward() = Pose(position + orientation, orientation)
31+
fun goBack() = Pose(position - orientation, orientation)
32+
fun turnLeft() = Pose(
33+
position, when (orientation) {
34+
Direction.NORTH -> Direction.WEST
35+
Direction.WEST -> Direction.SOUTH
36+
Direction.SOUTH -> Direction.EAST
37+
else -> Direction.NORTH
38+
}
39+
)
40+
41+
fun turnRight() = Pose(
42+
position, when (orientation) {
43+
Direction.NORTH -> Direction.EAST
44+
Direction.EAST -> Direction.SOUTH
45+
Direction.SOUTH -> Direction.WEST
46+
else -> Direction.NORTH
47+
}
48+
)
49+
50+
fun distanceTo(other: Pose) = if (orientation == other.orientation) 1.0 else 1000.0
3551
}
3652

37-
private fun Direction.turnLeft() = when (this) {
38-
Direction.NORTH -> Direction.WEST
39-
Direction.WEST -> Direction.SOUTH
40-
Direction.SOUTH -> Direction.EAST
41-
else -> Direction.NORTH
53+
class Traversal(val path: List<Pose>, val distance: Double) {
54+
val visited = path.toSet()
4255
}
4356

44-
fun String.findBestTiles(): Int {
45-
val maze = TextGrid(lines())
46-
val start = State(maze.coordinates().find { maze[it] == 'S' }!!, Direction.EAST)
57+
private const val WALL = '#'
58+
private const val END = 'E'
59+
private const val START = 'S'
4760

48-
val neighborsFn: (State) -> List<State> = { state ->
49-
listOf(state.moveForward(), state.turnRight(), state.turnLeft())
50-
.filter { maze[it.position] != '#' }
51-
}
52-
val reachable = Graphs.reachable(start, neighbors = neighborsFn)
53-
val ends = reachable.filter { maze[it.position] == 'E' }
54-
val memory = mutableMapOf<Pair<State, State>, Double>()
55-
val weight: (State, State) -> Double =
56-
{ s1, s2 -> memory.computeIfAbsent(Pair(s1, s2)) { (s1, s2) -> score(s1, s2) } }
57-
val lowestScore =
58-
ends.minOf { end -> Graphs.shortestPaths(start, reachable, neighborsFn, weight).distanceTo(end).toInt() }
59-
60-
val shortestPaths = ends
61-
.asSequence()
62-
.flatMap { end ->
63-
val shortestPathsForEnd =
64-
Graphs.shortestPaths(start, end, reachable, neighborsFn, weight)
65-
.takeWhile { path -> path.score().toInt() == lowestScore }
66-
67-
shortestPathsForEnd
68-
}.toList()
69-
70-
val bestTiles = shortestPaths
71-
.flatten()
72-
.map { it.position }
73-
.toSet()
74-
return bestTiles.size
61+
class ReindeerMaze(input: String) {
7562

76-
}
63+
private val maze = TextGrid(input.lines())
7764

78-
fun List<State>.score() = zipWithNext().sumOf { (s1, s2) -> score(s1, s2) }
65+
private fun Pose.isWall() = maze[position] == WALL
7966

80-
private fun score(s1: State, s2: State) = if (s1.orientation == s2.orientation) 1.0 else 1000.0
67+
fun successorsOf(pose: Pose) = neighborsFn(END) { it.goForward() }(pose)
68+
fun predecessorsOf(pose: Pose) = neighborsFn(START) { it.goBack() }(pose)
8169

82-
fun String.findLowestScore(): Int {
83-
val maze = TextGrid(lines())
84-
val start = State(maze.coordinates().find { maze[it] == 'S' }!!, Direction.EAST)
85-
val neighborsFn: (State) -> List<State> = { state ->
86-
listOf(state.moveForward(), state.turnRight(), state.turnLeft())
87-
.filter { maze[it.position] != '#' }
70+
private fun neighborsFn(goal: Char, advance: (Pose) -> Pose): (Pose) -> List<Pose> = { state ->
71+
val neighbors = mutableListOf<Pose>()
72+
if (maze[state.position] != goal) {
73+
if (!advance(state).isWall()) {
74+
neighbors.add(advance(state))
75+
}
76+
if (!advance(state.turnLeft()).isWall()) {
77+
neighbors.add(state.turnLeft())
78+
}
79+
if (!advance(state.turnRight()).isWall()) {
80+
neighbors.add(state.turnRight())
81+
}
82+
}
83+
neighbors
8884
}
89-
val reachable = Graphs.reachable(start, neighbors = neighborsFn)
90-
return reachable.filter { maze[it.position] == 'E' }
91-
.minOf { end ->
92-
Graphs.shortestPaths(
93-
start,
94-
reachable,
95-
neighborsFn
96-
) { s1, s2 -> score(s1, s2) }.distanceTo(end)
85+
86+
private fun shortestPaths(): ShortestPaths<Pose> {
87+
val start = Pose(maze.coordinates().find { maze[it] == START }!!, Direction.EAST)
88+
val vertices = Graphs.reachable(start, neighbors = ::successorsOf)
89+
return Graphs.shortestPaths(start, vertices, ::successorsOf) { p1, p2 -> p1.distanceTo(p2) }
90+
}
91+
92+
fun findLowestScore(): Int {
93+
val shortestPaths = shortestPaths()
94+
val ends = shortestPaths.reachable().filter { maze[it.position] == END }
95+
return ends.minOf { end -> shortestPaths.distanceTo(end) }.toInt()
96+
}
97+
98+
fun countBestTiles(): Int {
99+
val shortestPaths = shortestPaths()
100+
val ends = shortestPaths.reachable().filter { maze[it.position] == END }
101+
val shortestDistance = ends.minOf { end -> shortestPaths.distanceTo(end) }
102+
val stack = mutableListOf<Traversal>()
103+
ends.forEach { stack.addFirst(Traversal(listOf(it), 0.0)) }
104+
val allShortestPaths = mutableListOf<List<Pose>>()
105+
while (stack.isNotEmpty()) {
106+
val traversal = stack.removeFirst()
107+
val currentState = traversal.path.last()
108+
if (maze[currentState.position] == START) {
109+
allShortestPaths.add(traversal.path)
110+
} else {
111+
predecessorsOf(currentState)
112+
.filter { it !in traversal.visited }
113+
.filter { shortestPaths.distanceTo(it) + traversal.distance <= shortestDistance }
114+
.forEach { pred ->
115+
val newPath = traversal.path + pred
116+
val newDistance = traversal.distance + pred.distanceTo(currentState)
117+
stack.addFirst(Traversal(newPath, newDistance))
118+
}
119+
}
97120
}
98-
.toInt()
99-
}
121+
return allShortestPaths.flatten().map { it.position }.distinct().size
122+
}
123+
}
124+

src/main/kotlin/adventofcode/util/geom/plane/Point2D.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ data class Point2D(val x: Int, val y: Int) : Comparable<Point2D> {
4444

4545
fun manhattanDistance(other: Point2D) = abs(x - other.x) + abs(y - other.y)
4646

47-
operator fun plus(direction: Direction) = translate(direction)
47+
operator fun plus(direction: Direction) = translate(direction.dx, direction.dy)
48+
operator fun minus(direction: Direction) = translate(-direction.dx, -direction.dy)
4849

4950
fun isAdjacent(other: Point2D) = manhattanDistance(other) == 1
5051

@@ -54,8 +55,6 @@ data class Point2D(val x: Int, val y: Int) : Comparable<Point2D> {
5455

5556
fun translate(dx: Int, dy: Int) = Point2D(x + dx, y + dy)
5657

57-
fun translate(direction: Direction) = translate(direction.dx, direction.dy)
58-
5958
companion object {
6059
fun origin() = Point2D(0, 0)
6160
}

src/main/kotlin/adventofcode/util/graph/ShortestPaths.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class ShortestPaths<V>(
2424

2525
fun pathExists(end: V): Boolean = end in predecessors
2626

27+
fun reachable(): Set<V> = predecessors.keys.filter { pathExists(it) }.toSet()
28+
2729
fun pathTo(end: V): List<V> {
2830
val path = mutableListOf(end)
2931
while(path.first() in predecessors) {

src/test/kotlin/adventofcode/Day16Test.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package adventofcode
1818

19-
import adventofcode.day16.findBestTiles
19+
import adventofcode.day16.countBestTiles
2020
import adventofcode.day16.findLowestScore
2121
import io.kotest.matchers.shouldBe
2222
import org.junit.jupiter.api.Test
@@ -45,5 +45,5 @@ class Day16Test {
4545

4646
private fun calculatePart1(input: String): Int = input.findLowestScore()
4747

48-
private fun calculatePart2(input: String): Int = input.findBestTiles()
48+
private fun calculatePart2(input: String): Int = input.countBestTiles()
4949
}

src/test/kotlin/adventofcode/day16/ReindeerMazeTest.kt

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)