Skip to content

Commit 2485650

Browse files
committed
Day 16 done.
1 parent 99351d7 commit 2485650

File tree

11 files changed

+513
-11
lines changed

11 files changed

+513
-11
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (c) 2024 James Carman
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package adventofcode.day16
18+
19+
import adventofcode.util.geom.plane.Direction
20+
import adventofcode.util.geom.plane.Point2D
21+
import adventofcode.util.graph.Graphs
22+
import adventofcode.util.grid.TextGrid
23+
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+
}
29+
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
35+
}
36+
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
42+
}
43+
44+
fun String.findBestTiles(): Int {
45+
val maze = TextGrid(lines())
46+
val start = State(maze.coordinates().find { maze[it] == 'S' }!!, Direction.EAST)
47+
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
75+
76+
}
77+
78+
fun List<State>.score() = zipWithNext().sumOf { (s1, s2) -> score(s1, s2) }
79+
80+
private fun score(s1: State, s2: State) = if (s1.orientation == s2.orientation) 1.0 else 1000.0
81+
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] != '#' }
88+
}
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)
97+
}
98+
.toInt()
99+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ interface Graph<V : Any, E : Any> {
3131
fun shortestPaths(start: V): ShortestPaths<V> =
3232
Graphs.shortestPaths(start, vertices(), ::neighbors) { from, to -> edge(from, to)?.weight ?: Double.MAX_VALUE }
3333

34+
fun shortestPaths(start: V, end: V): Sequence<List<V>> = Graphs.shortestPaths(start, end, vertices(), ::neighbors) { from, to -> edge(from, to)?.weight ?: Double.MAX_VALUE }
35+
3436
fun dfs(start: V, end: V): List<V> = Graphs.dfs(start, end, ::neighbors)
3537
fun bfs(start: V, end: V): List<V> = Graphs.bfs(start, end, ::neighbors)
3638

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package adventofcode.util.graph
1818

1919
import java.util.PriorityQueue
20+
import kotlin.time.measureTime
21+
import kotlin.time.measureTimedValue
2022

2123
private fun compareDoubles(left: Double, right: Double): Int {
2224
if (left < right) {
@@ -29,6 +31,60 @@ private fun compareDoubles(left: Double, right: Double): Int {
2931
}
3032

3133
object Graphs {
34+
35+
/**
36+
* Implementation of Yen's K-Shortest Paths Algorithm as a sequence
37+
* @param start the starting vertex
38+
* @param end the ending vertex
39+
* @param vertices the set of all vertices
40+
* @param neighbors a function that returns the neighbors of a vertex
41+
* @param weight a function that returns the weight of an edge
42+
* @return a sequence of increasingly longer paths from the start vertex to the end vertex
43+
*/
44+
fun <V> shortestPaths(start: V, end: V, vertices: Set<V>, neighbors: (V) -> List<V>, weight: (V, V) -> Double) =
45+
sequence {
46+
val previousPaths = mutableListOf<List<V>>()
47+
val absoluteShortest = shortestPaths(start, vertices, neighbors, weight)
48+
if (absoluteShortest.pathExists(end)) {
49+
val shortest = absoluteShortest.pathTo(end)
50+
yield(shortest)
51+
previousPaths.addLast(shortest)
52+
var k = 2
53+
while (true) {
54+
val previousPath = previousPaths.last()
55+
val candidates = previousPath.asSequence().zipWithNext().map { (from, to) ->
56+
val head = previousPath.takeWhile { it != to }
57+
val visited = head.toSet()
58+
val limitedNeighbors: (V) -> List<V> =
59+
{ n -> if (n == from) neighbors(n) - visited - to else neighbors(n) - visited }
60+
val (tails, _) = measureTimedValue { shortestPaths(from, vertices, limitedNeighbors, weight) }
61+
head to tails
62+
}
63+
.filter { (_, tails) -> tails.pathExists(end) }
64+
.map { (head, tails) -> head + tails.pathTo(end).drop(1) }
65+
.filter { it !in previousPaths }
66+
.toList()
67+
68+
if (candidates.isEmpty()) {
69+
return@sequence
70+
}
71+
val nextShortest =
72+
candidates.minBy { it.asSequence().zipWithNext().sumOf { (from, to) -> weight(from, to) } }
73+
yield(nextShortest)
74+
previousPaths.addLast(nextShortest)
75+
k++
76+
}
77+
}
78+
}
79+
80+
/**
81+
* Implementation of Dijkstra's Algorithm
82+
* @param start the starting vertex
83+
* @param vertices the set of all vertices
84+
* @param neighbors a function that returns the neighbors of a vertex
85+
* @param weight a function that returns the weight of an edge
86+
* @return the shortest paths from the start vertex to all other vertices
87+
*/
3288
fun <V> shortestPaths(
3389
start: V,
3490
vertices: Set<V>,
@@ -49,9 +105,11 @@ object Graphs {
49105
queue.add(start)
50106
while (queue.isNotEmpty()) {
51107
val vertex = queue.poll()
108+
require(vertex in vertices)
52109
visited.add(vertex)
53110
val distanceToVertex = dist.getOrDefault(vertex, Double.MAX_VALUE)
54111
neighbors(vertex).filter { it !in visited }.forEach { neighbor ->
112+
require(neighbor in vertices)
55113
val distanceThroughVertex = distanceToVertex + weight(vertex, neighbor)
56114
if (distanceThroughVertex < dist[neighbor]!!) {
57115
pred[neighbor] = vertex
@@ -168,4 +226,17 @@ object Graphs {
168226
}
169227
return listOf()
170228
}
229+
230+
fun String.parseGraph(): Graph<String, String> {
231+
val regex = Regex("""\s*(\S+)\s*--\s*(\S+)\[(\d+\.\d+|\d+)]\s*-->\s*(\S+)\s*""")
232+
val graph = SparseGraph<String, String>()
233+
lines().forEach { line ->
234+
regex.matchEntire(line)?.destructured?.let { (from, edge, weight, to) ->
235+
graph.addVertex(from)
236+
graph.addVertex(to)
237+
graph.addEdge(from, to, edge, weight.toDouble())
238+
}
239+
}
240+
return graph
241+
}
171242
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2024 James Carman
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package adventofcode.util.graph
18+
19+
class Path<V>(val vertices: List<V>, val length: Double) {
20+
21+
fun append(vertex: V, distance: Double): Path<V> = Path(vertices + vertex, length + distance)
22+
23+
val origin: V
24+
get() = vertices.first()
25+
26+
val destination: V
27+
get() = vertices.last()
28+
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,20 @@
1717
package adventofcode.util.graph
1818

1919
class ShortestPaths<V>(
20-
private val start: V,
20+
val origin: V,
2121
private val distances: Map<V, Double>,
2222
private val predecessors: Map<V, V>
2323
) {
2424

25-
fun start() = start
2625
fun pathExists(end: V): Boolean = end in predecessors
2726

2827
fun pathTo(end: V): List<V> {
29-
if (!pathExists(end)) {
30-
return listOf()
28+
val path = mutableListOf(end)
29+
while(path.first() in predecessors) {
30+
path.addFirst(predecessors.getValue(path.first()))
3131
}
32-
return pathTo(predecessors[end]!!) + end
32+
return path
3333
}
3434

35-
fun distanceTo(end: V): Double = distances[end] ?: Double.POSITIVE_INFINITY
35+
fun distanceTo(end: V) = distances.getOrDefault(end, Double.POSITIVE_INFINITY)
3636
}

src/test/kotlin/adventofcode/Day16Test.kt

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

1717
package adventofcode
1818

19+
import adventofcode.day16.findBestTiles
20+
import adventofcode.day16.findLowestScore
1921
import io.kotest.matchers.shouldBe
2022
import org.junit.jupiter.api.Test
2123

2224
class Day16Test {
2325

2426
@Test
2527
fun example1() {
26-
calculatePart1(readExample1()) shouldBe 0
28+
calculatePart1(readExample1()) shouldBe 11048
2729
}
2830

2931
@Test
3032
fun part1() {
31-
calculatePart1(readInput()) shouldBe 0
33+
calculatePart1(readInput()) shouldBe 147628
3234
}
3335

3436
@Test
3537
fun example2() {
36-
calculatePart2(readExample2()) shouldBe 0
38+
calculatePart2(readExample2()) shouldBe 64
3739
}
3840

3941
@Test
4042
fun part2() {
4143
calculatePart2(readInput()) shouldBe 0
4244
}
4345

44-
private fun calculatePart1(input: String): Int = 0
46+
private fun calculatePart1(input: String): Int = input.findLowestScore()
4547

46-
private fun calculatePart2(input: String): Int = 0
48+
private fun calculatePart2(input: String): Int = input.findBestTiles()
4749
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package adventofcode.day16
2+
3+
import adventofcode.util.geom.plane.Direction
4+
import adventofcode.util.geom.plane.Point2D
5+
import io.kotest.matchers.shouldBe
6+
import org.junit.jupiter.api.Test
7+
8+
class ReindeerMazeTest {
9+
10+
@Test
11+
fun `path containing one item should have score zero`() {
12+
val path = listOf(State(Point2D.origin(), Direction.EAST))
13+
path.score() shouldBe 0
14+
}
15+
16+
@Test
17+
fun `path with only a left turn should have score 1000`() {
18+
val path = listOf(
19+
State(Point2D.origin(), Direction.EAST),
20+
State(Point2D.origin(), Direction.NORTH)
21+
)
22+
path.score() shouldBe 1000
23+
}
24+
25+
@Test
26+
fun `path with only a right turn should have score 1000`() {
27+
val path = listOf(
28+
State(Point2D.origin(), Direction.EAST),
29+
State(Point2D.origin(), Direction.SOUTH)
30+
)
31+
path.score() shouldBe 1000
32+
}
33+
34+
@Test
35+
fun `path with only a forward move should have score 1`() {
36+
val path = listOf(
37+
State(Point2D.origin(), Direction.EAST),
38+
State(Point2D(1, 0), Direction.EAST)
39+
)
40+
path.score() shouldBe 1
41+
}
42+
43+
@Test
44+
fun `path with multiple moves should have correct score`() {
45+
val path = listOf(
46+
State(Point2D.origin(), Direction.NORTH),
47+
State(Point2D.origin(), Direction.EAST),
48+
State(Point2D(1, 0), Direction.EAST),
49+
State(Point2D(2, 0), Direction.EAST),
50+
State(Point2D(3, 0), Direction.EAST),
51+
State(Point2D(3,0), Direction.SOUTH),
52+
)
53+
path.score() shouldBe 2003
54+
}
55+
56+
}

0 commit comments

Comments
 (0)