Skip to content

Commit 32f7ff9

Browse files
committed
Day 20 complete
1 parent 0242180 commit 32f7ff9

File tree

11 files changed

+76
-180
lines changed

11 files changed

+76
-180
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ Here are the solutions I have implemented along with the time it took to run eac
3939
| [Day 17: Chronospatial Computer](src/test/kotlin/adventofcode/Day17Test.kt) | 93 |
4040
| [Day 18: RAM Run](src/test/kotlin/adventofcode/Day18Test.kt) | 476 |
4141
| [Day 19: Linen Layout](src/test/kotlin/adventofcode/Day19Test.kt) | 60 |
42-
| [Day 20: Race Condition](src/test/kotlin/adventofcode/Day20Test.kt) | 0 |
42+
| [Day 20: Race Condition](src/test/kotlin/adventofcode/Day20Test.kt) | 474 |

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ class ReindeerMaze(input: String) {
6464

6565
private fun shortestPaths(): ShortestPaths<Pose> {
6666
val start = Pose(maze.coordinates().find { maze[it] == START }!!, Direction.EAST)
67-
val vertices = Graphs.reachable(start, neighbors = ::successorsOf)
68-
return Graphs.shortestPaths(start, vertices, ::successorsOf) { p1, p2 -> p1.distanceTo(p2) }
67+
return Graphs.shortestPaths(start, ::successorsOf) { p1, p2 -> p1.distanceTo(p2) }
6968
}
7069

7170
fun findLowestScore(): Int {

src/main/kotlin/adventofcode/day18/RamRun.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,9 @@ fun String.findShortestPathToExitAfter(nBytes: Int): Int {
5454
val points = parsePoints()
5555
val map = points.generateMap()
5656
points.take(nBytes).forEach { map[it] = '#' }
57-
val vertices = map.coordinates().filter { map[it] != '#' }.toSet()
5857
val neighbors = { p: Point2D -> p.neighbors().filter { it in map }.filter { map[it] != '#' } }
5958
return Graphs.shortestPaths(
6059
map.coordinates().first(),
61-
vertices,
6260
neighbors
6361
) { _, _ -> 1.0 }.pathTo(map.coordinates().last()).size - 1
6462
}

src/main/kotlin/adventofcode/day20/RaceCondition.kt

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,25 @@ import adventofcode.util.geom.plane.Point2D
2020
import adventofcode.util.graph.Graphs
2121
import adventofcode.util.grid.TextGrid
2222

23-
fun String.countBestCheats(savingsThreshold: Int): Int {
23+
fun String.countBestCheats(maxCheatLength: Int, savingsThreshold: Int): Int {
2424
val map = TextGrid(lines())
2525
val start = map.coordinates().find { map[it] == 'S' }!!
2626
val end = map.coordinates().find { map[it] == 'E' }!!
27-
val vertices = map.coordinates().filter { map[it] != '#' }.toSet()
2827
val neighbors: (Point2D) -> List<Point2D> =
29-
{ p: Point2D -> p.neighbors().filter { it in map }.filter { map[it] != '#' } }
30-
val shortestPaths = Graphs.shortestPaths(
31-
start,
32-
vertices,
33-
neighbors
34-
)
35-
val shortestDistance = shortestPaths.distanceTo(end)
36-
val shortestPath = shortestPaths.pathTo(end)
28+
{ p: Point2D -> map.neighborsOf(p).filter { map[it] != '#' } }
3729

38-
val cheats = shortestPath.flatMap { p -> p.neighbors().filter { map[it] == '#' && !map.isEdge(it) } }.toSet()
39-
return cheats.count { cheat ->
40-
map[cheat] = '.'
41-
val newShortestPaths = Graphs.shortestPaths(start, vertices + cheat, neighbors)
42-
map[cheat] = '#'
43-
newShortestPaths.distanceTo(end) <= shortestDistance - savingsThreshold
44-
}
30+
val shortestPath = Graphs.shortestPaths(start, neighbors).pathTo(end)
31+
32+
val remainingStepsFrom = shortestPath.mapIndexed { index, point -> point to shortestPath.size - 1 - index }.toMap()
33+
.withDefault { Int.MAX_VALUE }
34+
35+
return shortestPath.flatMapIndexed { index, origin ->
36+
shortestPath.subList(index + 1, shortestPath.size)
37+
.filter { cheat -> origin.manhattanDistance(cheat) <= maxCheatLength }
38+
.map { cheat ->
39+
remainingStepsFrom.getValue(origin) - origin.manhattanDistance(cheat) - remainingStepsFrom.getValue(
40+
cheat
41+
)
42+
}
43+
}.count { it >= savingsThreshold }
4544
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ interface Graph<V : Any, E : Any> {
2929
fun neighbors(from: V): List<V>
3030

3131
fun shortestPaths(start: V): ShortestPaths<V> =
32-
Graphs.shortestPaths(start, vertices(), ::neighbors) { from, to -> edge(from, to)?.weight ?: Double.MAX_VALUE }
33-
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 }
32+
Graphs.shortestPaths(start, ::neighbors) { from, to -> edge(from, to)?.weight ?: Double.MAX_VALUE }
3533

3634
fun dfs(start: V, end: V): List<V> = Graphs.dfs(start, end, ::neighbors)
3735
fun bfs(start: V, end: V): List<V> = Graphs.bfs(start, end, ::neighbors)

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

Lines changed: 15 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -17,141 +17,39 @@
1717
package adventofcode.util.graph
1818

1919
import java.util.PriorityQueue
20-
import kotlin.time.measureTime
21-
import kotlin.time.measureTimedValue
22-
23-
private fun compareDoubles(left: Double, right: Double): Int {
24-
if (left < right) {
25-
return -1
26-
}
27-
if (right < left) {
28-
return 1
29-
}
30-
return 0
31-
}
3220

3321
object Graphs {
3422

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-
*/
88-
fun <V> shortestPaths(
89-
start: V,
90-
neighbors: (V) -> List<V>,
91-
weight: (V, V) -> Double = { _, _ -> 1.0 }
92-
): ShortestPaths<V> {
93-
val pred = mutableMapOf<V, V>()
94-
val dist = mutableMapOf<V, Double>()
95-
val visited = mutableSetOf<V>()
96-
dist[start] = 0.0
97-
val queue = PriorityQueue { l: V, r: V ->
98-
compareDoubles(
99-
dist.getOrDefault(l, Double.MAX_VALUE),
100-
dist.getOrDefault(r, Double.MAX_VALUE)
101-
)
102-
}
103-
queue.add(start)
104-
while (queue.isNotEmpty()) {
105-
val vertex = queue.poll()
106-
visited.add(vertex)
107-
val distanceToVertex = dist.getOrDefault(vertex, Double.MAX_VALUE)
108-
neighbors(vertex).filter { it !in visited }.forEach { neighbor ->
109-
val distanceThroughVertex = distanceToVertex + weight(vertex, neighbor)
110-
if (distanceThroughVertex < dist.getOrDefault(neighbor, Double.MAX_VALUE)) {
111-
pred[neighbor] = vertex
112-
dist[neighbor] = distanceThroughVertex
113-
queue.add(neighbor)
114-
}
115-
}
116-
}
117-
return ShortestPaths(start, dist, pred)
118-
}
23+
private fun <V> MutableMap<V, Double>.distanceTo(vertex: V) = getOrPut(vertex) { Double.POSITIVE_INFINITY }
11924

12025
/**
12126
* Implementation of Dijkstra's Algorithm
27+
*
12228
* @param start the starting vertex
123-
* @param vertices the set of all vertices
12429
* @param neighbors a function that returns the neighbors of a vertex
125-
* @param weight a function that returns the weight of an edge
126-
* @return the shortest paths from the start vertex to all other vertices
30+
* @param distance a function that returns the distance between two vertices
31+
* @return a {@link ShortestPaths} object containing the shortest paths from the start vertex to all other vertices
12732
*/
12833
fun <V> shortestPaths(
12934
start: V,
130-
vertices: Set<V>,
13135
neighbors: (V) -> List<V>,
132-
weight: (V, V) -> Double = { _, _ -> 1.0 }
36+
distance: (V, V) -> Double = { _, _ -> 1.0 }
13337
): ShortestPaths<V> {
13438
val pred = mutableMapOf<V, V>()
13539
val dist = mutableMapOf<V, Double>()
13640
val visited = mutableSetOf<V>()
137-
vertices.forEach { vertex -> dist[vertex] = Double.POSITIVE_INFINITY }
13841
dist[start] = 0.0
13942
val queue = PriorityQueue { l: V, r: V ->
140-
compareDoubles(
141-
dist.getOrDefault(l, Double.MAX_VALUE),
142-
dist.getOrDefault(r, Double.MAX_VALUE)
143-
)
43+
dist.distanceTo(l).compareTo(dist.distanceTo(r))
14444
}
14545
queue.add(start)
14646
while (queue.isNotEmpty()) {
14747
val vertex = queue.poll()
148-
require(vertex in vertices)
14948
visited.add(vertex)
150-
val distanceToVertex = dist.getOrDefault(vertex, Double.MAX_VALUE)
49+
val distanceToVertex = dist.distanceTo(vertex)
15150
neighbors(vertex).filter { it !in visited }.forEach { neighbor ->
152-
require(neighbor in vertices)
153-
val distanceThroughVertex = distanceToVertex + weight(vertex, neighbor)
154-
if (distanceThroughVertex < dist[neighbor]!!) {
51+
val distanceThroughVertex = distanceToVertex + distance(vertex, neighbor)
52+
if (distanceThroughVertex < dist.distanceTo(neighbor)) {
15553
pred[neighbor] = vertex
15654
dist[neighbor] = distanceThroughVertex
15755
queue.add(neighbor)
@@ -167,13 +65,14 @@ object Graphs {
16765
val visited = mutableSetOf<V>()
16866
val queue = mutableListOf(Reachable(0, start))
16967
while (queue.isNotEmpty()) {
170-
val reachable = queue.removeLast()
68+
val reachable = queue.removeFirst()
17169
visited += reachable.vertex
17270
if (reachable.steps < maxSteps) {
173-
val ns = neighbors(reachable.vertex)
174-
.filter { it !in visited }
175-
.map { Reachable(reachable.steps + 1, it) }
176-
queue.addAll(ns)
71+
neighbors(reachable.vertex)
72+
.filter { it !in visited }
73+
.map { Reachable(reachable.steps + 1, it) }
74+
.forEach { queue.addLast(it) }
75+
17776
}
17877
}
17978
return visited

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ 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()
27+
fun reachable(): Set<V> = predecessors.keys.toSet() + origin
2828

2929
fun pathTo(end: V): List<V> {
3030
val path = mutableListOf(end)
@@ -34,5 +34,5 @@ class ShortestPaths<V>(
3434
return path
3535
}
3636

37-
fun distanceTo(end: V) = distances.getOrDefault(end, Double.POSITIVE_INFINITY)
37+
fun distanceTo(end: V) = if(origin == end) 0.0 else distances.getOrDefault(end, Double.POSITIVE_INFINITY)
3838
}

src/main/kotlin/adventofcode/util/grid/Grid.kt

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,6 @@ interface Grid<T : Any> {
4040
}
4141
}
4242

43-
fun neighborsOf(coordinate: Point2D) = sequence {
44-
coordinate.neighbors().forEach { neighbor ->
45-
if (neighbor in this@Grid) {
46-
yield(neighbor)
47-
}
48-
}
49-
}
50-
5143
operator fun get(x: Int, y: Int): T
5244

5345
operator fun set(x: Int, y: Int, value: T)
@@ -92,5 +84,17 @@ interface Grid<T : Any> {
9284

9385
fun pathsFrom(coordinate: Point2D) = Direction.entries.map { Path(this, it, coordinate) }
9486

95-
fun isEdge(coordinate: Point2D) = coordinate.x == 0 || coordinate.y == 0 || coordinate.x == width() - 1 || coordinate.y == height() - 1
87+
fun isEdge(coordinate: Point2D) =
88+
coordinate.x == 0 || coordinate.y == 0 || coordinate.x == width() - 1 || coordinate.y == height() - 1
89+
90+
fun neighborsOf(
91+
coordinate: Point2D,
92+
directions: List<Direction> = listOf(
93+
Direction.NORTH,
94+
Direction.EAST,
95+
Direction.SOUTH,
96+
Direction.WEST
97+
)
98+
) = directions.map { coordinate + it }.filter { it in this }
99+
96100
}

src/test/kotlin/adventofcode/Day20Test.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ class Day20Test {
3434

3535
@Test
3636
fun example2() {
37-
calculatePart2(readExample2()) shouldBe 0
37+
calculatePart2(readExample2(), 50) shouldBe 285
3838
}
3939

4040
@Test
4141
fun part2() {
42-
calculatePart2(readInput()) shouldBe 0
42+
calculatePart2(readInput(), 100) shouldBe 961364
4343
}
4444

45-
private fun calculatePart1(input: String, savingsThreshold:Int): Int = input.countBestCheats(savingsThreshold)
45+
private fun calculatePart1(input: String, savingsThreshold:Int): Int = input.countBestCheats(2, savingsThreshold)
4646

47-
private fun calculatePart2(input: String): Int = 0
47+
private fun calculatePart2(input: String, savingsThreshold: Int): Int = input.countBestCheats(20, savingsThreshold)
4848
}

src/test/kotlin/adventofcode/util/graph/GraphsTest.kt

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package adventofcode.util.graph
1818

1919
import adventofcode.util.geom.plane.Point2D
2020
import adventofcode.util.graph.Graphs.parseGraph
21+
import adventofcode.util.grid.TextGrid
2122
import io.kotest.matchers.shouldBe
2223
import org.assertj.core.api.Assertions.assertThat
2324
import org.junit.jupiter.api.Test
@@ -45,17 +46,6 @@ class GraphsTest {
4546
assertThat(Graphs.reachable(Point2D.origin()) { it.neighbors().filter { n -> n in searchSpace } }).hasSize(5)
4647
}
4748

48-
@Test
49-
fun `shortest paths sequence should be empty when there are no paths`() {
50-
val start = Point2D.origin()
51-
val end = Point2D(1, 1)
52-
val vertices = setOf(start, end)
53-
54-
val neighborsFn: (Point2D) -> List<Point2D> = { _ -> emptyList() }
55-
val weightFn: (Point2D, Point2D) -> Double = { _, _ -> 1.0 }
56-
Graphs.shortestPaths(start, end, vertices, neighborsFn, weightFn).toList() shouldBe emptyList()
57-
}
58-
5949
@Test
6050
fun `dijkstra shortest path should return absolute shortest path`() {
6151
val graph = SparseGraph<Char, Double>()
@@ -99,18 +89,12 @@ class GraphsTest {
9989
}
10090

10191
@Test
102-
fun `shortest path sequence should terminate after finding all paths`() {
103-
val graph = """
104-
A -- e1[1.0] --> B
105-
B -- e2[1.0] --> C
106-
A -- e3[5.0] --> D
107-
D -- e4[5.0] --> C
108-
""".trimIndent().parseGraph()
109-
val shortestPaths = graph.shortestPaths("A", "C").toList()
110-
shortestPaths shouldBe listOf(
111-
listOf("A", "B", "C"),
112-
listOf("A", "D", "C")
113-
)
92+
fun `reachable should return all reachable neighbors`() {
93+
val point = Point2D.origin()
94+
Graphs.reachable(point, 0, Point2D::neighbors).size shouldBe 1
95+
Graphs.reachable(point, 1, Point2D::neighbors).size shouldBe 5
96+
Graphs.reachable(point, 2, Point2D::neighbors).size shouldBe 13
97+
Graphs.reachable(point, 3, Point2D::neighbors).size shouldBe 25
98+
Graphs.reachable(point, 4, Point2D::neighbors).size shouldBe 41
11499
}
115-
116100
}

0 commit comments

Comments
 (0)