|
| 1 | +package day20 |
| 2 | + |
| 3 | +import scala.annotation.tailrec |
| 4 | +import scala.collection.immutable.Range.Inclusive |
| 5 | + |
| 6 | +import locations.Directory.currentDir |
| 7 | +import inputs.Input.loadFileSync |
| 8 | + |
| 9 | +@main def part1: Unit = |
| 10 | + println(s"The solution is ${part1(loadInput())}") |
| 11 | + |
| 12 | +@main def part2: Unit = |
| 13 | + println(s"The solution is ${part2(loadInput())}") |
| 14 | + |
| 15 | +def loadInput(): String = loadFileSync(s"$currentDir/../input/day20") |
| 16 | + |
| 17 | +extension (x: Int) inline def ±(y: Int) = x - y to x + y |
| 18 | +extension (x: Inclusive) |
| 19 | + inline def &(y: Inclusive) = (x.start max y.start) to (x.end min y.end) |
| 20 | + |
| 21 | +opaque type Pos = (Int, Int) |
| 22 | + |
| 23 | +object Pos: |
| 24 | + val up: Pos = (0, -1) |
| 25 | + val down: Pos = (0, 1) |
| 26 | + val left: Pos = (-1, 0) |
| 27 | + val right: Pos = (1, 0) |
| 28 | + val zero: Pos = (0, 0) |
| 29 | + def apply(x: Int, y: Int): Pos = (x, y) |
| 30 | + |
| 31 | + extension (p: Pos) |
| 32 | + inline def x = p._1 |
| 33 | + inline def y = p._2 |
| 34 | + inline def neighbors: List[Pos] = |
| 35 | + List(p + up, p + right, p + down, p + left) |
| 36 | + inline def +(q: Pos): Pos = (p.x + q.x, p.y + q.y) |
| 37 | + inline infix def taxiDist(q: Pos) = (p.x - q.x).abs + (p.y - q.y).abs |
| 38 | + |
| 39 | +case class Rect(x: Inclusive, y: Inclusive): |
| 40 | + inline def &(that: Rect) = Rect(x & that.x, y & that.y) |
| 41 | + |
| 42 | + def iterator: Iterator[Pos] = for |
| 43 | + y <- y.iterator |
| 44 | + x <- x.iterator |
| 45 | + yield Pos(x, y) |
| 46 | + |
| 47 | +object Track: |
| 48 | + def parse(input: String) = |
| 49 | + val lines = input.trim.split('\n') |
| 50 | + val bounds = Rect(0 to lines.head.size - 1, 0 to lines.size - 1) |
| 51 | + val track = Track(Pos.zero, Pos.zero, Set.empty, bounds) |
| 52 | + bounds.iterator.foldLeft(track) { (track, p) => |
| 53 | + lines(p.y)(p.x) match |
| 54 | + case 'S' => track.copy(start = p) |
| 55 | + case 'E' => track.copy(end = p) |
| 56 | + case '#' => track.copy(walls = track.walls + p) |
| 57 | + case _ => track |
| 58 | + } |
| 59 | + |
| 60 | +case class Track(start: Pos, end: Pos, walls: Set[Pos], bounds: Rect): |
| 61 | + lazy val path: Vector[Pos] = |
| 62 | + inline def canMove(prev: List[Pos])(p: Pos) = |
| 63 | + !walls.contains(p) && Some(p) != prev.headOption |
| 64 | + |
| 65 | + @tailrec def go(xs: List[Pos]): List[Pos] = xs match |
| 66 | + case Nil => Nil |
| 67 | + case p :: _ if p == end => xs |
| 68 | + case p :: ys => go(p.neighbors.filter(canMove(ys)) ++ xs) |
| 69 | + |
| 70 | + go(List(start)).reverseIterator.toVector |
| 71 | + |
| 72 | + lazy val zipped = path.zipWithIndex |
| 73 | + lazy val pathMap = zipped.toMap |
| 74 | + |
| 75 | + def cheatedPaths(maxDist: Int) = |
| 76 | + def radius(p: Pos) = |
| 77 | + (Rect(p.x ± maxDist, p.y ± maxDist) & bounds).iterator |
| 78 | + .filter(p.taxiDist(_) <= maxDist) |
| 79 | + |
| 80 | + zipped.map { (p, i) => |
| 81 | + radius(p) |
| 82 | + .flatMap(pathMap.get) |
| 83 | + .map { j => (j - i) - (p taxiDist path(j)) } |
| 84 | + .count(_ >= 100) |
| 85 | + }.sum |
| 86 | + |
| 87 | +def part1(input: String): Int = Track.parse(input).cheatedPaths(2) |
| 88 | +def part2(input: String): Int = Track.parse(input).cheatedPaths(20) |
0 commit comments