Skip to content

Commit b2247ff

Browse files
committed
Solution Day18, part one - the big one
Restoring the aStar implementation of day 16, part one (the one that returns just one shortest path), leads to a much faster solution.
1 parent 932e711 commit b2247ff

File tree

5 files changed

+138
-82
lines changed

5 files changed

+138
-82
lines changed

src/main/kotlin/Day16.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ private class ReindeerMaze(input: List<String>) : Grid<Char>(input, '#') {
170170
println(info.invoke())
171171
}
172172

173-
return aStar(start, goal::positionEquals, neighbours, d, h)
173+
return aStarAllPaths(start, goal::positionEquals, neighbours, d, h)
174174
}
175175

176176
}

src/main/kotlin/Day18.kt

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import de.ronny_h.extensions.Coordinates
22
import de.ronny_h.extensions.Direction
3-
import de.ronny_h.extensions.Direction.EAST
4-
import de.ronny_h.extensions.Direction.NORTH
5-
import de.ronny_h.extensions.Direction.SOUTH
6-
import de.ronny_h.extensions.Direction.WEST
73
import de.ronny_h.extensions.Grid
84
import de.ronny_h.extensions.ShortestPath
95
import de.ronny_h.extensions.aStar
@@ -18,9 +14,9 @@ fun main() {
1814
})
1915
memorySpace.printGrid()
2016
println("-----------------")
21-
val shortestPaths = memorySpace.shortestPath(Coordinates(0,0), Coordinates(width-1, width-1))
22-
memorySpace.printGrid(path = shortestPaths.first().path.associateWith { 'O' })
23-
return shortestPaths.first().distance
17+
val shortestPath = memorySpace.shortestPath(Coordinates(0,0), Coordinates(width-1, width-1))
18+
memorySpace.printGrid(path = shortestPath.path.associateWith { 'O' })
19+
return shortestPath.distance
2420
}
2521

2622
fun part1Small(input: List<String>) = part1(input, 7) // 0..6
@@ -36,7 +32,7 @@ fun main() {
3632
printAndCheck(testInput.subList(0, 12), ::part1Small, 22)
3733

3834
val input = readInput(day)
39-
printAndCheck(input.subList(0, 1024), ::part1Big, 3714264)
35+
printAndCheck(input.subList(0, 1024), ::part1Big, 416)
4036

4137

4238
println("$day part 2")
@@ -54,7 +50,7 @@ private class MemorySpace(width: Int, corrupted: List<Coordinates>) : Grid<Char>
5450
private val corrupted = '#'
5551
override fun Char.toElementType() = this
5652

57-
fun shortestPath(start: Coordinates, goal: Coordinates): List<ShortestPath<Coordinates>> {
53+
fun shortestPath(start: Coordinates, goal: Coordinates): ShortestPath<Coordinates> {
5854
val neighbours: (Coordinates) -> List<Coordinates> = { node ->
5955
Direction
6056
.entries
@@ -69,6 +65,6 @@ private class MemorySpace(width: Int, corrupted: List<Coordinates>) : Grid<Char>
6965

7066
val h: (Coordinates) -> Int = { it taxiDistanceTo goal}
7167

72-
return aStar(start, goal::equals, neighbours, d, h)
68+
return aStar(start, goal, neighbours, d, h)
7369
}
7470
}
Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,70 @@
11
package de.ronny_h.extensions
22

3-
import java.util.*
3+
import java.util.PriorityQueue
44
import kotlin.Int.Companion.MAX_VALUE
55

6-
// NOTE: This modified A* implementation is based on the pseudocode on the Wikipedia page
6+
// NOTE: This A* implementation is a 1:1 equivalent in Kotlin to the pseudo code on the Wikipedia page
77
// https://en.wikipedia.org/wiki/A*_search_algorithm
88

9-
10-
private fun <N> reconstructPaths(cameFrom: Map<N, Collection<N>>, last: N): List<List<N>> {
11-
if (!cameFrom.contains(last)) {
12-
return listOf(listOf(last))
9+
private fun <N> reconstructPath(cameFrom: Map<N, N>, last: N): List<N> {
10+
var current = last
11+
val totalPath = mutableListOf(current)
12+
while (current in cameFrom.keys) {
13+
current = cameFrom.getValue(current)
14+
totalPath.add(0, current)
1315
}
14-
return cameFrom.getValue(last)
15-
.flatMap { pred -> reconstructPaths(cameFrom, pred) }
16-
.map { path -> path + last }
17-
.toList()
16+
return totalPath
1817
}
1918

2019
data class ShortestPath<N>(val path: List<N>, val distance: Int)
2120

2221
private const val LARGE_VALUE = MAX_VALUE / 2
2322

2423
/**
25-
* A modified A* algorithm that finds all shortest paths from `start` to `goal`.
24+
* A* finds a path from `start` to `goal`.
2625
* @param start the start node
27-
* @param isGoal predicate deciding if a node is a goal
26+
* @param goal the goal node
2827
* @param neighbors is a function that returns the list of neighbours for a given node.
2928
* @param d is the distance/cost function. d(m,n) provides the distance (or cost) to reach node n from node m.
3029
* @param h is the heuristic function. h(n) estimates the cost to reach goal from node n.
3130
*/
32-
fun <N> aStar(
33-
start: N, isGoal: N.() -> Boolean, neighbors: (N) -> List<N>, d: (N, N) -> Int, h: (N) -> Int,
34-
printIt: (visited: Set<N>, current: N, additionalInfo: () -> String) -> Unit = { _, _, _ -> }
35-
): List<ShortestPath<N>> {
31+
fun <N> aStar(start: N, goal: N, neighbors: (N) -> List<N>, d: (N, N) -> Int, h: (N) -> Int,
32+
printIt: (visited: Set<N>, current: N, additionalInfo: () -> String) -> Unit = {_, _, _ -> }): ShortestPath<N> {
3633
// For node n, fScore[n] := gScore[n] + h(n). fScore[n] represents our current best guess as to
3734
// how cheap a path could be from start to finish if it goes through n.
38-
val fScore = mutableMapOf<N, Int>().withDefault({ _ -> LARGE_VALUE }) // map with default value of Infinity
35+
val fScore = mutableMapOf<N, Int>().withDefault { _ -> LARGE_VALUE } // map with default value of Infinity
3936

4037
// The set of discovered nodes that may need to be (re-)expanded.
4138
// Initially, only the start node is known.
4239
// This is usually implemented as a min-heap or priority queue rather than a hash-set.
43-
val openSet =
44-
PriorityQueue<N> { a, b -> fScore.getValue(a).compareTo(fScore.getValue(b)) }
40+
val openSet = PriorityQueue<N> { a, b -> fScore.getValue(a).compareTo(fScore.getValue(b)) }
4541
openSet.add(start)
4642

47-
// For node n, cameFrom[n] is the list of nodes immediately preceding it on the cheapest paths from the start
43+
// For node n, cameFrom[n] is the node immediately preceding it on the cheapest path from the start
4844
// to n currently known.
49-
val cameFrom = mutableMapOf<N, MutableSet<N>>()
45+
val cameFrom = mutableMapOf<N, N>()
5046

5147
// For node n, gScore[n] is the currently known cost of the cheapest path from start to n.
52-
val gScore = mutableMapOf<N, Int>().withDefault({ _ -> LARGE_VALUE }) // map with default value of Infinity
48+
val gScore = mutableMapOf<N, Int>().withDefault { _ -> LARGE_VALUE } // map with default value of Infinity
5349
gScore[start] = 0
5450

5551
fScore[start] = h(start)
5652

5753
while (openSet.isNotEmpty()) {
5854
// This operation can occur in O(Log(N)) time if openSet is a min-heap or a priority queue
5955
val current = openSet.peek()
60-
if (current.isGoal()) {
61-
return reconstructPaths(cameFrom, current).map { x -> ShortestPath(x, gScore.getValue(current)) }
56+
if (current == goal) {
57+
return ShortestPath(reconstructPath(cameFrom, current), gScore.getValue(current))
6258
}
6359

6460
openSet.remove(current)
6561
for (neighbor in neighbors(current)) {
6662
// d(current,neighbor) is the weight of the edge from current to neighbor
6763
// tentative_gScore is the distance from start to the neighbor through current
6864
val tentativeGScore = gScore.getValue(current) + d(current, neighbor)
69-
if (tentativeGScore <= gScore.getValue(neighbor)) {
70-
if (tentativeGScore < gScore.getValue(neighbor)) {
71-
// This path to neighbor is better than any previous one. Record it!
72-
cameFrom[neighbor] = mutableSetOf(current)
73-
} else {
74-
// This path to neighbor is equal to the best one. Record it!
75-
cameFrom.getOrPut(neighbor) { mutableSetOf() } += current
76-
}
65+
if (tentativeGScore < gScore.getValue(neighbor)) {
66+
// This path to neighbor is better than any previous one. Record it!
67+
cameFrom[neighbor] = current
7768
gScore[neighbor] = tentativeGScore
7869
fScore[neighbor] = tentativeGScore + h(neighbor)
7970
if (neighbor !in openSet) {
@@ -87,5 +78,5 @@ fun <N> aStar(
8778
}
8879

8980
// Open set is empty but goal was never reached
90-
error("No path found from $start to goal")
81+
error("No path found from $start to $goal")
9182
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package de.ronny_h.extensions
2+
3+
import java.util.*
4+
import kotlin.Int.Companion.MAX_VALUE
5+
6+
// NOTE: This modified A* implementation is based on the pseudocode on the Wikipedia page
7+
// https://en.wikipedia.org/wiki/A*_search_algorithm
8+
9+
10+
private fun <N> reconstructPaths(cameFrom: Map<N, Collection<N>>, last: N): List<List<N>> {
11+
if (!cameFrom.contains(last)) {
12+
return listOf(listOf(last))
13+
}
14+
return cameFrom.getValue(last)
15+
.flatMap { pred -> reconstructPaths(cameFrom, pred) }
16+
.map { path -> path + last }
17+
.toList()
18+
}
19+
20+
private const val LARGE_VALUE = MAX_VALUE / 2
21+
22+
/**
23+
* A modified A* algorithm that finds all shortest paths from `start` to `goal`.
24+
* @param start the start node
25+
* @param isGoal predicate deciding if a node is a goal
26+
* @param neighbors is a function that returns the list of neighbours for a given node.
27+
* @param d is the distance/cost function. d(m,n) provides the distance (or cost) to reach node n from node m.
28+
* @param h is the heuristic function. h(n) estimates the cost to reach goal from node n.
29+
*/
30+
fun <N> aStarAllPaths(
31+
start: N, isGoal: N.() -> Boolean, neighbors: (N) -> List<N>, d: (N, N) -> Int, h: (N) -> Int,
32+
printIt: (visited: Set<N>, current: N, additionalInfo: () -> String) -> Unit = { _, _, _ -> }
33+
): List<ShortestPath<N>> {
34+
// For node n, fScore[n] := gScore[n] + h(n). fScore[n] represents our current best guess as to
35+
// how cheap a path could be from start to finish if it goes through n.
36+
val fScore = mutableMapOf<N, Int>().withDefault { _ -> LARGE_VALUE } // map with default value of Infinity
37+
38+
// The set of discovered nodes that may need to be (re-)expanded.
39+
// Initially, only the start node is known.
40+
// This is usually implemented as a min-heap or priority queue rather than a hash-set.
41+
val openSet =
42+
PriorityQueue<N> { a, b -> fScore.getValue(a).compareTo(fScore.getValue(b)) }
43+
openSet.add(start)
44+
45+
// For node n, cameFrom[n] is the list of nodes immediately preceding it on the cheapest paths from the start
46+
// to n currently known.
47+
val cameFrom = mutableMapOf<N, MutableSet<N>>()
48+
49+
// For node n, gScore[n] is the currently known cost of the cheapest path from start to n.
50+
val gScore = mutableMapOf<N, Int>().withDefault { _ -> LARGE_VALUE } // map with default value of Infinity
51+
gScore[start] = 0
52+
53+
fScore[start] = h(start)
54+
55+
while (openSet.isNotEmpty()) {
56+
// This operation can occur in O(Log(N)) time if openSet is a min-heap or a priority queue
57+
val current = openSet.peek()
58+
if (current.isGoal()) {
59+
return reconstructPaths(cameFrom, current).map { x -> ShortestPath(x, gScore.getValue(current)) }
60+
}
61+
62+
openSet.remove(current)
63+
for (neighbor in neighbors(current)) {
64+
// d(current,neighbor) is the weight of the edge from current to neighbor
65+
// tentative_gScore is the distance from start to the neighbor through current
66+
val tentativeGScore = gScore.getValue(current) + d(current, neighbor)
67+
if (tentativeGScore <= gScore.getValue(neighbor)) {
68+
if (tentativeGScore < gScore.getValue(neighbor)) {
69+
// This path to neighbor is better than any previous one. Record it!
70+
cameFrom[neighbor] = mutableSetOf(current)
71+
} else {
72+
// This path to neighbor is equal to the best one. Record it!
73+
cameFrom.getOrPut(neighbor) { mutableSetOf() } += current
74+
}
75+
gScore[neighbor] = tentativeGScore
76+
fScore[neighbor] = tentativeGScore + h(neighbor)
77+
if (neighbor !in openSet) {
78+
openSet.add(neighbor)
79+
}
80+
}
81+
printIt(cameFrom.keys, neighbor) {
82+
"current: $current=${fScore[current]}, neighbor: $neighbor=${fScore[neighbor]}, open: " + openSet.joinToString { "$it=${fScore[it]}" }
83+
}
84+
}
85+
}
86+
87+
// Open set is empty but goal was never reached
88+
error("No path found from $start to goal")
89+
}

src/test/kotlin/de/ronny_h/extensions/ShortestPathTest.kt

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ class ShortestPathTest : StringSpec({
5454
val d: (Node, Node) -> Int = { _, _ -> 1 }
5555
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
5656

57-
aStar(start, goal::positionEquals, neighbours, d, h) shouldBe listOf(ShortestPath(listOf(start, goal), 1))
57+
aStarAllPaths(start, goal::positionEquals, neighbours, d, h) shouldBe listOf(ShortestPath(listOf(start, goal), 1))
58+
aStar(start, goal, neighbours, d, h) shouldBe ShortestPath(listOf(start, goal), 1)
5859
}
5960

6061
"With 2 different nodes between start and goal, the shorter path is taken" {
@@ -80,15 +81,10 @@ class ShortestPathTest : StringSpec({
8081
val d: (Node, Node) -> Int = { m, n -> distances.getValue(m to n) }
8182
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
8283

83-
aStar(start, goal::positionEquals, { n -> neighbours.getValue(n) }, d, h) shouldBe listOf(
84-
ShortestPath(
85-
listOf(
86-
start,
87-
a,
88-
goal
89-
), 10
90-
)
91-
)
84+
aStarAllPaths(start, goal::positionEquals, { n -> neighbours.getValue(n) }, d, h) shouldBe
85+
listOf(ShortestPath(listOf(start, a, goal), 10))
86+
aStar(start, goal, { n -> neighbours.getValue(n) }, d, h) shouldBe
87+
ShortestPath(listOf(start, a, goal), 10)
9288
}
9389

9490
"When direct distance from start to goal is longer, the path through a third node is taken" {
@@ -111,15 +107,10 @@ class ShortestPathTest : StringSpec({
111107
val d: (Node, Node) -> Int = { m, n -> distances.getValue(m to n) }
112108
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
113109

114-
aStar(start, goal::positionEquals, { n -> neighbours.getValue(n) }, d, h) shouldBe listOf(
115-
ShortestPath(
116-
listOf(
117-
start,
118-
a,
119-
goal
120-
), 9
121-
)
122-
)
110+
aStarAllPaths(start, goal::positionEquals, { n -> neighbours.getValue(n) }, d, h) shouldBe
111+
listOf(ShortestPath(listOf(start, a, goal), 9))
112+
aStar(start, goal, { n -> neighbours.getValue(n) }, d, h) shouldBe
113+
ShortestPath(listOf(start, a, goal), 9)
123114
}
124115

125116
"The shortest path in a not directed graph is found" {
@@ -146,15 +137,10 @@ class ShortestPathTest : StringSpec({
146137
val d: (Node, Node) -> Int = { m, n -> distances.getValue(m to n) }
147138
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
148139

149-
aStar(start, goal::positionEquals, { n -> neighbours.getValue(n) }, d, h) shouldBe listOf(
150-
ShortestPath(
151-
listOf(
152-
start,
153-
a,
154-
goal
155-
), 9
156-
)
157-
)
140+
aStarAllPaths(start, goal::positionEquals, { n -> neighbours.getValue(n) }, d, h) shouldBe
141+
listOf(ShortestPath(listOf(start, a, goal), 9))
142+
aStar(start, goal, { n -> neighbours.getValue(n) }, d, h) shouldBe
143+
ShortestPath(listOf(start, a, goal), 9)
158144
}
159145

160146
"Distances of 0 can be taken and nodes with same coordinates don't cause problems" {
@@ -180,15 +166,9 @@ class ShortestPathTest : StringSpec({
180166
val d: (Node, Node) -> Int = { m, n -> distances.getValue(m to n) }
181167
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
182168

183-
aStar(start, goal::positionEquals, { n -> neighbours.getValue(n) }, d, h) shouldBe listOf(
184-
ShortestPath(
185-
listOf(
186-
start,
187-
a,
188-
b,
189-
goal
190-
), 9
191-
)
192-
)
169+
aStarAllPaths(start, goal::positionEquals, { n -> neighbours.getValue(n) }, d, h) shouldBe
170+
listOf(ShortestPath(listOf(start, a, b, goal), 9))
171+
aStar(start, goal, { n -> neighbours.getValue(n) }, d, h) shouldBe
172+
ShortestPath(listOf(start, a, b, goal), 9)
193173
}
194174
})

0 commit comments

Comments
 (0)