From ea33062cae21063951c38313f733bcb86d848312 Mon Sep 17 00:00:00 2001 From: Simmo Saan Date: Mon, 23 Dec 2024 17:37:23 +0200 Subject: [PATCH 1/3] Add all paths Dijkstra solution to 2024 day 16 part 2 --- .../eu/sim642/adventofcode2024/Day16.scala | 62 +++++++++++++------ .../adventofcodelib/graph/Dijkstra.scala | 54 ++++++++++++++++ .../graph/GraphTraversal.scala | 4 ++ .../sim642/adventofcode2024/Day16Test.scala | 46 +++++++++----- 4 files changed, 132 insertions(+), 34 deletions(-) diff --git a/src/main/scala/eu/sim642/adventofcode2024/Day16.scala b/src/main/scala/eu/sim642/adventofcode2024/Day16.scala index 57170848..88f475ac 100644 --- a/src/main/scala/eu/sim642/adventofcode2024/Day16.scala +++ b/src/main/scala/eu/sim642/adventofcode2024/Day16.scala @@ -34,29 +34,50 @@ object Day16 { Dijkstra.search(graphSearch).target.get._2 } - def bestPathTiles(grid: Grid[Char]): Int = { - val forwardSearch = forwardGraphSearch(grid) - val forwardResult = Dijkstra.search(forwardSearch) - - val backwardTraversal = new GraphTraversal[Reindeer] with UnitNeighbors[Reindeer] { - override val startNode: Reindeer = forwardResult.target.get._1 // TODO: other orientations - - override def unitNeighbors(reindeer: Reindeer): IterableOnce[Reindeer] = { - val distance = forwardResult.distances(reindeer) - for { - (oldReindeer, step) <- Seq( - reindeer.copy(pos = reindeer.pos - reindeer.direction) -> 1, // backward steo - reindeer.copy(direction = reindeer.direction.left) -> 1000, - reindeer.copy(direction = reindeer.direction.right) -> 1000, - ) - if grid(oldReindeer.pos) != '#' - oldDistance <- forwardResult.distances.get(oldReindeer) - if oldDistance + step == distance // if step on shortest path - } yield oldReindeer + trait Part2Solution { + def bestPathTiles(grid: Grid[Char]): Int + } + + object BackwardNeighborsPart2Solution extends Part2Solution { + override def bestPathTiles(grid: Grid[Char]): Int = { + val forwardSearch = forwardGraphSearch(grid) + val forwardResult = Dijkstra.search(forwardSearch) + + val backwardTraversal = new GraphTraversal[Reindeer] with UnitNeighbors[Reindeer] { + override val startNode: Reindeer = forwardResult.target.get._1 // TODO: other orientations + + override def unitNeighbors(reindeer: Reindeer): IterableOnce[Reindeer] = { + val distance = forwardResult.distances(reindeer) + for { + (oldReindeer, step) <- Seq( + reindeer.copy(pos = reindeer.pos - reindeer.direction) -> 1, // backward step + reindeer.copy(direction = reindeer.direction.left) -> 1000, + reindeer.copy(direction = reindeer.direction.right) -> 1000, + ) + if grid(oldReindeer.pos) != '#' + oldDistance <- forwardResult.distances.get(oldReindeer) + if oldDistance + step == distance // if step on shortest path + } yield oldReindeer + } } + + BFS.traverse(backwardTraversal).nodes.map(_.pos).size } + } - BFS.traverse(backwardTraversal).nodes.map(_.pos).size + object AllPathsPart2Solution extends Part2Solution { + override def bestPathTiles(grid: Grid[Char]): Int = { + val forwardSearch = forwardGraphSearch(grid) + val forwardResult = Dijkstra.searchAllPaths(forwardSearch) + + val backwardTraversal = new GraphTraversal[Reindeer] with UnitNeighbors[Reindeer] { + override val startNode: Reindeer = forwardResult.target.get._1 // TODO: other orientations + + override def unitNeighbors(reindeer: Reindeer): IterableOnce[Reindeer] = forwardResult.allPrevNodes.getOrElse(reindeer, Set.empty) + } + + BFS.traverse(backwardTraversal).nodes.map(_.pos).size + } } def parseGrid(input: String): Grid[Char] = input.linesIterator.map(_.toVector).toVector @@ -64,6 +85,7 @@ object Day16 { lazy val input: String = scala.io.Source.fromInputStream(getClass.getResourceAsStream("day16.txt")).mkString.trim def main(args: Array[String]): Unit = { + import AllPathsPart2Solution._ println(lowestScore(parseGrid(input))) println(bestPathTiles(parseGrid(input))) } diff --git a/src/main/scala/eu/sim642/adventofcodelib/graph/Dijkstra.scala b/src/main/scala/eu/sim642/adventofcodelib/graph/Dijkstra.scala index 5503b2fe..c33db4e9 100644 --- a/src/main/scala/eu/sim642/adventofcodelib/graph/Dijkstra.scala +++ b/src/main/scala/eu/sim642/adventofcodelib/graph/Dijkstra.scala @@ -79,4 +79,58 @@ object Dijkstra { override def target: Option[(A, Int)] = None } } + + // copied from search, modified like BFS.searchPaths + def searchAllPaths[A](graphSearch: GraphSearch[A]): Distances[A] & AllPaths[A] & Target[A] = { + val visitedDistance: mutable.Map[A, Int] = mutable.Map.empty + val prevNode: mutable.Map[A, mutable.Set[A]] = mutable.Map.empty + val toVisit: mutable.PriorityQueue[(Int, Option[A], A)] = mutable.PriorityQueue.empty(Ordering.by(-_._1)) + + def enqueue(oldNode: Option[A], node: A, dist: Int): Unit = { + toVisit.enqueue((dist, oldNode, node)) + } + + enqueue(None, graphSearch.startNode, 0) + + while (toVisit.nonEmpty) { + val (dist, oldNode, node) = toVisit.dequeue() + if (!visitedDistance.contains(node)) { + visitedDistance(node) = dist + for (oldNode <- oldNode) + prevNode(node) = mutable.Set(oldNode) + + if (graphSearch.isTargetNode(node, dist)) { + return new Distances[A] with AllPaths[A] with Target[A] { + override def distances: collection.Map[A, Int] = visitedDistance + + override def allPrevNodes: collection.Map[A, collection.Set[A]] = prevNode + + override def target: Option[(A, Int)] = Some(node -> dist) + } + } + + + def goNeighbor(newNode: A, distDelta: Int): Unit = { + if (!visitedDistance.contains(newNode)) { // avoids some unnecessary queue duplication but not all + val newDist = dist + distDelta + enqueue(Some(node), newNode, newDist) + } + } + + graphSearch.neighbors(node).iterator.foreach(goNeighbor.tupled) + } + else { // visitedDistance.contains(node) + for (oldNode <- oldNode if visitedDistance(node) == dist) + prevNode(node) += oldNode + } + } + + new Distances[A] with AllPaths[A] with Target[A] { + override def distances: collection.Map[A, Int] = visitedDistance + + override def allPrevNodes: collection.Map[A, collection.Set[A]] = prevNode + + override def target: Option[(A, Int)] = None + } + } } diff --git a/src/main/scala/eu/sim642/adventofcodelib/graph/GraphTraversal.scala b/src/main/scala/eu/sim642/adventofcodelib/graph/GraphTraversal.scala index 6798d97f..715f9107 100644 --- a/src/main/scala/eu/sim642/adventofcodelib/graph/GraphTraversal.scala +++ b/src/main/scala/eu/sim642/adventofcodelib/graph/GraphTraversal.scala @@ -29,6 +29,10 @@ trait Paths[A] { ) } +trait AllPaths[A] { // does not extend Paths, because prevNodes is Map, not function + def allPrevNodes: collection.Map[A, collection.Set[A]] +} + trait Order[A] { def nodeOrder: collection.Seq[A] } diff --git a/src/test/scala/eu/sim642/adventofcode2024/Day16Test.scala b/src/test/scala/eu/sim642/adventofcode2024/Day16Test.scala index 190258eb..837b6380 100644 --- a/src/test/scala/eu/sim642/adventofcode2024/Day16Test.scala +++ b/src/test/scala/eu/sim642/adventofcode2024/Day16Test.scala @@ -1,9 +1,17 @@ package eu.sim642.adventofcode2024 -import Day16._ +import Day16.* +import Day16Test.* +import org.scalatest.Suites import org.scalatest.funsuite.AnyFunSuite -class Day16Test extends AnyFunSuite { +class Day16Test extends Suites( + new Part1Test, + new BackwardNeighborsPart2SolutionTest, + new AllPathsPart2SolutionTest, +) + +object Day16Test { val exampleInput = """############### @@ -41,21 +49,31 @@ class Day16Test extends AnyFunSuite { |#S#.............# |#################""".stripMargin - test("Part 1 examples") { - assert(lowestScore(parseGrid(exampleInput)) == 7036) - assert(lowestScore(parseGrid(exampleInput2)) == 11048) - } + class Part1Test extends AnyFunSuite { + test("Part 1 examples") { + assert(lowestScore(parseGrid(exampleInput)) == 7036) + assert(lowestScore(parseGrid(exampleInput2)) == 11048) + } - test("Part 1 input answer") { - assert(lowestScore(parseGrid(input)) == 73404) + test("Part 1 input answer") { + assert(lowestScore(parseGrid(input)) == 73404) + } } - test("Part 2 examples") { - assert(bestPathTiles(parseGrid(exampleInput)) == 45) - assert(bestPathTiles(parseGrid(exampleInput2)) == 64) - } + class Part2SolutionTest(part2Solution: Part2Solution) extends AnyFunSuite { + import part2Solution._ - test("Part 2 input answer") { - assert(bestPathTiles(parseGrid(input)) == 449) + test("Part 2 examples") { + assert(bestPathTiles(parseGrid(exampleInput)) == 45) + assert(bestPathTiles(parseGrid(exampleInput2)) == 64) + } + + test("Part 2 input answer") { + assert(bestPathTiles(parseGrid(input)) == 449) + } } + + class BackwardNeighborsPart2SolutionTest extends Part2SolutionTest(BackwardNeighborsPart2Solution) + + class AllPathsPart2SolutionTest extends Part2SolutionTest(AllPathsPart2Solution) } From ad4578ff3e36f317963f34c3e2bc135794a64428 Mon Sep 17 00:00:00 2001 From: Simmo Saan Date: Mon, 23 Dec 2024 17:43:56 +0200 Subject: [PATCH 2/3] Add 2024 day 16 part 2 test with multi-predecessor target --- .../scala/eu/sim642/adventofcode2024/Day16Test.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/scala/eu/sim642/adventofcode2024/Day16Test.scala b/src/test/scala/eu/sim642/adventofcode2024/Day16Test.scala index 837b6380..96573329 100644 --- a/src/test/scala/eu/sim642/adventofcode2024/Day16Test.scala +++ b/src/test/scala/eu/sim642/adventofcode2024/Day16Test.scala @@ -68,6 +68,16 @@ object Day16Test { assert(bestPathTiles(parseGrid(exampleInput2)) == 64) } + test("Part 2 target with multiple predecessors") { + assert(bestPathTiles(parseGrid( + """##### + |#...# + |#S#E# + |#...# + |##### + |""".stripMargin)) == 8) + } + test("Part 2 input answer") { assert(bestPathTiles(parseGrid(input)) == 449) } From fcd5795fb0e5a884ed67da92b103e7f98ad503f1 Mon Sep 17 00:00:00 2001 From: Simmo Saan Date: Mon, 23 Dec 2024 23:29:42 +0200 Subject: [PATCH 3/3] Try BFS with multiple start nodes --- .../scala/eu/sim642/adventofcode2024/Day16.scala | 4 ++-- .../scala/eu/sim642/adventofcodelib/graph/BFS.scala | 4 ++-- .../sim642/adventofcodelib/graph/GraphSearch.scala | 4 +++- .../adventofcodelib/graph/GraphTraversal.scala | 12 ++++++++++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/main/scala/eu/sim642/adventofcode2024/Day16.scala b/src/main/scala/eu/sim642/adventofcode2024/Day16.scala index 88f475ac..fef5a553 100644 --- a/src/main/scala/eu/sim642/adventofcode2024/Day16.scala +++ b/src/main/scala/eu/sim642/adventofcode2024/Day16.scala @@ -43,8 +43,8 @@ object Day16 { val forwardSearch = forwardGraphSearch(grid) val forwardResult = Dijkstra.search(forwardSearch) - val backwardTraversal = new GraphTraversal[Reindeer] with UnitNeighbors[Reindeer] { - override val startNode: Reindeer = forwardResult.target.get._1 // TODO: other orientations + val backwardTraversal = new GraphTraversal0[Reindeer] with UnitNeighbors[Reindeer] { + override def startNodes: IterableOnce[Reindeer] = Pos.axisOffsets.map(d => forwardResult.target.get._1.copy(direction = d)).filter(forwardResult.nodes.contains) override def unitNeighbors(reindeer: Reindeer): IterableOnce[Reindeer] = { val distance = forwardResult.distances(reindeer) diff --git a/src/main/scala/eu/sim642/adventofcodelib/graph/BFS.scala b/src/main/scala/eu/sim642/adventofcodelib/graph/BFS.scala index 72bf61fd..fa991851 100644 --- a/src/main/scala/eu/sim642/adventofcodelib/graph/BFS.scala +++ b/src/main/scala/eu/sim642/adventofcodelib/graph/BFS.scala @@ -7,7 +7,7 @@ object BFS { // TODO: reduce duplication without impacting performance // copied from Dijkstra - def traverse[A](graphTraversal: GraphTraversal[A] & UnitNeighbors[A]): Distances[A] = { + def traverse[A](graphTraversal: GraphTraversal0[A] & UnitNeighbors[A]): Distances[A] = { val visitedDistance: mutable.Map[A, Int] = mutable.Map.empty val toVisit: mutable.Queue[(Int, A)] = mutable.Queue.empty @@ -15,7 +15,7 @@ object BFS { toVisit.enqueue((dist, node)) } - enqueue(graphTraversal.startNode, 0) + graphTraversal.startNodes.iterator.foreach(enqueue(_, 0)) while (toVisit.nonEmpty) { val (dist, node) = toVisit.dequeue() diff --git a/src/main/scala/eu/sim642/adventofcodelib/graph/GraphSearch.scala b/src/main/scala/eu/sim642/adventofcodelib/graph/GraphSearch.scala index 46b28ffb..7e561adf 100644 --- a/src/main/scala/eu/sim642/adventofcodelib/graph/GraphSearch.scala +++ b/src/main/scala/eu/sim642/adventofcodelib/graph/GraphSearch.scala @@ -1,10 +1,12 @@ package eu.sim642.adventofcodelib.graph -trait GraphSearch[A] extends GraphTraversal[A] { +trait GraphSearch0[A] extends GraphTraversal0[A] { //def isTargetNode(node: A): Boolean def isTargetNode(node: A, dist: Int): Boolean // TODO: does dist-based target make sense for A*? } +trait GraphSearch[A] extends GraphTraversal[A], GraphSearch0[A] + trait TargetNode[A] { this: GraphSearch[A] => val targetNode: A diff --git a/src/main/scala/eu/sim642/adventofcodelib/graph/GraphTraversal.scala b/src/main/scala/eu/sim642/adventofcodelib/graph/GraphTraversal.scala index 715f9107..e803ff7d 100644 --- a/src/main/scala/eu/sim642/adventofcodelib/graph/GraphTraversal.scala +++ b/src/main/scala/eu/sim642/adventofcodelib/graph/GraphTraversal.scala @@ -2,12 +2,20 @@ package eu.sim642.adventofcodelib.graph import eu.sim642.adventofcodelib.LazyListImplicits._ -trait GraphTraversal[A] { +trait GraphTraversal0[A] { + def startNodes: IterableOnce[A] // TODO: should be val? + def neighbors(node: A): IterableOnce[(A, Int)] +} + +trait GraphTraversal[A] extends GraphTraversal0[A] { val startNode: A + + override def startNodes: IterableOnce[A] = Iterator.single(startNode) + def neighbors(node: A): IterableOnce[(A, Int)] } -trait UnitNeighbors[A] { this: GraphTraversal[A] => +trait UnitNeighbors[A] { this: GraphTraversal0[A] => def unitNeighbors(node: A): IterableOnce[A] override final def neighbors(node: A): IterableOnce[(A, Int)] = unitNeighbors(node).iterator.map(_ -> 1)