From e9c7f78ddd698e61a7fcc0f135320b52bbd90913 Mon Sep 17 00:00:00 2001 From: Thomas Plumpton Date: Sun, 8 Dec 2024 19:15:03 +0000 Subject: [PATCH 1/9] Math | Added "real" or Euclidean distance between function to Point2D --- .../github/tomplum/libs/math/point/Point2D.kt | 22 +++++++++ .../tomplum/libs/math/point/Point2DTest.kt | 45 ++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt index 3aec898..9b40eb2 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt @@ -61,9 +61,31 @@ data class Point2D(val x: Int, val y: Int) : Point, Comparable { /** * Calculates the Manhattan Distance between two [Point2D]s. * The distance between the points is measured along the axes at right angles. + * + * For the Euclidean distance between two points, see [realDistanceBetween]. */ fun distanceBetween(point: Point2D): Int = abs(this.x - point.x) + abs(this.y - point.y) + /** + * Calculates the "real" distance between two [Point2D]s + * as the square root of dx^2 + dy^2. + * + * This treats the points like standard coordinates on a + * cartesian grid system and calculates the distance of a + * line drawn between the two, also known as the Euclidean + * distance. + * + * For the manhattan distance between two points, see [distanceBetween]. + * + * @param point The point to check the distance to. + * @return The distance as a [Double] for full precision. + */ + fun realDistanceBetween(point: Point2D): Double { + val dx = point.x.toDouble() - this.x.toDouble() + val dy = point.y.toDouble() - this.y.toDouble() + return sqrt(dx * dx + dy * dy) + } + /** * Calculates the positive clockwise angle between two [Point2D] in degrees. * Angles are calculated from the sector's true north in the range of 0 =< angle < 360. diff --git a/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt index 62a7df6..af6cf74 100644 --- a/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt +++ b/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt @@ -63,7 +63,7 @@ class Point2DTest { } @Nested - inner class ManhattanDistance { + inner class ManhattanDistanceBetween { @Test fun targetToTheRight() { assertThat(Point2D(0, 0).distanceBetween(Point2D(0, 5))).isEqualTo(5) @@ -105,6 +105,49 @@ class Point2DTest { } } + @Nested + inner class RealDistanceBetween { + @Test + fun targetToTheRight() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(0, 5))).isEqualTo(5.0) + } + + @Test + fun targetIsBelow() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(0, -4))).isEqualTo(4.0) + } + + @Test + fun targetToTheLeft() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(-12, 0))).isEqualTo(12.0) + } + + @Test + fun targetIsAbove() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(0, 8))).isEqualTo(8.0) + } + + @Test + fun targetDiagonalTopRight() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(3, 3))).isEqualTo(4.242640687119285) + } + + @Test + fun targetDiagonalBottomRight() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(4, -4))).isEqualTo(5.656854249492381) + } + + @Test + fun targetDiagonalBottomLeft() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(-6, -6))).isEqualTo(8.48528137423857) + } + + @Test + fun targetDiagonalTopLeft() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(-12, 12))).isEqualTo(16.97056274847714) + } + } + @Nested inner class AngleBetween { @ParameterizedTest From 4ef35749e62d6563b614b563523faf6eb701c250 Mon Sep 17 00:00:00 2001 From: Thomas Plumpton Date: Fri, 13 Dec 2024 12:38:10 +0000 Subject: [PATCH 2/9] Math | Added LinearEquation to the math package --- .../libs/math/equation/LinearEquation.kt | 60 +++++++++++++++++++ .../libs/math/equation/LinearEquationTest.kt | 40 +++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 library/src/main/kotlin/io/github/tomplum/libs/math/equation/LinearEquation.kt create mode 100644 library/src/test/kotlin/io/github/tomplum/libs/math/equation/LinearEquationTest.kt diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/equation/LinearEquation.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/equation/LinearEquation.kt new file mode 100644 index 0000000..57d8a24 --- /dev/null +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/equation/LinearEquation.kt @@ -0,0 +1,60 @@ +package io.github.tomplum.libs.math.equation + +/** + * Represents a system of two linear equations in the form: + * + * a1 * x + b1 * y = c1 + * a2 * x + b2 * y = c2 + * + * This class provides a method to solve the system using Cramer's rule. + * + * @param a1 The coefficient of x in the first equation. + * @param b1 The coefficient of y in the first equation. + * @param c1 The constant term in the first equation. + * @param a2 The coefficient of x in the second equation. + * @param b2 The coefficient of y in the second equation. + * @param c2 The constant term in the second equation. + */ +data class LinearEquation( + val a1: Double, + val b1: Double, + val c1: Double, + val a2: Double, + val b2: Double, + val c2: Double +) { + /** + * Solves the system of two linear equations using Cramer's rule. + * + * Cramer's rule states that for a system of two linear equations: + * + * a1 * x + b1 * y = c1 + * a2 * x + b2 * y = c2 + * + * The solution is given by: + * + * x = (c1 * b2 - c2 * b1) / (a1 * b2 - a2 * b1) + * y = (a1 * c2 - a2 * c1) / (a1 * b2 - a2 * b1) + * + * If the determinant (a1 * b2 - a2 * b1) is 0, the system has no unique solution. + * + * @return A Pair of values (x, y) representing the solution to the system, + * or null if the system has no unique solution. + */ + fun solve(): Pair? { + val determinant = a1 * b2 - a2 * b1 + + // If determinant is zero, the system has no unique solution + if (determinant == 0.0) { + return null + } + + val determinantX = c1 * b2 - c2 * b1 + val determinantY = a1 * c2 - a2 * c1 + + val x = determinantX / determinant + val y = determinantY / determinant + + return x to y + } +} \ No newline at end of file diff --git a/library/src/test/kotlin/io/github/tomplum/libs/math/equation/LinearEquationTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/math/equation/LinearEquationTest.kt new file mode 100644 index 0000000..207beec --- /dev/null +++ b/library/src/test/kotlin/io/github/tomplum/libs/math/equation/LinearEquationTest.kt @@ -0,0 +1,40 @@ +package io.github.tomplum.libs.math.equation + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNull +import org.junit.jupiter.api.Test + +class LinearEquationTest { + @Test + fun `solves system with unique solution`() { + val equation = LinearEquation(2.0, 1.0, 8.0, 1.0, 3.0, 13.0) + val result = equation.solve() + + assertThat(result).isEqualTo(2.2 to 3.6) + } + + @Test + fun `returns null for no unique solution`() { + val equation = LinearEquation(1.0, -1.0, 0.0, 2.0, -2.0, 0.0) + val result = equation.solve() + + assertThat(result).isNull() + } + + @Test + fun `solves system with negative coefficients`() { + val equation = LinearEquation(-1.0, -2.0, -3.0, -3.0, -4.0, -5.0) + val result = equation.solve() + + assertThat(result).isEqualTo(-1.0 to 2.0) + } + + @Test + fun `solves system with fractional coefficients`() { + val equation = LinearEquation(0.5, 1.5, 3.0, 1.5, 0.5, 4.0) + val result = equation.solve() + + assertThat(result).isEqualTo(2.25 to 1.25) + } +} \ No newline at end of file From 36d0a1cd36661b8ddad1e891fde3acc6d77107e0 Mon Sep 17 00:00:00 2001 From: Thomas Plumpton Date: Fri, 13 Dec 2024 12:43:17 +0000 Subject: [PATCH 3/9] Math | Added isWithinBounds function to AdventMap2D --- .../github/tomplum/libs/math/map/AdventMap.kt | 2 +- .../tomplum/libs/math/map/AdventMap2D.kt | 15 ++++ .../github/tomplum/libs/math/point/Point2D.kt | 4 +- .../tomplum/libs/math/map/AdventMap2DTest.kt | 86 +++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap.kt index 0bc1afe..5652376 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap.kt @@ -7,7 +7,7 @@ import io.github.tomplum.libs.math.point.Point * * Lots of the days involve the concept of a 'Map' or a 'Maze' which can be represented as a cartesian grid. * A cartesian-style grid is internally maintained that maps tiles to [Point] coordinates. - * Several sub-type abstract classes exist mapping the different dimensions against the tiles. + * Several subtype abstract classes exist mapping the different dimensions against the tiles. * * @param T The type of [MapTile] that will be recorded in the grid. */ diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap2D.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap2D.kt index cee44ba..039037b 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap2D.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap2D.kt @@ -44,6 +44,21 @@ abstract class AdventMap2D>: AdventMap() { return positions.flatMap { pos -> pos.orthogonallyAdjacent() }.associateWith(this::getTile) } + /** + * Determines if the given [position] falls within the given [bounds]. + * If [bounds] are not provided, then they default to the current known + * bounds of the map [data] denoted by ([xMin], [yMin]) and ([xMax], [yMax]). + * + * @param position The position to check for. + * @param bounds Opposite corners of a custom square boundary to check within. + * @returns true if the given [position] falls within the bounds, else false. + */ + protected fun isWithinBounds(position: Point2D, bounds: Pair? = null): Boolean { + val (xMin, yMin) = bounds?.first ?: Point2D(xMin()!!, yMin()!!) + val (xMax, yMax) = bounds?.second ?: Point2D(xMax()!!, yMax()!!) + return position.x in xMin..xMax && position.y >= yMin && position.y <= yMax + } + /** * @return The minimum x-ordinate currently recorded in the map. */ diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt index 9b40eb2..73fb045 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt @@ -35,7 +35,7 @@ data class Point2D(val x: Int, val y: Int) : Point, Comparable { /** * Orthogonally adjacent points are the 4 points immediately horizontal or vertical. - * A.K.A 'Edge Adjacent' + * Also-known-as 'Edge Adjacent' * @see adjacent for a function that returns on the diagonal too. * @return The four points that are orthogonally adjacent. */ @@ -75,7 +75,7 @@ data class Point2D(val x: Int, val y: Int) : Point, Comparable { * line drawn between the two, also known as the Euclidean * distance. * - * For the manhattan distance between two points, see [distanceBetween]. + * For the Manhattan distance between two points, see [distanceBetween]. * * @param point The point to check the distance to. * @return The distance as a [Double] for full precision. diff --git a/library/src/test/kotlin/io/github/tomplum/libs/math/map/AdventMap2DTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/math/map/AdventMap2DTest.kt index c991053..96edad2 100644 --- a/library/src/test/kotlin/io/github/tomplum/libs/math/map/AdventMap2DTest.kt +++ b/library/src/test/kotlin/io/github/tomplum/libs/math/map/AdventMap2DTest.kt @@ -103,6 +103,91 @@ class AdventMap2DTest { } } + @Nested + inner class IsWithinBounds { + @Test + fun `position is within default bounds`() { + val position = Point2D(5, 5) + + val map = TestAdventMap2D() + map.addExampleTile(Point2D(0, 0), TestMapTile(4)) + map.addExampleTile(Point2D(10, 10), TestMapTile(4)) + + val result = map.isWithinBoundsExample(position) + assertThat(result).isTrue() + } + + @Test + fun `position is outside default bounds`() { + // Assuming default bounds are xMin=0, yMin=0, xMax=10, yMax=10 + val position = Point2D(11, 5) + + val map = TestAdventMap2D() + map.addExampleTile(Point2D(0, 0), TestMapTile(4)) + map.addExampleTile(Point2D(10, 10), TestMapTile(4)) + + val result = map.isWithinBoundsExample(position) + assertThat(result).isFalse() + } + + @Test + fun `position is within custom bounds`() { + val position = Point2D(3, 4) + val customBounds = Pair(Point2D(2, 3), Point2D(5, 6)) + + val map = TestAdventMap2D() + + val result = map.isWithinBoundsExample(position, customBounds) + assertThat(result).isTrue() + } + + @Test + fun `position is on the boundary within custom bounds`() { + val position = Point2D(5, 6) + val customBounds = Pair(Point2D(2, 3), Point2D(5, 6)) + + val map = TestAdventMap2D() + + val result = map.isWithinBoundsExample(position, customBounds) + assertThat(result).isTrue() + } + + @Test + fun `position is outside custom bounds`() { + val position = Point2D(6, 7) + val customBounds = Pair(Point2D(2, 3), Point2D(5, 6)) + + val map = TestAdventMap2D() + + val result = map.isWithinBoundsExample(position, customBounds) + assertThat(result).isFalse() + } + + @Test + fun `position is on the lower boundary within default bounds`() { + val position = Point2D(0, 0) + + val map = TestAdventMap2D() + map.addExampleTile(Point2D(0, 0), TestMapTile(4)) + map.addExampleTile(Point2D(10, 10), TestMapTile(4)) + + val result = map.isWithinBoundsExample(position) + assertThat(result).isTrue() + } + + @Test + fun `position is outside lower boundary within default bounds`() { + val position = Point2D(-1, -1) + + val map = TestAdventMap2D() + map.addExampleTile(Point2D(0, 0), TestMapTile(4)) + map.addExampleTile(Point2D(10, 10), TestMapTile(4)) + + val result = map.isWithinBoundsExample(position) + assertThat(result).isFalse() + } + } + @Nested inner class HasTile { @Test @@ -460,6 +545,7 @@ class AdventMap2DTest { fun getExampleTile(pos: Point2D) = getTile(pos) fun getExampleTile(pos: Point2D, default: TestMapTile) = getTile(pos, default) fun hasRecordedExample(pos: Point2D) = hasRecorded(pos) + fun isWithinBoundsExample(position: Point2D, bounds: Pair? = null) = isWithinBounds(position, bounds) fun hasTileExample(tile: TestMapTile) = hasTile(tile) fun filterPointsExample(positions: Set) = filterPoints(positions) fun filterTilesExample(predicate: (TestMapTile) -> Boolean) = filterTiles(predicate) From 6507b0912735c769452b28a4b73a9435248d9b3b Mon Sep 17 00:00:00 2001 From: Thomas Plumpton Date: Sun, 15 Dec 2024 16:35:27 +0000 Subject: [PATCH 4/9] Math | Added fromChar static factory constructor to Direction enum --- .../io/github/tomplum/libs/math/Direction.kt | 10 ++++++ .../github/tomplum/libs/math/DirectionTest.kt | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt index 0d605cd..7fcab6a 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt @@ -14,6 +14,16 @@ enum class Direction(private val degree: Int) { LEFT(270), TOP_LEFT(315); + companion object { + fun fromChar(value: Char) = when (value) { + '^' -> UP + '>' -> RIGHT + 'v' -> DOWN + '<' -> LEFT + else -> throw IllegalArgumentException("Invalid Direction String: $value") + } + } + /** * Rotates the current direction by the given [angle]. * Angles can be negative to rotate anti-clockwise. diff --git a/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt index 6219227..0515257 100644 --- a/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt +++ b/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt @@ -10,6 +10,37 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource class DirectionTest { + @Nested + inner class FromCharStaticFactoryConstructor { + @Test + fun up() { + assertThat(Direction.fromChar('^')).isEqualTo(UP) + } + + @Test + fun right() { + assertThat(Direction.fromChar('>')).isEqualTo(RIGHT) + } + + @Test + fun down() { + assertThat(Direction.fromChar('v')).isEqualTo(DOWN) + } + + @Test + fun left() { + assertThat(Direction.fromChar('<')).isEqualTo(LEFT) + } + + @Test + fun invalidChar() { + val e = assertThrows { + Direction.fromChar('A') + } + assertThat(e.message).isEqualTo("Invalid Direction String: A") + } + } + @Nested inner class Rotate { @Nested From 0ae7ec3c40e7f5201ae335c96ea68f28a102d18e Mon Sep 17 00:00:00 2001 From: Thomas Plumpton Date: Mon, 16 Dec 2024 17:44:09 +0000 Subject: [PATCH 5/9] Math | Direction enum now has an isOpposite function --- .../io/github/tomplum/libs/math/Direction.kt | 18 +++++++ .../github/tomplum/libs/math/DirectionTest.kt | 52 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt index 7fcab6a..a6e2a87 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt @@ -36,6 +36,24 @@ enum class Direction(private val degree: Int) { .find { it.degree == normalise(angle) } ?: throw IllegalArgumentException("Invalid Angle $angle") + /** + * Determines if the direction is opposite the [other]. + * + * @param other The other direction to compare against. + * @returns true if the [other] direction is opposite this one, else false. + */ + fun isOpposite(other: Direction): Boolean = when { + this == UP -> other == DOWN + this == RIGHT -> other == LEFT + this == DOWN -> other == UP + this == LEFT -> other == RIGHT + this == TOP_RIGHT -> other == BOTTOM_LEFT + this == BOTTOM_RIGHT -> other == TOP_LEFT + this == BOTTOM_LEFT -> other == TOP_RIGHT + this == TOP_LEFT -> other == BOTTOM_RIGHT + else -> false + } + private fun normalise(angle: Int): Int { var targetDegree = degree + angle diff --git a/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt index 0515257..a9930f7 100644 --- a/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt +++ b/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt @@ -2,11 +2,14 @@ package io.github.tomplum.libs.math import assertk.assertThat import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue import io.github.tomplum.libs.math.Direction.* import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.junit.jupiter.params.provider.ValueSource class DirectionTest { @@ -104,4 +107,53 @@ class DirectionTest { assertThat(e.message).isEqualTo("Invalid Angle $angle") } } + + @Nested + inner class Opposite { + @ParameterizedTest(name = "{0} is opposite of {1}") + @CsvSource( + "UP, DOWN", + "DOWN, UP", + "LEFT, RIGHT", + "RIGHT, LEFT", + "TOP_RIGHT, BOTTOM_LEFT", + "BOTTOM_LEFT, TOP_RIGHT", + "BOTTOM_RIGHT, TOP_LEFT", + "TOP_LEFT, BOTTOM_RIGHT" + ) + fun `direction is opposite of expected`(direction: Direction, opposite: Direction) { + assertThat(direction.isOpposite(opposite)).isTrue() + } + + @ParameterizedTest(name = "{0} is not opposite of {1}") + @CsvSource( + "UP, UP", + "DOWN, DOWN", + "LEFT, LEFT", + "RIGHT, RIGHT", + "TOP_RIGHT, TOP_RIGHT", + "BOTTOM_LEFT, BOTTOM_LEFT", + "BOTTOM_RIGHT, BOTTOM_RIGHT", + "TOP_LEFT, TOP_LEFT" + ) + fun `direction is not opposite of itself`(direction: Direction, same: Direction) { + assertThat(direction.isOpposite(same)).isFalse() + } + + @ParameterizedTest(name = "{0} is not opposite of unrelated direction {1}") + @CsvSource( + "UP, RIGHT", + "UP, TOP_RIGHT", + "DOWN, LEFT", + "LEFT, BOTTOM_LEFT", + "RIGHT, TOP_LEFT", + "TOP_RIGHT, TOP_LEFT", + "BOTTOM_LEFT, BOTTOM_RIGHT", + "BOTTOM_RIGHT, TOP_RIGHT", + "TOP_LEFT, BOTTOM_LEFT" + ) + fun `direction is not opposite of unrelated`(direction: Direction, unrelated: Direction) { + assertThat(direction.isOpposite(unrelated)).isFalse() + } + } } \ No newline at end of file From 99390101f6792f7ce055a7c73ff0d0780ab3639b Mon Sep 17 00:00:00 2001 From: Thomas Plumpton Date: Mon, 16 Dec 2024 17:44:36 +0000 Subject: [PATCH 6/9] Math | Point2D shift function now has an optional isRasterSystem argument --- .../github/tomplum/libs/math/point/Point2D.kt | 11 +++-- .../tomplum/libs/math/point/Point2DTest.kt | 47 ++++++++++++++++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt index 73fb045..4423436 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt @@ -105,12 +105,17 @@ data class Point2D(val x: Int, val y: Int) : Point, Comparable { /** * Shifts the [Point2D] one unit in the given [direction] unless specified by the [units] parameter. * E.g. (0, 0) shifted [Direction.RIGHT] would become (1, 0) + * + * @param direction The direction to shift in. + * @param units The number of units to shift by. + * @param isRasterSystem Whether the shift should be treated as if it's in a raster or screen coordinate system (Up goes down below the x-axis). + * * @return A point at the shifted location. */ - fun shift(direction: Direction, units: Int = 1): Point2D = when (direction) { - Direction.UP -> Point2D(x, y + units) + fun shift(direction: Direction, units: Int = 1, isRasterSystem: Boolean = false): Point2D = when (direction) { + Direction.UP -> Point2D(x, if (isRasterSystem) y - units else y + units) Direction.RIGHT -> Point2D(x + units, y) - Direction.DOWN -> Point2D(x, y - units) + Direction.DOWN -> Point2D(x, if (isRasterSystem) y + units else y - units) Direction.LEFT -> Point2D(x - units, y) Direction.TOP_RIGHT -> Point2D(x + units, y + units) Direction.BOTTOM_RIGHT -> Point2D(x + units, y - units) diff --git a/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt index af6cf74..9265715 100644 --- a/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt +++ b/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt @@ -214,45 +214,90 @@ class Point2DTest { assertThat(Point2D(0, 0).shift(Direction.UP)).isEqualTo(Point2D(0, 1)) } + @Test + fun shiftUp_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.UP, isRasterSystem = true)).isEqualTo(Point2D(0, -1)) + } + @Test fun shiftRight() { assertThat(Point2D(0, 0).shift(Direction.RIGHT)).isEqualTo(Point2D(1, 0)) } + @Test + fun shiftRight_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.RIGHT, isRasterSystem = true)).isEqualTo(Point2D(1, 0)) + } + @Test fun shiftDown() { assertThat(Point2D(0, 0).shift(Direction.DOWN)).isEqualTo(Point2D(0, -1)) } + @Test + fun shiftDown_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.DOWN, isRasterSystem = true)).isEqualTo(Point2D(0, 1)) + } + @Test fun shiftLeft() { assertThat(Point2D(0, 0).shift(Direction.LEFT)).isEqualTo(Point2D(-1, 0)) } + @Test + fun shiftLeft_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.LEFT, isRasterSystem = true)).isEqualTo(Point2D(-1, 0)) + } + @Test fun shiftTopRight() { assertThat(Point2D(0, 0).shift(Direction.TOP_RIGHT)).isEqualTo(Point2D(1, 1)) } + @Test + fun shiftTopRight_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.TOP_RIGHT, isRasterSystem = true)).isEqualTo(Point2D(1, 1)) + } + @Test fun shiftBottomRight() { assertThat(Point2D(0, 0).shift(Direction.BOTTOM_RIGHT)).isEqualTo(Point2D(1, -1)) } + @Test + fun shiftBottomRight_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.BOTTOM_RIGHT, isRasterSystem = true)).isEqualTo(Point2D(1, -1)) + } + @Test fun shiftBottomLeft() { assertThat(Point2D(0, 0).shift(Direction.BOTTOM_LEFT)).isEqualTo(Point2D(-1, -1)) } + @Test + fun shiftBottomLeft_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.BOTTOM_LEFT, isRasterSystem = true)).isEqualTo(Point2D(-1, -1)) + } + @Test fun shiftTopLeft() { assertThat(Point2D(0, 0).shift(Direction.TOP_LEFT)).isEqualTo(Point2D(-1, 1)) } @Test - fun shiftUpMoreThanOneUnit() { + fun shiftTopLeft_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.TOP_LEFT, isRasterSystem = true)).isEqualTo(Point2D(-1, 1)) + } + + @Test + fun shiftUp_givenUnit() { assertThat(Point2D(0, 0).shift(Direction.UP, 4)).isEqualTo(Point2D(0, 4)) } + + @Test + fun shiftUp_givenUnit_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.UP, 4, isRasterSystem = true)).isEqualTo(Point2D(0, -4)) + } } @Nested From 9ba203cd350521b45a685a7a44d18402c684b7d7 Mon Sep 17 00:00:00 2001 From: Thomas Plumpton Date: Mon, 16 Dec 2024 17:46:21 +0000 Subject: [PATCH 7/9] Math | DijkstrasAlgorithm now makes processNode predicate optional --- .../libs/algorithm/DijkstrasAlgorithm.kt | 4 +- .../algorithm/DijkstrasAlgorithmKtTest.kt | 281 +++++++++++------- .../resources/input/2024/day16/example-1.txt | 15 + .../resources/input/2024/day16/example-2.txt | 17 ++ .../test/resources/input/2024/day16/input.txt | 141 +++++++++ 5 files changed, 354 insertions(+), 104 deletions(-) create mode 100644 library/src/test/resources/input/2024/day16/example-1.txt create mode 100644 library/src/test/resources/input/2024/day16/example-2.txt create mode 100644 library/src/test/resources/input/2024/day16/input.txt diff --git a/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt b/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt index d7ceb59..fb0b567 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt @@ -27,8 +27,8 @@ data class Node(val value: T, val distance: Int): Comparable> { fun dijkstraShortestPath( startingPositions: Collection, evaluateAdjacency: (currentNode: Node) -> Collection>, - processNode: (currentNode: Node, adjacentNode: Node) -> Node, - terminates: (currentNode: Node) -> Boolean + terminates: (currentNode: Node) -> Boolean, + processNode: (currentNode: Node, adjacentNode: Node) -> Node = { _, adjacentNode -> adjacentNode } ): Int { // A map of nodes and the shortest distance from the given starting positions to it val distance = mutableMapOf() diff --git a/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmKtTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmKtTest.kt index 5b7f0e4..85b7c51 100644 --- a/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmKtTest.kt +++ b/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmKtTest.kt @@ -11,8 +11,129 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test class DijkstrasAlgorithmKtTest { + private data class CrucibleLocation( + val position: Point2D, + val direction: Direction, + val isMovingStraight: Boolean, + var consecutiveSteps: Int = 1 + ) + + private class CityMap(data: List): AdventMap2D() { + private val factoryLocation: Point2D + + init { + var x = 0 + var y = 0 + + data.forEach { row -> + row.forEach { column -> + val tile = CityBlock(column.toString().toInt()) + val position = Point2D(x, y) + + addTile(position, tile) + x++ + } + x = 0 + y++ + } + + factoryLocation = Point2D(xMax()!!, yMax()!!) + } + + fun solutionPartOne(): Int = dijkstraShortestPath( + startingPositions = listOf( + CrucibleLocation(Point2D.origin(), Direction.RIGHT, true, 0), + CrucibleLocation(Point2D.origin(), Direction.UP, true, 0) + ), + evaluateAdjacency = { (currentNode) -> + val (currentPos, currentDirection, _, consecutiveSteps) = currentNode + + currentPos.let { pos -> + val rightDirection = currentDirection.rotate(90) + val rightPosition = pos.shift(rightDirection) + val right = CrucibleLocation(rightPosition, rightDirection, false) + + val leftDirection = currentDirection.rotate(-90) + val leftPosition = pos.shift(leftDirection) + val left = CrucibleLocation(leftPosition, leftDirection, false) + + val candidates = mutableListOf(left, right) + + if (consecutiveSteps < 3) { + val straightPosition = pos.shift(currentDirection) + val straight = CrucibleLocation(straightPosition, currentDirection, true) + candidates.add(straight) + } + + candidates.filter { (position) -> hasRecorded(position) } + .map { location -> Node(location, getTile(location.position).value) } + } + }, + processNode = { currentNode, adjacentNode -> + val updatedNodeValue = adjacentNode.value.apply { + consecutiveSteps = if (adjacentNode.value.isMovingStraight) { + currentNode.value.consecutiveSteps + 1 + } else 1 + } + + Node(updatedNodeValue, adjacentNode.distance) + }, + terminates = { currentNode -> + currentNode.value.position == factoryLocation + } + ) + + fun solutionPartTwo(): Int = dijkstraShortestPath( + startingPositions = listOf( + CrucibleLocation(Point2D.origin(), Direction.RIGHT, true, 0), + CrucibleLocation(Point2D.origin(), Direction.UP, true, 0) + ), + evaluateAdjacency = { (currentNode) -> + val (currentPos, currentDirection, _, consecutiveSteps) = currentNode + + currentPos.let { pos -> + val candidates = mutableListOf() + + if (consecutiveSteps >= 4) { + val rightDirection = currentDirection.rotate(90) + val right = CrucibleLocation(pos.shift(rightDirection), rightDirection, false) + candidates.add(right) + + val leftDirection = currentDirection.rotate(-90) + val left = CrucibleLocation(pos.shift(leftDirection), leftDirection, false) + candidates.add(left) + } + + if (consecutiveSteps < 10) { + val straight = CrucibleLocation(pos.shift(currentDirection), currentDirection, true) + candidates.add(straight) + } + + candidates.filter { (position) -> hasRecorded(position) } + .map { location -> Node(location, getTile(location.position).value) } + } + }, + processNode = { currentNode, adjacentNode -> + val updatedNodeValue = adjacentNode.value.apply { + consecutiveSteps = if (adjacentNode.value.isMovingStraight) { + currentNode.value.consecutiveSteps + 1 + } else 1 + } + + Node(updatedNodeValue, adjacentNode.distance) + }, + terminates = { currentNode -> + val hasReachedFactory = currentNode.value.position == factoryLocation + val hasTravelledMoreThanFourStraight = currentNode.value.consecutiveSteps >= 4 + hasReachedFactory && hasTravelledMoreThanFourStraight + } + ) + } + + private class CityBlock(override val value: Int): MapTile(value) + @Nested - class ExamplesFrom2023Day17 { + inner class ExamplesFrom2023Day17 { private val exampleInputLocation = "2023/day17" @Test @@ -35,126 +156,82 @@ class DijkstrasAlgorithmKtTest { val citMap = CityMap(data.value) assertThat(citMap.solutionPartTwo()).isEqualTo(71) } + } - private data class CrucibleLocation( - val position: Point2D, - val direction: Direction, - val isMovingStraight: Boolean, - var consecutiveSteps: Int = 1 - ) - - private class CityMap(data: List): AdventMap2D() { - private val factoryLocation: Point2D - - init { - var x = 0 - var y = 0 - - data.forEach { row -> - row.forEach { column -> - val tile = CityBlock(column.toString().toInt()) - val position = Point2D(x, y) + private class MazeTile(override val value: Char): MapTile(value) { + fun isReindeerStart() = value == 'S' + fun isEnd() = value == 'E' + fun isTraversable() = value == '.' || isReindeerStart() || isEnd() + } - addTile(position, tile) - x++ - } - x = 0 - y++ - } + private class ReindeerMaze(data: List): AdventMap2D() { + private val directions = listOf(Direction.UP, Direction.DOWN, Direction.LEFT, Direction.RIGHT) - factoryLocation = Point2D(xMax()!!, yMax()!!) + init { + init(data) { + MazeTile(it as Char) } + } - fun solutionPartOne(): Int = dijkstraShortestPath( - startingPositions = listOf( - CrucibleLocation(Point2D.origin(), Direction.RIGHT, true, 0), - CrucibleLocation(Point2D.origin(), Direction.UP, true, 0) - ), - evaluateAdjacency = { (currentNode) -> - val (currentPos, currentDirection, _, consecutiveSteps) = currentNode - - currentPos.let { pos -> - val rightDirection = currentDirection.rotate(90) - val rightPosition = pos.shift(rightDirection) - val right = CrucibleLocation(rightPosition, rightDirection, false) + fun calculateLowestPossibleScore(): Int { + val (startingPosition) = findTile { it.isReindeerStart() }!! + val startingDirection = Direction.RIGHT - val leftDirection = currentDirection.rotate(-90) - val leftPosition = pos.shift(leftDirection) - val left = CrucibleLocation(leftPosition, leftDirection, false) + return dijkstraShortestPath( + startingPositions = listOf(startingPosition to startingDirection), + evaluateAdjacency = { currentNode -> + val (currentPosition, direction) = currentNode.value + val adjacentNodes = mutableListOf>>() - val candidates = mutableListOf(left, right) + val nextPosition = currentPosition.shift(direction) + if (getTile(nextPosition, MazeTile('?')).isTraversable()) { + adjacentNodes.add(Node(nextPosition to direction, 1)) + } - if (consecutiveSteps < 3) { - val straightPosition = pos.shift(currentDirection) - val straight = CrucibleLocation(straightPosition, currentDirection, true) - candidates.add(straight) + val rotations = directions + .filterNot { currentDirection -> + val isSameDirection = currentDirection == direction + val isBacktracking = currentDirection.isOpposite(direction) + isSameDirection || isBacktracking + } + .map { currentDirection -> + Node(currentPosition to currentDirection, 1000) } - candidates.filter { (position) -> hasRecorded(position) } - .map { location -> Node(location, getTile(location.position).value) } - } - }, - processNode = { currentNode, adjacentNode -> - val updatedNodeValue = adjacentNode.value.apply { - consecutiveSteps = if (adjacentNode.value.isMovingStraight) { - currentNode.value.consecutiveSteps + 1 - } else 1 - } + adjacentNodes.addAll(rotations) - Node(updatedNodeValue, adjacentNode.distance) + adjacentNodes }, terminates = { currentNode -> - currentNode.value.position == factoryLocation + getTile(currentNode.value.first, MazeTile('?')).isEnd() } ) + } + } - fun solutionPartTwo(): Int = dijkstraShortestPath( - startingPositions = listOf( - CrucibleLocation(Point2D.origin(), Direction.RIGHT, true, 0), - CrucibleLocation(Point2D.origin(), Direction.UP, true, 0) - ), - evaluateAdjacency = { (currentNode) -> - val (currentPos, currentDirection, _, consecutiveSteps) = currentNode - - currentPos.let { pos -> - val candidates = mutableListOf() - - if (consecutiveSteps >= 4) { - val rightDirection = currentDirection.rotate(90) - val right = CrucibleLocation(pos.shift(rightDirection), rightDirection, false) - candidates.add(right) - - val leftDirection = currentDirection.rotate(-90) - val left = CrucibleLocation(pos.shift(leftDirection), leftDirection, false) - candidates.add(left) - } - - if (consecutiveSteps < 10) { - val straight = CrucibleLocation(pos.shift(currentDirection), currentDirection, true) - candidates.add(straight) - } + @Nested + inner class ExamplesFrom2024Day16 { + private val exampleInputLocation = "2024/day16" - candidates.filter { (position) -> hasRecorded(position) } - .map { location -> Node(location, getTile(location.position).value) } - } - }, - processNode = { currentNode, adjacentNode -> - val updatedNodeValue = adjacentNode.value.apply { - consecutiveSteps = if (adjacentNode.value.isMovingStraight) { - currentNode.value.consecutiveSteps + 1 - } else 1 - } + @Test + fun partOneExampleOne() { + val data = TestInputReader.read("$exampleInputLocation/example-1.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.calculateLowestPossibleScore()).isEqualTo(7036) + } - Node(updatedNodeValue, adjacentNode.distance) - }, - terminates = { currentNode -> - val hasReachedFactory = currentNode.value.position == factoryLocation - val hasTravelledMoreThanFourStraight = currentNode.value.consecutiveSteps >= 4 - hasReachedFactory && hasTravelledMoreThanFourStraight - } - ) + @Test + fun partOneExampleTwo() { + val data = TestInputReader.read("$exampleInputLocation/example-2.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.calculateLowestPossibleScore()).isEqualTo(11048) } - private class CityBlock(override val value: Int): MapTile(value) + @Test + fun partOneSolution() { + val data = TestInputReader.read("$exampleInputLocation/input.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.calculateLowestPossibleScore()).isEqualTo(65436) + } } } \ No newline at end of file diff --git a/library/src/test/resources/input/2024/day16/example-1.txt b/library/src/test/resources/input/2024/day16/example-1.txt new file mode 100644 index 0000000..6a5bb85 --- /dev/null +++ b/library/src/test/resources/input/2024/day16/example-1.txt @@ -0,0 +1,15 @@ +############### +#.......#....E# +#.#.###.#.###.# +#.....#.#...#.# +#.###.#####.#.# +#.#.#.......#.# +#.#.#####.###.# +#...........#.# +###.#.#####.#.# +#...#.....#.#.# +#.#.#.###.#.#.# +#.....#...#.#.# +#.###.#.#.#.#.# +#S..#.....#...# +############### \ No newline at end of file diff --git a/library/src/test/resources/input/2024/day16/example-2.txt b/library/src/test/resources/input/2024/day16/example-2.txt new file mode 100644 index 0000000..54f4cd7 --- /dev/null +++ b/library/src/test/resources/input/2024/day16/example-2.txt @@ -0,0 +1,17 @@ +################# +#...#...#...#..E# +#.#.#.#.#.#.#.#.# +#.#.#.#...#...#.# +#.#.#.#.###.#.#.# +#...#.#.#.....#.# +#.#.#.#.#.#####.# +#.#...#.#.#.....# +#.#.#####.#.###.# +#.#.#.......#...# +#.#.###.#####.### +#.#.#...#.....#.# +#.#.#.#####.###.# +#.#.#.........#.# +#.#.#.#########.# +#S#.............# +################# \ No newline at end of file diff --git a/library/src/test/resources/input/2024/day16/input.txt b/library/src/test/resources/input/2024/day16/input.txt new file mode 100644 index 0000000..de76ceb --- /dev/null +++ b/library/src/test/resources/input/2024/day16/input.txt @@ -0,0 +1,141 @@ +############################################################################################################################################# +#.....#...#.....#...#.........#...#.....#...........#.........#...................#.#.........#.......#.........#.......#.......#.........#E# +#.#.#.#.###.#.#.#.#.#.###.###.###.#.#.#.###.#######.#.#.#####.#.#########.#####.#.#.#.#.###.#.#####.#.###.#####.#.#####.#.###.#.#.#######.#.# +#.#.#.............#...#...#.#...#.#.#.....................#...#.#.......#.#.....#.#.....#...#.......#...#.....#...#...#...#.#...#.....#.#.#.# +#.#.#.#####.#.#.#.#####.###.###.#.#.#.#.#.###.#.#.#.#####.###.###.#####.###.#####.#####.#.#############.#.###.#.###.#.#####.#.###.###.#.#.#.# +#.#.#.....#.#.#.#.#.#...#.....#.#...#.#.#.#.#...#...#...#...#...#.....#.....#...#.....#.#...#...#.....#.#...#.#.....#.........#.#...#.#.#.#.# +###.#####.#.#.###.#.#.###.###.#.#.###.#.#.#.#.#######.#.###.###.#.###.#########.#####.#.###.#.###.###.#.#.###.#.###############.###.#.#.#.#.# +#...#.....#.#.......#.#...#...#.#...#.#...#.#...#.....#...#.#.#.#.....#.................................#...#.#.....#.........#.....#.#.#.#.# +#.###.#####.#.###.###.#####.###.###.#.#####.###.#####.###.#.#.#.#.#####.#.#.#######.###.#.#.#.#####.#####.#.#.#.###.#####.###.#######.#.#.#.# +#.#.......#.#...#.#...#...#.....#...#...#.............#.#...#...#.#...#.#.#.#.....#...........................#...........#.#.....#...#.....# +#.#######.###.#.###.###.#.###.###.#####.###############.#####.###.#.#.#.#.###.###.###.#####.###########.#.#################.#####.#.#.####### +#...#.........#.....#...#...#.#.....#.#.........#...........#...#...#.#.#...#.#...#.....#.#.#.....#.....#.............................#.....# +###.#.###############.#####.#.#####.#.#########.###########.###.#####.#.###.#.#.#######.#.#.###.#.#.###.#.#####.#.#####.###.#########.#.###.# +#...#...#.#.....#.........#.#.....#.#.........#.#.............#...#...#.#.#.#.#.......#...#.....#.#...#.#.#...#...#...#...#...........#.#...# +#.#####.#.#.###.#.###.#####.#####.#.#####.#.###.#.###############.#.###.#.#.#.#######.###.#.#####.#.#.#.#.#.#.#####.#.###.###########.#.#.#.# +#.....#...#...#.#.#.#.......#...#.#.......#.#...#...............#...#...#.#.#.#.#...#...#...#.....#.#.#.#...#...#...#...#...#...........#...# +#.###.###.###.#.#.#.#.#######.#.#.#######.###.###.###########.#########.#.#.#.#.#.#.###.###.#.#.###.#.#########.###.#.#####.#.#.#####.#####.# +#.#.#.#...#...#...#.#.#.....#.#.#.#.#...#.#...#.#.....#.....#.#.......#.....#.#...#.#...#.....#...#.#.........#...#.#.....#...#...........#.# +#.#.#.###.#.#######.#.#.###.#.#.#.#.#.#.#.#.###.#####.#####.#.#.#.###.#####.#.#####.#.###########.#####.#####.###.#.###.#######.#######.#.#.# +#...#...#.#.#.......#.#...#...#.#.#.#.#...#.#.......#...#...#...#...#...#...#.#...#.#...........#.....#.#.......#.#...#.....#...............# +###.###.#.#.#######.#.###.#####.#.#.#.#####.###.###.###.#.#.#.#####.###.#.###.#.#.#.###########.#####.#.#########.#.#####.#.#.#.#####.#####.# +#...#...#.#.#.....#.#...#.#...#.#.#...#...#...#.#...#...#.......#...#.#...#...#...#...#...#...#.....#.#.....#.....#.#.....#.#...#...#.#...#.# +#####.#####.#.###.#.###.#.#.#.#.#.###.###.###.#.#.###.#####.###.#.###.###.#.###.#.#.#.#.#.#.#.###.###.#.###.#.#######.#####.#.#.#.#.#.#.#.#.# +#.....#.....#...#.#...#.#.#.#...#...#.......#.#.#.........#...#.#.#.....#.#.....#...#...#...#...#.....#...#.#...#...#.#...#...#...#...#.#.#.# +#.#####.#######.#.#.###.#.#.#######.#######.#.#.#########.###.#.#.#.#.#.#.#################.###.###.###.###.###.#.#.#.#.###.#.#########.#.#.# +#.....#.#.......#...#.....#.......#.....#...#.#...#.....#.....#.#.#.#...#.#...........#.....#.....#.....#...#.#...#.#.#.....#...........#...# +#.###.#.#.#.###.###.#.###.###.#########.###.#.#####.###.#######.#.#.###.#.###.#.#####.#.#####.###.#######.#.#.#####.#.###.#.#.#.#.#.###.##### +#.#.#...#.#...#...#.....#.#...#.......#...#.#.....#...#.....#...#.#...#.#...#.#.#.....#...#...#.#.....#...#...#.....#...#.#.....#.#...#.#...# +#.#.#########.#.#.#####.#.#####.#.###.###.#.#.###.#.#######.#.###.###.#.###.#.#.###.#####.#.#.#.#####.#.#######.#####.#.#.#.#####.###.#.#.#.# +#.............#.#.....#.#.....#.#.#.#.....#...#...#.#.........#.#.#...#.#...#.#.....#...#.#.#.#.......#.#.....#...#...#.#.#.....#.#...#.#.#.# +#.###########.#.#####.#.#.###.#.#.#.###.#.#####.###.#.#######.#.#.#.###.#.#######.#.#.###.#.#.#.#######.#.###.#.#.#####.#.###.#.#.#.#.###.#.# +#.#...#...#...#.#.#.....#...#...#.#.....#...........#.....#.#.#.#.#.#...#.........#...#...#...#.....#.......#...#...#...#...#.#...#.#.#...#.# +###.#.#.#.#.#.#.#.#.#############.###########.#.#########.#.#.#.#.#.#########.#######.#.###.###.###.#########.#####.#.#####.#.###.#.###.###.# +#...#.#.#...#.#.#...#...........#.............#.......#...#.#.#.#.#.......#...#.....#.#.#.#...#.#...#.......#.#.....#...#...#.#...#.......#.# +#.###.#.#####.#.#####.#########.###.###.#####.#######.#.###.#.#.#.#.###.#.#.###.###.###.#.#.#.#.#.###.#####.#.#.#######.#.###.#.#########.#.# +#.#.....#...#.#.#...........#.#...#...#.....#.#.......#.#.....#.#.#.....#.......#.#...#.#.#.#.#.....#...#...#.#...#.....#...#.#.....#...#.#.# +#.#######.#.#.#.#.#########.#.###.###.#####.#.#.#######.#####.#.#.#.#.#######.###.###.#.#.#.#.#####.###.#.###.###.#.#######.#.#####.#.#.###.# +#.........#.#.#.#...#.#...#.....#.#.#...#...#.#.......#.....#...#...#.#.....#.#.....#...#...#...#...#...#.#.....#...#.....#.#...#.....#.....# +#.#########.#.#.###.#.#.#.#.#####.#.#.###.###.#.#######.###.#######.###.###.#.#####.#####.###.#.###.#.###.#.###.#####.###.#.###.#.#.###.##### +#...#...#...#.....#...#.#...#.....#...#...#...#.#.....#.#...#.....#.......#...#.....#...#.#.......#...#...#...#.#...#.#.#...#...#.#...#.#...# +###.#.###.###.#.#####.#.###.#.#####.###.###.#####.#.#.#.#.#.#.###.#######.#####.#####.#.#.#.#.###.#####.#####.#.#.#.#.#.#####.###.#.#.###.#.# +#.#...#...#.....#.....#.#.#.#.....#.#.#...........#.....#.#.#...#...#...#.....#.........#.#.#.....#...#.......#...#.#.......#.#...#.#.#...#.# +#.###.#.#.###.#.#.#####.#.#.#####.#.#.#################.#.#####.###.#.#.#####.#######.#.#.#.#######.#.###########.#########.#.#.###.#.#.###.# +#...#.#.#.#...#.#.#...#.#.#...#...#.....#...........#.....#.....#.#.#.#...#.........#.#...#.....#...#.......#.....#.......#...#.....#.....#.# +#.###.#.#.#.#.###.#.#.#.#.###.#.#######.#.#####.###.#.#####.#####.#.#.###.#########.#.#.#####.#.#.#######.#.#######.#.#####.#######.###.###.# +#.#...#.#.#.#.....#.#...#...#.#...#...#...........#.#...#...#.....#...#.#...#.....#...#.#.#.....#.#.......#.........#.......#.....#.........# +#.#.###.###.#.#####.#.#####.#.###.#.#.#.#########.#.#.#.#.#######.#.#.#.###.#####.###.#.#.#.#####.###.###.#############.#####.###.#.###.###.# +#.#...#.#...#.......#.....#.......#.#.....#...#.#.#...#...#.....#.#.#.#...#...#...#...#...#.#...#...#.#.......#.......#.....#.#...#...#...#.# +#.###.#.#.###.#####.#.###.#######.#.#####.#.#.#.#.#########.###.#.#.#.#.#.###.#.#.#.#######.#.#.###.###.#####.#.#####.#######.###.#.#.#.#.#.# +#.....#.....#.#...#.#.#.#.#...#.........#...#.#.#.#.....#.....#...#.#...#...#.#.......#...#.#.#...#...#.....#.#...#.....#...#...#.....#.#...# +#.###########.#.###.#.#.#.#.#.#####.###.#####.#.#.#.#####.###.#####.#######.#.#######.#.#.#.#.#.#####.#####.#.###.#####.#.#.###.###.###.###.# +#...#.....#...#.....#.#.#...#.#.......#...#.....#.#.........#.....#.#...#...#.#...#...#.#.#...#.#...#.......#...#.....#.#.#...#.#.....#.#...# +###.###.###.#######.#.#.#####.#.#######.#.#.#####.#.#.###########.#.#.#.#.###.#.#.#.###.#.#####.#.#.###########.#####.#.#.###.#.#.#.###.#.#.# +#.#...#.#...#.....#.#...#...#.....#...#.#.#.#.....#.#.#.......#...#...#...#...#.#.#.....#.....#.#.#.........#...#...#.#...#...#.#.......#...# +#.#.#.#.#.#.#.###.#####.#.#########.#.###.###.#####.#.#.#####.#.#.#########.###.#.###########.#.#.#########.#.###.#.#.#####.###.#.#.###.###.# +#.....#.#.#.#.#...#.....#.......#...#.....#...#.......#.#.....#.#.......#.#.....#.#.#.........#...#.......#...#...#.#.#.....#...#.........#.# +#.#####.#.#.#.#.###.#####.###.#.#.#######.#.#####.#####.###.###.#######.#.#######.#.#.#####.#####.#######.#####.#####.#.#######.#######.#.#.# +#...#...#.........#.#.....#...#.#.#.....#.#.....#.#...#...#.....#.#.....#.#.....#.#...#.........#...#...#.......#.....#.#.......#.#.....#.#.# +###.#.#.#.#.#.###.#.#.#####.#####.#.#.#.#.#####.#.###.###.#######.#.#####.#.###.#.#.#####.#####.###.#.#.#.#.#####.#####.#####.#.#.#.###.#.#.# +#.....#.#.#.....#...........#.....#.#.#.#.#.....#.....#...#...#...#.#...#...#...#.#.......#.....#.#...#.#.#.#...#...#.........#.....#.#.#.#.# +#######.#.#.#################.#####.#.###.#.#######.#.#.###.#.#.#.#.#.#.#.#.#.###.#########.###.#.#####.#.###.#.#.#.###.#######.#####.#.#.#.# +#.....#.#...#...............#...#.#.#.#...#...#.....#.#.......#.#.#...#.#.#.#.#...........#.....#.....#.#.#...#.#.#...#.......#.#.....#...#.# +#.###.#####.#.#####.#####.#.###.#.#.#.#.#####.#.###.#.#######.#.#.#.#.#.#.#.#.###########.#.#.#####.#.#.#.#.###.#####.#.#####.#.#.###.#####.# +#.#...#...#...#.......#...#...#.#.#.#...#...#.#.#...#...#...#.#.#.#...#...#.#...............#.....#.#.....#.#.#...#...#.#...#.#...#...#.....# +#.#.###.#.#.#.#.#.#.#.#.#.###.#.#.#.#####.#.#.#.#.#####.###.#.#.#.#.#.#####.###.#.#########.###.#.#.#######.#.###.#.###.#.#.###.###.###.###.# +#.#.#...#...#.#.#...#...#.#.#.#.#.#...#...#.#.#.#.#.........#.#...#.#.......#.#...#.......#.#...#.#.#.......#...#.#...#...#.........#.#.#...# +#.#.###.###.###.#########.#.#.#.#.###.#.###.#.#.###.#####.###.#.###.#.#.#####.#####.#####.#.#.###.###.#######.#.#.###.#########.#.#.#.#.#.#.# +#.#.....#.......#.........#...#.#...#.#.#...#.......#...#.#...#...#.#.#...........#.....#...#.#.#.....#.........#...#.#...#.....#.#.#...#...# +#.#######.#######.#########.###.#.#.#.#.###.#.#######.#.###.###.#.#.#.#####.#.###.#####.#####.#.#######.###.#######.#.#.#.#.###.#.#.#######.# +#.#.....#...#.#...#...#.....#.....#.#.#.#...#.#.....#.#.....#.#.#...#.#...#.#.........#.#.....#.....#...#.#.........#.#.#...#.....#.......#.# +#.#.###.###.#.#.###.#.#.#########.#.#.#.#.#.#.#.#.#.#.#######.#.#####.#.#.###.#.#######.#.#####.###.#.###.#######.#.#.#.#######.#########.#.# +#.....#...#...#.#...#.#.#.....#...#.#...#.#...#...#...#.......#...#...#.#...#.#...#.....#.#...#.#.#...#.....#.......#.#.......#.........#...# +#.#####.#####.#.#.###.#.#.#.#.#.###.###.#.#####.#.#.###.#.###.###.#.#####.#.#.###.#.#####.###.#.#.#####.#.###.#.#####.#.#####.#.#####.#####.# +#...#...#.....#...#...#.#.#.#.#...#.#...#.....#.#.#...#.#...#.#...#.........#...#.#.......#...#.#.....#.#.....#.......#...#.#.......#.#.....# +#.###.#.#.#########.###.#.#.#.###.#.#.#######.#.#.#.#.#####.###.###############.#.#########.###.#.###.###.###########.###.#.###.###.#.#.##### +#.#...#.#.#.#.....#...#...#.#.....#.#.#...#.#.#.#.#.#...#.......#.....#.......#.#.....#...#.....#...#...#...........#...#.#.......#.#.#.....# +#.#.#####.#.#.#.#####.#####.#######.#.#.#.#.#.###.#.###.#.###.###.###.#######.#.#####.#.#.#.#####.#.###.#########.#####.#.#####.#.#.#.#.###.# +#.#.......#...#.#.....#...#.........#.......#...#...#...#...#.#...#...#.......#.#...#.#.#.......#.#.......#.......#.....#.......#...........# +#.#############.#.#####.#.###.#############.###.#####.###.#.#.#.###.###.#####.#.###.#.#.#####.#.#.###.###.#####.###.#######.###.#####.#.#.#.# +#.#...........#.#.......#.................#...#.....#.....#.#.#.#.#.#...#.....#.#...#.#...#.#.#.........#.....#...#.#.....#.....#...#...#...# +#.###.#######.#.###########.###.#######.#####.#####.###.#.#.#.#.#.#.#.#########.#.#.#.###.#.#.#####.###.#####.#####.#.###.#.#####.#.#.#.##### +#.....#.....#...#.........#...#...#...#...........#.....#.#...#.#...#.............#.#.......#.#...#...#.#...#.......#...#...#...#.#.#.#.....# +###########.###.#.#######.###.###.###.###########.#######.#####.#.#################.#.#.#.#.#.#.###.#.#.#.#############.#####.#.#.#.#.#.###.# +#.............#.#.#...#.#.#.#.#.#...#.#.....#...........#.#.....#.#.............#...#.#...#.#.....#.#.#.#.#.....#.....#.#.#...#...#.#.#...#.# +#.###.#######.#.#.#.#.#.#.#.#.#.###.#.#.###.#.#########.#.###.###.###.#.###.###.#.###.###.#.#.#.#.#.#.#.#.#.###.#.#.###.#.#.#######.#.#.#.#.# +#.#...#...#.#.#.#...#.#.....#.#.......#...#.#...........#...#.#.#.#...#.#...#.#.#...#.#...#.#.#.....#.#...#...#.#.#.......#.#...#.#.......#.# +#.#.#.#.#.#.#.#.#####.#####.#.#####.#.###.#.###.#######.###.#.#.#.#.###.#.#.#.#.#####.#.###.#.#.#####.#.#.###.#.#.#.#####.#.#.#.#.###.#.#.#.# +#...#.#.#...........#.....#.#.......#.#.#.#.....#.....#...#...#...#.#...#.#.#.........#.#...#.#...#...#.#...........#...#.#...#...#.....#.#.# +#.#.###.#.#########.#####.#.#######.#.#.#.#######.###.###.#####.###.#####.#.#######.#.#.###.#.#####.#.#.#####.#.#####.#.#########.#.#.#.###.# +#.#.......#...#...#.#.....#...#...#.#.#.#.#.......#...#.....#...#.........#.#.....#...#.#...........#.#.#...#.#.......#.#.....#.............# +#.#.#####.#.#.#.#.#.#.#####.#.#.###.#.#.#.###.#####.###.#####.###########.#.#.###.#.###.#.#######.#####.#.#.#.#.#.#####.#.###.#.###.#.#.#.### +#.#...#...#.#...#...#.#.#.......#...#...#.....#.#...#.#.......#.........#.#...#...#.#...#.........#...#...#.#.#.....#...#.#.#.#.#...#...#...# +###.#.#.###.#########.#.#.#######.###.#.#######.#.###.#########.#######.#.#####.###.#.#######.#####.#.#.###.#.###.###.#.#.#.#.#.#.###.###.#.# +#...#.#...#.#.........#.#...#.....#.#...#.....#...#.............#.....#.#.....#.#...#.......#...#...#.#.#...#.#...#...#.#.#.#...#.#.#.#.#.#.# +#.#.#.###.#.#.#.#######.#.#.#.#####.#.###.#.###.#####.###########.#.#.#.#######.#.#########.###.#.###.###.###.#.###.#####.#.#####.#.#.#.#.#.# +#...#.....#.#.#.#...#.....#.#...#...#.#...#...#...........#.........#.#...#.....#.......#...#.....#.......#...#...#...........#.............# +#.#.###.###.#.#.#.#.#.#.###.###.#.#.#.#.#####.###########.#.#.#.#########.#.#######.#####.###.#####.#############.#####.#####.#.###.#.#.#.#.# +#.#.#...#...#.#.#.#.#...#...#...#...#.#.#...#.#.........#...#.#.#.......#.#.....#...#.....#...#.....#...........#...#...#...#.#...#...#...#.# +#.#.###.#.#####.#.###.#.#.###.#####.#.#.###.#.#.###.###.#####.#.#.###.###.#.###.#####.#####.#.#.#####.#########.###.#.#.#.#.#####.#.###.#.#.# +#.#...#.#.#.....#.....#...#...#.....#.#.....#.#.#.#...#.#.....#.#.#.#.#...#...#.......#.....#.#.#.........#...#.....#.#.#.#...#...#...#.#...# +#.###.###.#.#########.#.###.#####.###.#####.#.#.#.#.#.#.#.#.#####.#.#.#.#.###.#########.###.#.###.#######.#.#.#######.#.#.###.#.#####.###.#.# +#.#.#.....#...........#.#...#...............#.....#...#.#.#.#...#...#...#.#...#.....#.......#.#...#.....#...#...#.#...#.#.#.#...#...#.....#.# +#.#.#######.#########.#.#.#.#.#.#########.#########.#.#.#.###.#.###.#####.#.###.#####.#.###.#.#.###.###.#######.#.#.###.#.#.#####.#.#####.#.# +#...#.....#...#.....#.#.#.....#...#...#.#.#.........#...#.#...#.....#.#...#.#...#.....#...#.#...#...#.#.#...#.....#...#...#.....#.#...#...#.# +###.#.###.#####.###.#.#.#.#.#.###.#.#.#.#.#.#########.###.#.#########.#.#.#.#.#.#.###.#.#.#.#####.###.#.#.#.#.#######.#####.###.#.#####.#.#.# +#.#...#.#.#.....#.#.#.#.#.#.....#...#.#...#...#...#.#.#...#.....#.......#.#.#.#.....#...#.#...#...#...#.#.#.....#.....#.#...#.....#.........# +#.#####.#.#.#####.#.#.###.#####.#####.#.#####.#.#.#.#.#.#######.###.#####.#.#######.#.###.###.#.###.###.#######.#.#####.#.#.#####.#.#####.### +#.....#...#.#...#.....#.......#.....#.#.#...#...#.#...#.....#.#...#.....#.#...#.....#.#.#.#...#...#.....#.....#.#...#.#...#...#...#.#...#...# +#.#.###.###.###.#.#####.###.#######.#.#.#.#.#####.#.#####.#.#.###.#######.###.#.###.#.#.#.#####.#.#.#####.###.#####.#.#.#####.#.###.#.#.#.### +#.#...#.....#...#.#...#...#.#.....#.#.#...#.....#.#.#...#.#.#...#.#.....#...#.#.#...#.#.#.....#.#.#.......#...#.....#.#...#.#.#...#...#.#...# +#.###.#####.#.###.#.#.###.###.###.#.#.#########.#.#.#.#.#.#.#.###.#.###.#.#.#.#.#.###.#.#####.###.#####.#####.#.#####.###.#.#.###.#####.#.#.# +#.#.......#...#...#.#...#.#...#.#...#...#.....#...#...#...#...#...#.#.#...#.#...#.....#.....#...#.#.....#...#...#.......#...#.#...#...#.....# +###.#.#####.#.#.###.###.#.#.###.#######.###.#.###.#########.###.###.#.#.#.#.#######.###.###.###.#.###.###.#.#####.#####.###.#.#.###.#.###.#.# +#.............#.#.#.#.....#...#.......#...#.#...#.#...#...#.#...#...#.....#.......#.....#.#.#.#.#...#.#...#.#...#.........#.#.#.....#.#.....# +#.###.#.#.#.#.#.#.#.#########.#.#####.###.#.###.#.#.#.#.#.###.###.#######.#######.#.#.###.#.#.#.###.#.#.###.#.#.#########.#.#.#######.#.#.#.# +#.#.#.#.#...#...#.#.........#...#.....#...#...#...#.#...#.....#.#.......#...#.....#.#.#...#.#.#.#...#.#...#...#...#.....#...#...#...#.#.#...# +#.#.#.#.###.#####.#.#.###.#.#####.#####.###.#.#####.###########.#.#####.#.#.#.#######.#.#.#.#.#.#.###.###.#######.#.###.#######.#.#.###.###.# +#.#...#...........#.....#.#.....#...#.#.#.......#...#...#...#.....#...#.#.#.#.........#.#.#.#.#.#...#.#.....#...#.....#.......#.#.#.#...#...# +#.###########.#########.#.#.###.###.#.#.#########.###.#.#.#.#.###.###.#.###.#.#####.###.#.#.#.#.#.#.###.#####.#.#####.#######.#.#.#.#.###.#.# +#.....#.....#.#.......#.#...#...#...#.#...............#.#.#.#.#.......#...#...#...#.....#...#.#.#.#.....#...#.#...............#...#.#.#...#.# +#.###.#.#.###.#.#####.#.#####.###.###.#############.#.###.#.#.#.#.#.#.###.#####.#.###.#######.#.###.#####.#.#.###############.#.###.#.#.###.# +#...#...#.....#...#...#.....#.#...#...#...........#.#.....#.#.#.#...#.#.#.....#.#...#.........#...#...#...#.#...#.......#.....#.#...#...#...# +###.#####.#####.#.#.#########.#.#####.#.#########.#.#######.#.###.###.#.###.#.#.###.#############.#####.###.###.#.#####.#.#.###.#.#######.#.# +#...#.......#.....#.#.......#.#.#.....#.#.#...#...#.#.....#...#...#...#.....#...#.#...#.........#.#.....#.#...#.#...#.#...#.....#.........#.# +#.###.###.#.#.###.#.#.#####.#.#.###.###.#.#.#.#.###.#.#########.###.###.#########.###.#.#######.#.#.#####.###.#.###.#.###########.#.#######.# +#.#.......#.#.....#...#.......#...#...#.#.#.#.#.....#.#.......#.#.#.#...........#...#...#.......#...#.......#...#.#...#...............#.....# +#.#######.#.#####.#####.#####.###.###.#.#.#.#.#######.#.###.#.#.#.#.#######.###.#.#.#####.#.#######.#.###.#.#.###.###.#.#########.#.#.#.###.# +#.........#.#...#...#.......#...#...#.#.#...#.............#.#...#.#.#.......#...#.#.....#.#.........................................#...#.#.# +#########.#.#.#.###.#########.#.###.#.#.###.###############.#####.#.#.#########.#.###.#.#.###.#####.#####.###.#.#.###.#.###.#.#.#.#.#####.#.# +#.........#.#.#...#.........#.#.....#.#...#.......................#.#.........#.#...#.#.#...#.#...........#.....#.#...#...#...#.#.....#.....# +#.#######.#.###.#.#########.#.#######.###.###########.###.###.#.###.#.#######.#.#.###.#.###.#.#.#.#.#.#.#.###.#####.###.#######.#.#.#.#.###.# +#.......#.#.....#.#.........#.........................#.#.....#...#.#.....#...#.#.#...#...#...#.#...#.#.#.........#...#.#.......#...#.#.#...# +#.#####.#.#######.#.#.###.#.###.#.#####.#####.#.#######.#########.#.#.#####.###.#.#.#####.#########.#.#.#.###.###.###.#.#.#######.#.###.###.# +#.....#.#...#.....#.#.#...#...#.#.....#.....#...#.................#.#.#...#...#.#.#.#...#...........#.#.#...#...#.#...#.....................# +#####.#.#.#.#.#####.#.#.#.#.#.#.#####.#####.###########.#.#.#.#####.###.#.#.#.#.###.#.#.#######.###.#.#.#####.#.#.#.###.#.#.###.#.#.#.#####.# +#...#.#...#.#.....#.#.#.#.#.#.#...#.#.#...#.#.......#...#.#.#.#.....#...#.#.#.#...#.#.......#...#.....#.....#.#.............#.#.....#.....#.# +#.###.###.#######.#.#.#.#.#.#####.#.#.###.#.#.#.###.#.###.#.#.#.#####.###.###.###.#.#######.#.###.#.#######.#.#####.#.###.#.#.###.#.#####.#.# +#.....#.#.........#.#...#.#.....#.#.#.........................#...#...#...#...#...#.................#.....#...#...#.#.....#...#...........#.# +#.#####.#############.###.#####.#.#.#####.#.#.###.###############.###.#.###.###.###.#.#########.###.#.###.###.#.#.#.#.#######.#.#.#.#######.# +#S............................#...#.......#.....#.....................#.....#.......#.............#.....#.......#...........................# +############################################################################################################################################# \ No newline at end of file From e49fe2a5b3f233cf9f75b5239af7be6204500945 Mon Sep 17 00:00:00 2001 From: Thomas Plumpton Date: Mon, 16 Dec 2024 18:22:21 +0000 Subject: [PATCH 8/9] Math | DijkstrasAlgorithm now exposes an all paths variant for find all variations of the shortest path --- .../libs/algorithm/DijkstrasAlgorithm.kt | 135 ++++++++++++++++++ ...thmKtTest.kt => DijkstrasAlgorithmTest.kt} | 58 +++++++- 2 files changed, 192 insertions(+), 1 deletion(-) rename library/src/test/kotlin/io/github/tomplum/libs/algorithm/{DijkstrasAlgorithmKtTest.kt => DijkstrasAlgorithmTest.kt} (79%) diff --git a/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt b/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt index fb0b567..22f9f55 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt @@ -83,4 +83,139 @@ fun dijkstraShortestPath( val message = "Could not find a path from the given starting positions to the node indicated by the terminates predicate." throw IllegalStateException(message) +} + +data class DijkstraAllPaths( + /** + * The shortest distance from the starting + * position to the end position. + */ + val shortestDistance: Int, + + + /** + * A list of all the nodes from all + * variations of the shortest path. + */ + val shortestPaths: List> +) + +/** + * Calculates the shortest distance to all nodes in a weighted-graph from the given [startingPositions] + * and terminates based on the given [terminates] predicate. + * + * All shortest paths (that is paths that all share the same, shortest distance) are tracked. + * + * If you do not need all the shortest paths, but simply any of them for the distance, + * then use [dijkstraShortestPath] instead as it will be more performant. + * + * @param startingPositions A collection of nodes to start from. + * @param evaluateAdjacency A function that is passed an instance of the current node and should return a collection of all adjacent nodes or "neighbours" that should be evaluated as part of the pathfinding. + * @param processNode An optional function that is applied after the adjacency evaluation for each neighbouring node. Can be used to mutate the state of the node before evaluation. + * @param terminates A boolean predicate used to determine when the destination node has been reached. When it evaluates as true, the algorithm is terminated and the shortest path for the current node is returned. + * + * @returns The shortest path from the starting nodes to the node that produces true when passed into the [terminates] predicate. + */ +fun dijkstraAllShortestPaths( + startingPositions: Collection, + evaluateAdjacency: (currentNode: Node) -> Collection>, + terminates: (currentNode: Node) -> Boolean, + processNode: (currentNode: Node, adjacentNode: Node) -> Node = { _, adjacentNode -> adjacentNode } +): DijkstraAllPaths { + // A map of nodes and the shortest distance from the starting positions to it + val distance = mutableMapOf() + + // A map to track predecessors for each node (to reconstruct paths) + val predecessors = mutableMapOf>() + + // Unsettled nodes prioritized by their distance + val next = PriorityQueue>() + + startingPositions.forEach { startingPosition -> + next.offer(Node(startingPosition, 0)) + distance[startingPosition] = 0 + } + + var shortestDistance: Int? = null + + while (next.isNotEmpty()) { + // Take the next node from the queue + val currentNode = next.poll() + + // If we have determined the shortest distance and this node's distance exceeds it, stop processing + if (shortestDistance != null && currentNode.distance > shortestDistance) { + break + } + + // If the termination condition is met, record the shortest distance + if (terminates(currentNode)) { + if (shortestDistance == null) { + shortestDistance = currentNode.distance + } + } + + // Find all adjacent nodes + evaluateAdjacency(currentNode).forEach { adjacentNode -> + // Perform additional processing on the adjacent node before evaluation + val evaluationNode = processNode(currentNode, adjacentNode) + + // The new shortest path to the adjacent node + val updatedDistance = currentNode.distance + evaluationNode.distance + + // If the distance of this new path is shorter than or equal to the known shortest distance + if (updatedDistance <= distance.getOrDefault(evaluationNode.value, Int.MAX_VALUE)) { + // Update distance + distance[evaluationNode.value] = updatedDistance + + // Update predecessors + predecessors.computeIfAbsent(evaluationNode.value) { mutableSetOf() }.add(currentNode.value) + + // Add the adjacent node to the queue + next.add(Node(evaluationNode.value, updatedDistance)) + } + } + } + + // Now we need to collect all nodes that are part of the shortest paths + val shortestPathNodes = mutableSetOf() + + // Helper function to collect all nodes along the shortest paths + fun collectShortestPathNodes(node: N) { + if (shortestPathNodes.contains(node)) { + return // Already visited + } + + shortestPathNodes.add(node) + + predecessors[node]?.forEach { parent -> + collectShortestPathNodes(parent) + } + } + + // Recursive helper to backtrack and collect all paths + fun collectPaths(node: N, currentPath: Set): List> { + val newPath = setOf(node) + currentPath + val currentNodePredecessors = predecessors[node] + + return if (currentNodePredecessors.isNullOrEmpty()) { + // No more predecessors, this is the start of a path + listOf(newPath) + } else { + currentNodePredecessors.flatMap { predecessorNode -> + collectPaths(predecessorNode, newPath) + } + } + } + + // Collect all nodes for the shortest path, starting from the end nodes + // This step ensures we get all nodes in all shortest paths, not just the first one + val endNodes = distance.filter { terminates(Node(it.key, it.value)) }.keys + endNodes.forEach { collectShortestPathNodes(it) } + + val shortestPaths = endNodes.flatMap { node -> collectPaths(node, emptySet()) } + + return DijkstraAllPaths( + shortestDistance = shortestDistance ?: 0, + shortestPaths = shortestPaths + ) } \ No newline at end of file diff --git a/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmKtTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmTest.kt similarity index 79% rename from library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmKtTest.kt rename to library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmTest.kt index 85b7c51..22604ce 100644 --- a/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmKtTest.kt +++ b/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmTest.kt @@ -10,7 +10,7 @@ import io.github.tomplum.libs.math.point.Point2D import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -class DijkstrasAlgorithmKtTest { +class DijkstrasAlgorithmTest { private data class CrucibleLocation( val position: Point2D, val direction: Direction, @@ -207,6 +207,41 @@ class DijkstrasAlgorithmKtTest { } ) } + + fun countBestPathTiles(): Int { + val (startingPosition) = findTile { it.isReindeerStart() }!! + val startingDirection = Direction.RIGHT + + return dijkstraAllShortestPaths( + startingPositions = listOf(startingPosition to startingDirection), + evaluateAdjacency = { currentNode -> + val (currentPosition, direction) = currentNode.value + val adjacentNodes = mutableListOf>>() + + val nextPosition = currentPosition.shift(direction) + if (getTile(nextPosition, MazeTile('?')).isTraversable()) { + adjacentNodes.add(Node(nextPosition to direction, 1)) + } + + val rotations = directions + .filterNot { currentDirection -> + val isSameDirection = currentDirection == direction + val isBacktracking = currentDirection.isOpposite(direction) + isSameDirection || isBacktracking + } + .map { currentDirection -> + Node(currentPosition to currentDirection, 1000) + } + + adjacentNodes.addAll(rotations) + + adjacentNodes + }, + terminates = { currentNode -> + getTile(currentNode.value.first, MazeTile('?')).isEnd() + } + ).shortestPaths.flatten().map { it.first }.toSet().count() + } } @Nested @@ -233,5 +268,26 @@ class DijkstrasAlgorithmKtTest { val reindeerMaze = ReindeerMaze(data.value) assertThat(reindeerMaze.calculateLowestPossibleScore()).isEqualTo(65436) } + + @Test + fun partTwoExampleOne() { + val data = TestInputReader.read("$exampleInputLocation/example-1.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.countBestPathTiles()).isEqualTo(45) + } + + @Test + fun partTwoExampleTwo() { + val data = TestInputReader.read("$exampleInputLocation/example-2.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.countBestPathTiles()).isEqualTo(64) + } + + @Test + fun partTwoSolution() { + val data = TestInputReader.read("$exampleInputLocation/input.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.countBestPathTiles()).isEqualTo(489) + } } } \ No newline at end of file From 32052b9ec6f84403e179ff49014181683af3184b Mon Sep 17 00:00:00 2001 From: Thomas Plumpton Date: Mon, 16 Dec 2024 18:23:22 +0000 Subject: [PATCH 9/9] Release | Bumped release version to 2.6.0 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index b4c01f1..1c6c5bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ subprojects { apply(plugin = "maven-publish") ext { - set("releaseVersion", "2.5.0") + set("releaseVersion", "2.6.0") } repositories {