Skip to content

Commit 98ea557

Browse files
committed
Dijkstra: Fix some edge cases
* Prevent an overflow when something is added to the max value of edge weights * Allow the Dijkstra implementation to find no path to a goal * In a Grid, include the obstacles (all but the nullElement which typically is a wall or outside the grid) in the set of vertices as well. Especially the start and the goals need to be visitable vertices.
1 parent 10ef600 commit 98ea557

File tree

6 files changed

+32
-17
lines changed

6 files changed

+32
-17
lines changed

src/main/kotlin/de/ronny_h/aoc/extensions/graphs/shortestpath/AStar.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ private fun <N> reconstructPath(cameFrom: Map<N, N>, last: N): List<N> {
1616
return totalPath
1717
}
1818

19-
private const val LARGE_VALUE = MAX_VALUE / 2
19+
/**
20+
* The maximum value for edge weights. In pseudocode of path-searching algorithms
21+
* this is typically denoted as infinity (= a value larger than all others).
22+
*/
23+
const val LARGE_VALUE = MAX_VALUE / 2
2024

2125
/**
2226
* A* finds a path from `start` to `goal`.

src/main/kotlin/de/ronny_h/aoc/extensions/graphs/shortestpath/AStarMultipath.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package de.ronny_h.aoc.extensions.graphs.shortestpath
22

33
import java.util.*
4-
import kotlin.Int.Companion.MAX_VALUE
54

65
// NOTE: This modified A* implementation is based on the pseudocode on the Wikipedia page
76
// https://en.wikipedia.org/wiki/A*_search_algorithm
@@ -16,8 +15,6 @@ private fun <N> reconstructPaths(cameFrom: Map<N, Collection<N>>, currentNode: N
1615
.map { path -> path + currentNode }
1716
}
1817

19-
private const val LARGE_VALUE = MAX_VALUE / 2
20-
2118
/**
2219
* A modified A* algorithm that finds all shortest paths from `start` to `goal`.
2320
* @param start the start node

src/main/kotlin/de/ronny_h/aoc/extensions/graphs/shortestpath/Dijkstra.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ data class DijkstraResult<V>(val distances: Map<V, Int>, val predecessors: Map<V
1717
*/
1818
fun <V> dijkstra(graph: Graph<V>, source: V, targets: List<V>): List<ShortestPath<V>> {
1919
val dijkstraResult = dijkstra(graph, source)
20-
return targets.map { reconstructPath(dijkstraResult, source, it) }
20+
return targets.mapNotNull { reconstructPath(dijkstraResult, source, it) }
2121
}
2222

2323
// NOTE: This implementation of Dijkstra's Algorithm is a 1:1 equivalent in Kotlin to the pseudo code on the Wikipedia page
@@ -27,7 +27,7 @@ fun <V> dijkstra(graph: Graph<V>, source: V, targets: List<V>): List<ShortestPat
2727
* Dijkstra's algorithm finds the shortest path from node [source] to all vertices in the [graph].
2828
*/
2929
fun <V> dijkstra(graph: Graph<V>, source: V): DijkstraResult<V> {
30-
val dist = mutableMapOf(source to 0).withDefault { Int.MAX_VALUE }
30+
val dist = mutableMapOf(source to 0).withDefault { LARGE_VALUE }
3131
val prev = mutableMapOf<V, V>()
3232
val q = graph.vertices.toMutableSet()
3333

@@ -39,6 +39,7 @@ fun <V> dijkstra(graph: Graph<V>, source: V): DijkstraResult<V> {
3939
for (v in q) {
4040
val edgeWeight = graph.edges(u, v) ?: continue
4141
val alt = dist.getValue(u) + edgeWeight
42+
// TODO use <= and take the prev with highest precedence
4243
if (alt < dist.getValue(v)) {
4344
dist[v] = alt
4445
prev[v] = u
@@ -49,9 +50,9 @@ fun <V> dijkstra(graph: Graph<V>, source: V): DijkstraResult<V> {
4950
return DijkstraResult(dist.toMap(), prev.toMap())
5051
}
5152

52-
private fun <V> reconstructPath(dijkstraResult: DijkstraResult<V>, source: V, target: V): ShortestPath<V> {
53+
private fun <V> reconstructPath(dijkstraResult: DijkstraResult<V>, source: V, target: V): ShortestPath<V>? {
5354
if (dijkstraResult.predecessors[target] == null && target != source) {
54-
throw IllegalStateException("Target vertex $target is not reachable from $source")
55+
return null
5556
}
5657

5758
val s = mutableListOf<V>()

src/main/kotlin/de/ronny_h/aoc/extensions/grids/Grid.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ abstract class Grid<T>(
222222
): List<ShortestPath<Coordinates>> {
223223
val graph = Graph(
224224
vertices = forEachCoordinates { position, element ->
225-
if (isObstacle(element)) null else position
225+
if (element == nullElement) null else position
226226
}.filterNotNull().toSet(),
227227
edges = { from, to ->
228228
if (to in from.neighbours().filter { !isObstacle(getAt(it)) }) 1 else null

src/test/kotlin/de/ronny_h/aoc/extensions/graphs/shortestpath/ShortestPathTest.kt

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,22 +197,16 @@ class ShortestPathTest : StringSpec({
197197
listOf(ShortestPath(listOf(start, a, b, goal), 9))
198198
}
199199

200-
"When there is no path at all, aStarAllPaths returns an empty list" {
200+
"When there is no path at all, aStarAllPaths and dijkstra return an empty list" {
201201
val start = Node(At(0, 0))
202202
val goal = Node(At(0, 10))
203203

204204
val d: (Node, Node) -> Int = { _, _ -> 0 }
205205
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
206206

207207
aStarAllPaths(start, goal::positionEquals, { emptyList() }, d, h) shouldBe emptyList()
208+
dijkstra(Graph(setOf(start, goal), { _, _ -> null }), start, listOf(goal)) shouldBe emptyList()
208209

209210
shouldThrow<IllegalStateException> { aStar(start, goal::positionEquals, { emptyList() }, d, h) }
210-
shouldThrow<IllegalStateException> {
211-
dijkstra(
212-
Graph(setOf(start, goal), { _, _ -> null }),
213-
start,
214-
listOf(goal)
215-
)
216-
}
217211
}
218212
})

src/test/kotlin/de/ronny_h/aoc/extensions/grids/SimpleCharGridTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,25 @@ class SimpleCharGridTest : StringSpec({
182182
)
183183
}
184184

185+
"the Dijkstra implementation ignores unreachable goals" {
186+
val input = """
187+
1#2
188+
3#4
189+
5#6
190+
""".asList()
191+
SimpleCharGrid(input)
192+
.shortestPaths(
193+
start = Coordinates(0, 0),
194+
goals = listOf(Coordinates(0, 2), Coordinates(2, 0), Coordinates(2, 2))
195+
) shouldBe listOf(
196+
ShortestPath(
197+
listOf(
198+
Coordinates(0, 0), Coordinates(1, 0), Coordinates(2, 0),
199+
), 2
200+
),
201+
)
202+
}
203+
185204
"cluster regions of same char with a single region" {
186205
val input = """
187206
xx

0 commit comments

Comments
 (0)