Skip to content

Commit

Permalink
✨ day 20 part 2
Browse files Browse the repository at this point in the history
  • Loading branch information
twentylemon committed Dec 20, 2024
1 parent cac5f8a commit 5bc06fd
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 37 deletions.
37 changes: 35 additions & 2 deletions src/main/scala/org/lemon/advent/lib/graph/fill.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package org.lemon.advent.lib.graph

import scala.collection.mutable
import scala.math.Numeric.Implicits.infixNumericOps
import scala.math.Numeric.Implicits._
import scala.math.Ordering.Implicits._

/** Performs a breadth first fill of the graph from the starting node, returning
* the set of all reachable nodes.
Expand All @@ -19,6 +20,38 @@ def fill[N](adjacency: N => Seq[N], start: N): Set[N] =
queue ++= adjacency(node).filter(nodes.add)
nodes.toSet

/** Performs a breadth first fill of the graph from the starting node, returning
* the distance to all reachable nodes.
*
* @param adjacency function to return edges for a given node
* @param end the node to calculate distances to
* @return the set of reachable nodes from `start`
* @tparam N the node type
* @tparam D the distance type
*/
def distanceFrom[N, D: Numeric](adjacency: N => Seq[(N, D)], end: N): Map[N, D] =
val distances = mutable.Map(end -> summon[Numeric[D]].zero)
val queue = mutable.Queue(end)
while !queue.isEmpty do
val node = queue.dequeue
val dist = distances(node)
queue ++= adjacency(node)
.filter((neigh, d) => distances.get(neigh).forall(_ > dist + d))
.tapEach((neigh, d) => distances(neigh) = dist + d)
.map(_._1)
distances.toMap

/** Performs a breadth first fill of the graph from the starting node, returning
* the distance to all reachable nodes. The distance between each node is assumed to be one.
*
* @param adjacency function to return edges for a given node
* @param end the node to calculate distances to
* @return the set of reachable nodes from `start`
* @tparam N the node type
*/
def distanceFrom[N](adjacency: N => Seq[N], end: N): Map[N, Int] =
distanceFrom(unitAdjacency(adjacency), end)

/** Performs a breadth first fill of the graph from the starting node to the ending nodes, returning
* all possible paths between the two sets.
*
Expand All @@ -41,7 +74,7 @@ def allPaths[N, D: Numeric](adjacency: N => Seq[(N, D)], start: N, ends: N => Bo
paths.toSet

/** Performs a breadth first fill of the graph from the starting node to the ending nodes, returning
* all possible paths between the two sets.
* all possible paths between the two sets. The distance between each node is assumed to be one.
*
* Note that if `adjacency` can form loops, this function will not terminate.
*
Expand Down
56 changes: 26 additions & 30 deletions src/main/scala/org/lemon/advent/year2024/Day20.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,34 @@ private object Day20:

def parse = Coord.gridToMap

def findCheats(grid: Map[Coord, Char]): Seq[(Coord, Coord)] =
def isTrack(c: Char) = c == '.' || c == 'S' || c == 'E'
def get(coord: Coord) =
val c = grid.getOrElse(coord, ' ')
if isTrack(c) then '.' else c

grid.iterator
.filter((_, c) => isTrack(c))
.flatMap((coord, _) =>
for
d <- Direction.values
if get(coord.shift(d, 1)) == '#'
if get(coord.shift(d, 2)) == '.'
yield (coord, coord.shift(d, 2))
def adjacency(grid: Map[Coord, Char])(coord: Coord) =
coord.adjacent.filter(c => grid.getOrElse(c, '#') != '#')

def manhattans(coord: Coord, maxDist: Int) =
def manhattan(dist: Int) = (1 to dist).iterator
.flatMap(d =>
val inv = dist - d
Seq(
Coord(coord.x + d, coord.y + inv),
Coord(coord.x + inv, coord.y - d),
Coord(coord.x - d, coord.y - inv),
Coord(coord.x - inv, coord.y + d)
)
)
.toSeq.sorted
(2 to maxDist).iterator.flatMap(manhattan)

def adjacency(grid: Map[Coord, Char], cheat: (Coord, Coord))(coord: Coord) =
val neigh = coord.adjacent.filter(c => grid.getOrElse(c, '#') != '#').map((_, 1))
if coord == cheat._1 then (cheat._2, 2) +: neigh else neigh

def part1(input: String) =
val grid = parse(input)
val start = grid.find(_._2 == 'S').get._1
def goodCheats(grid: Map[Coord, Char], maxCheatDistance: Int) =
val end = grid.find(_._2 == 'E').get._1
val noCheating = pathFind(adjacency(grid, ((-1, -1), (-1, -1))), start, end).get.distance
val distances = distanceFrom(adjacency(grid), end)
distances.keysIterator
.flatMap(start =>
manhattans(start, maxCheatDistance)
.filter(distances.contains)
.map(to => distances(start) - distances(to) - start.manhattan(to))
)

val cheats = findCheats(grid)
cheats
.map(cheat => pathFind(adjacency(grid, cheat), start, end).get.distance)
.map(noCheating - _)
.count(_ >= 100)
def part1(input: String, minSaving: Int = 100) =
goodCheats(parse(input), 2).count(_ >= minSaving)

def part2(input: String) =
0
def part2(input: String, minSaving: Int = 100) =
goodCheats(parse(input), 20).count(_ >= minSaving)
23 changes: 18 additions & 5 deletions src/test/scala/org/lemon/advent/year2024/Day20Test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,32 @@ class Day20Test extends UnitTest:
|#.#.#.#.#.#.###
|#...#...#...###
|###############""".stripMargin
part1(in) shouldBe 0
part1(in, minSaving = 10) shouldBe 10
}

test("part 1") {
part1(read(file(2024)(20))) shouldBe 1289
}

test("part 2 example") {
val in = """|
|""".stripMargin
part2(in) shouldBe 0
val in = """|###############
|#...#...#.....#
|#.#.#.#.#.###.#
|#S#...#.#.#...#
|#######.#.#.###
|#######.#.#...#
|#######.#.###.#
|###..E#...#...#
|###.#######.###
|#...###...#...#
|#.#####.#.###.#
|#.#...#.#.#...#
|#.#.#.#.#.#.###
|#...#...#...###
|###############""".stripMargin
part2(in, minSaving = 50) shouldBe 285
}

test("part 2") {
part2(read(file(2024)(20))) shouldBe 0
part2(read(file(2024)(20))) shouldBe 982425
}

0 comments on commit 5bc06fd

Please sign in to comment.