Skip to content

Commit 07c38ab

Browse files
committed
Solution Day 16, part two
1 parent f3fb646 commit 07c38ab

File tree

3 files changed

+142
-36
lines changed

3 files changed

+142
-36
lines changed

src/main/kotlin/Day16.kt

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import de.ronny_h.extensions.Coordinates
2-
import de.ronny_h.extensions.Direction
3-
import de.ronny_h.extensions.Grid
4-
import de.ronny_h.extensions.aStar
1+
import de.ronny_h.extensions.*
52

63
fun main() {
74
val day = "Day16"
@@ -60,6 +57,59 @@ fun main() {
6057

6158
val input = readInput(day)
6259
printAndCheck(input, ::part1, 89460)
60+
61+
62+
println("$day part 2")
63+
64+
fun part2(input: List<String>): Int {
65+
val maze = ReindeerMaze(input)
66+
return maze.collectAllShortestPathsTiles()
67+
}
68+
69+
printAndCheck(
70+
"""
71+
###############
72+
#.......#....E#
73+
#.#.###.#.###.#
74+
#.....#.#...#.#
75+
#.###.#####.#.#
76+
#.#.#.......#.#
77+
#.#.#####.###.#
78+
#...........#.#
79+
###.#.#####.#.#
80+
#...#.....#.#.#
81+
#.#.#.###.#.#.#
82+
#.....#...#.#.#
83+
#.###.#.#.#.#.#
84+
#S..#.....#...#
85+
###############
86+
""".trimIndent().lines(),
87+
::part2, 45
88+
)
89+
90+
printAndCheck(
91+
"""
92+
#################
93+
#...#...#...#..E#
94+
#.#.#.#.#.#.#.#.#
95+
#.#.#.#...#...#.#
96+
#.#.#.#.###.#.#.#
97+
#...#.#.#.....#.#
98+
#.#.#.#.#.#####.#
99+
#.#...#.#.#.....#
100+
#.#.#####.#.###.#
101+
#.#.#.......#...#
102+
#.#.###.#####.###
103+
#.#.#...#.....#.#
104+
#.#.#.#####.###.#
105+
#.#.#.........#.#
106+
#.#.#.#########.#
107+
#S#.............#
108+
#################
109+
""".trimIndent().lines(),
110+
::part2, 64
111+
)
112+
printAndCheck(input, ::part2, 504)
63113
}
64114

65115
private class ReindeerMaze(input: List<String>) : Grid<Char>(input) {
@@ -69,9 +119,23 @@ private class ReindeerMaze(input: List<String>) : Grid<Char>(input) {
69119

70120
data class Node(val direction: Direction, val position: Coordinates) {
71121
override fun toString() = "$position$direction"
122+
fun positionEquals(other: Node) = this.position == other.position
72123
}
73124

74125
fun calculateLowestScore(): Int {
126+
val allShortestPaths = getAllShortestPaths()
127+
128+
printGrid(path = allShortestPaths.map { it.path }.flatten().associate { it.position to it.direction.asChar() })
129+
return allShortestPaths.first().distance
130+
}
131+
132+
fun collectAllShortestPathsTiles() = getAllShortestPaths()
133+
.flatMap { it.path }
134+
.map { it.position }
135+
.toSet()
136+
.size
137+
138+
private fun getAllShortestPaths(): List<ShortestPath<Node>> {
75139
// A* algorithm
76140
// - heuristic function: manhattan distance
77141
// - weight function:
@@ -107,9 +171,7 @@ private class ReindeerMaze(input: List<String>) : Grid<Char>(input) {
107171
println(info.invoke())
108172
}
109173

110-
val shortestPath = aStar(start, goal, neighbours, d, h) //, printIt)
111-
printGrid(path = shortestPath.path.associate { it.position to it.direction.asChar() })
112-
return shortestPath.distance
174+
return aStar(start, goal::positionEquals, neighbours, d, h)
113175
}
114176

115177
}

src/main/kotlin/de/ronny_h/extensions/ShortestPath.kt

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,77 @@ package de.ronny_h.extensions
33
import java.util.*
44
import kotlin.Int.Companion.MAX_VALUE
55

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

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)
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))
1513
}
16-
return totalPath
14+
return cameFrom.getValue(last)
15+
.flatMap { pred -> reconstructPaths(cameFrom, pred) }
16+
.map { path -> path + last }
17+
.toList()
1718
}
1819

1920
data class ShortestPath<N>(val path: List<N>, val distance: Int)
2021

2122
private const val LARGE_VALUE = MAX_VALUE / 2
2223

2324
/**
24-
* A* finds a path from `start` to `goal`.
25+
* A modified A* algorithm that finds all shortest paths from `start` to `goal`.
2526
* @param start the start node
26-
* @param goal the goal node
27+
* @param isGoal predicate deciding if a node is a goal
2728
* @param neighbors is a function that returns the list of neighbours for a given node.
2829
* @param d is the distance/cost function. d(m,n) provides the distance (or cost) to reach node n from node m.
2930
* @param h is the heuristic function. h(n) estimates the cost to reach goal from node n.
3031
*/
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> {
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>> {
3336
// For node n, fScore[n] := gScore[n] + h(n). fScore[n] represents our current best guess as to
3437
// how cheap a path could be from start to finish if it goes through n.
35-
val fScore = mutableMapOf<N, Int>() // map with default value of Infinity
38+
val fScore = mutableMapOf<N, Int>().withDefault({ _ -> LARGE_VALUE }) // map with default value of Infinity
3639

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

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

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

5155
fScore[start] = h(start)
5256

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

6064
openSet.remove(current)
6165
for (neighbor in neighbors(current)) {
6266
// d(current,neighbor) is the weight of the edge from current to neighbor
6367
// tentative_gScore is the distance from start to the neighbor through current
64-
val tentativeGScore = gScore.getOrDefault(current, LARGE_VALUE) + d(current, neighbor)
65-
if (tentativeGScore < gScore.getOrDefault(neighbor, LARGE_VALUE)) {
66-
// This path to neighbor is better than any previous one. Record it!
67-
cameFrom[neighbor] = current
68+
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+
}
6877
gScore[neighbor] = tentativeGScore
6978
fScore[neighbor] = tentativeGScore + h(neighbor)
7079
if (neighbor !in openSet) {
@@ -78,5 +87,5 @@ fun <N> aStar(start: N, goal: N, neighbors: (N) -> List<N>, d: (N, N) -> Int, h:
7887
}
7988

8089
// Open set is empty but goal was never reached
81-
error("No path found from $start to $goal")
90+
error("No path found from $start to goal")
8291
}

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

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ data class Node(val position: At, val id: Long = nextId) {
1313
init {
1414
nextId++
1515
}
16+
17+
fun positionEquals(other: Node) = other.position == this.position
1618
}
1719

1820

@@ -52,7 +54,7 @@ class ShortestPathTest : StringSpec({
5254
val d: (Node, Node) -> Int = { _, _ -> 1 }
5355
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
5456

55-
aStar(start, goal, neighbours, d, h) shouldBe ShortestPath(listOf(start, goal), 1)
57+
aStar(start, goal::positionEquals, neighbours, d, h) shouldBe listOf(ShortestPath(listOf(start, goal), 1))
5658
}
5759

5860
"With 2 different nodes between start and goal, the shorter path is taken" {
@@ -78,7 +80,15 @@ class ShortestPathTest : StringSpec({
7880
val d: (Node, Node) -> Int = { m, n -> distances.getValue(m to n) }
7981
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
8082

81-
aStar(start, goal, { n -> neighbours.getValue(n) }, d, h) shouldBe ShortestPath(listOf(start, a, goal), 10)
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+
)
8292
}
8393

8494
"When direct distance from start to goal is longer, the path through a third node is taken" {
@@ -101,7 +111,15 @@ class ShortestPathTest : StringSpec({
101111
val d: (Node, Node) -> Int = { m, n -> distances.getValue(m to n) }
102112
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
103113

104-
aStar(start, goal, { n -> neighbours.getValue(n) }, d, h) shouldBe ShortestPath(listOf(start, a, goal), 9)
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+
)
105123
}
106124

107125
"The shortest path in a not directed graph is found" {
@@ -128,7 +146,15 @@ class ShortestPathTest : StringSpec({
128146
val d: (Node, Node) -> Int = { m, n -> distances.getValue(m to n) }
129147
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
130148

131-
aStar(start, goal, { n -> neighbours.getValue(n) }, d, h) shouldBe ShortestPath(listOf(start, a, goal), 9)
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+
)
132158
}
133159

134160
"Distances of 0 can be taken and nodes with same coordinates don't cause problems" {
@@ -154,6 +180,15 @@ class ShortestPathTest : StringSpec({
154180
val d: (Node, Node) -> Int = { m, n -> distances.getValue(m to n) }
155181
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
156182

157-
aStar(start, goal, { n -> neighbours.getValue(n) }, d, h) shouldBe ShortestPath(listOf(start, a, b, goal), 9)
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+
)
158193
}
159194
})

0 commit comments

Comments
 (0)