package aoc.day21 import aoc._ import aoc.direction._ import scala.collection.mutable val board = lines .map(_.toArray) .toArray val startPoint = board.toSeq.zipWithIndex.findMap { (row, idx) => row.toSeq.zipWithIndex.findMap: (cell, col) => if cell == 'S' then Some((idx, col)) else None }.get def inside(x: Int, y: Int) = x >= 0 && x < board.length && y >= 0 && y < board(0).length def walkable(x: Int, y: Int) = inside(x, y) && board(x)(y) != '#' def getInf(x: Int, y: Int) = def mod(x: Int, m: Int): Int = if x < 0 then mod(x % m + m, m) else x % m board(mod(x, board.size))(mod(y, board(0).size)) def walkableInf(x: Int, y: Int) = getInf(x, y) != '#' // part 1 def walk(steps: Int, starting: Set[(Int, Int)]): Set[(Int, Int)] = if steps == 0 then starting else // println(s"$steps => ${starting.size}") val ns = starting .flatMap((a, b) => Dir.all.map(_(a, b))) .filter(walkableInf(_, _)) walk(steps - 1, ns) def part1 = val possible = walk(300, Set(startPoint)) println(possible.size) // part 2 object shortestPathsFrom: val cache = mutable.Map.empty[(Int, Int), Array[Array[Int]]] def apply(x: Int, y: Int) = cache.getOrElseUpdate( (x, y), { val mp = mutable.Map((x, y) -> 0) val queue = mutable.Queue((x, y)) while !queue.isEmpty do val (x, y) = queue.dequeue() val dist = mp((x, y)) Dir.all .map(_(x, y)) .filter(walkable(_, _)) .foreach { pt => if !mp.contains(pt) then mp += pt -> (dist + 1) queue += pt } val (a, b) = mp.toMap.partition((_, dist) => dist % 2 == 0) Iterator(a, b) .map(v => v.values.toSeq.sorted.toArray) .toArray } ) // input is funni val _ = assert(board.head.forall(_ == '.')) assert(board.last.forall(_ == '.')) assert(board.forall(_.head == '.')) assert(board.forall(_.last == '.')) val (x, y) = startPoint assert((0 until board.length).forall(board(_)(y) != '#')) assert((0 until board(0).length).forall(board(x)(_) != '#')) def pointsReachable(steps: Int): Long = val (sx, sy) = startPoint def distToRow(delta: Int) = if delta == 0 then 0L else if delta < 0 then // go up 1L + sx + (delta + 1L).abs * board.size else 0L + (board.size - sx) + (delta - 1L) * board.size def distToCol(delta: Int) = if delta == 0 then 0L else if delta < 0 then // go left 1L + sy + (delta + 1L).abs * board(0).size else 0L + (board(0).size - sy) + (delta - 1L) * board(0).size def startWhere(dx: Int, dy: Int) = val x = if dx == 0 then sx else if dx < 0 then board.size - 1 else 0 val y = if dy == 0 then sy else if dy < 0 then board(0).size - 1 else 0 (x, y) def countWhere(steps: Int, dx: Int, dy: Int) = val dToBoard = distToRow(dx) + distToCol(dy) if dToBoard > steps then 0L else val (stx, sty) = startWhere(dx, dy) val dists = shortestPathsFrom(stx, sty)((steps - dToBoard).toInt % 2) val res = dists.sortedCountUntil(steps - dToBoard.toInt).toLong // println(s"got $dx $dy $dToBoard $stx $sty => $res ${shortestPathsFrom(stx, sty).map(_.size).toSeq}") res (-1 to 1).zip((-1 to 1)).foreach { (dx, dy) => val (stx, sty) = startWhere(dx, dy) // println(s"$dx $dy ${shortestPathsFrom(stx, sty).map(_.size).toSeq}") } extension (arr: Array[Int]) def sortedCountUntil(max: Int) = var low = 0 var hi = arr.size while low < hi do val mid = (low + hi) / 2 // println(s"$mid => ${arr(mid)} ~ $max") if arr(mid) <= max then low = mid + 1 else hi = mid // println(s"found $low => ${arr(low - 1)}") low ((0 to steps).toIterator .takeWhile(distToRow(_) <= steps) ++ (-1 to -steps by -1).toIterator.takeWhile(distToRow(_) <= steps)) .map(dx => val dRow = distToRow(dx) val zero = countWhere(steps, dx, 0) val right = val (stx, sty) = startWhere(dx, 1) val baseDistance = dRow + distToCol(1) // dToBoard = baseDistance + (delta-1) * board.size if baseDistance > steps then 0L else val maxRightAllBoard = ((steps - baseDistance - shortestPathsFrom(stx, sty).map(_.last).max) / board.size).toInt.max(0) + 1 val odd = ((steps - baseDistance) % 2).toInt val a = (1L until maxRightAllBoard by 2).size * shortestPathsFrom(stx, sty)(odd).size val b = (2L until maxRightAllBoard by 2).size * shortestPathsFrom(stx, sty)((board.size + odd) % 2).size // println( // s"$dx => $maxRightAllBoard [${distToRow(dx) + distToCol(maxRightAllBoard)} ${shortestPathsFrom(stx, sty) // .map(_.last) // .max}] ${a + b}" // ) val rest = (maxRightAllBoard to steps).toIterator .takeWhile(dRow + distToCol(_) <= steps) .map(countWhere(steps, dx, _)) .sum a + b + rest val left = val (stx, sty) = startWhere(dx, -1) val baseDistance = dRow + distToCol(-1) // dToBoard = baseDistance + (delta-1) * board.size if baseDistance > steps then 0L else val maxLeftAllBoard = ((steps - baseDistance - shortestPathsFrom(stx, sty).map(_.last).max) / board.size).toInt.max(0) + 1 // println(s"$dx => -$maxLeftAllBoard") val odd = ((steps - baseDistance) % 2).toInt val a = (-1L until -maxLeftAllBoard by -2).size * shortestPathsFrom(stx, sty)(odd).size val b = (-2L until -maxLeftAllBoard by -2).size * shortestPathsFrom(stx, sty)((board.size + odd) % 2).size // println(s"$dx => -$maxLeftAllBoard [${distToRow(dx) + distToCol(-maxLeftAllBoard)}] ${a + b}") val rest = (-maxLeftAllBoard to -steps by -1).toIterator .takeWhile(dRow + distToCol(_) <= steps) .map(countWhere(steps, dx, _)) .sum a + b + rest zero + right + left ) .sum def part2 = val inputs = Seq(26501365) // val inputs = Seq(1, 131) println(inputs.map(pointsReachable)) // val s = walk(200, Set(startPoint)) // // divide by quadrant // val quads = s.toSeq // .groupBy((x, y) => // val qx = if x < 0 then -(-x + board.size - 1) / board.size else x / board.size // val qy = if y < 0 then -(-y + board(0).size - 1) / board(0).size else y / board(0).size // (qx, qy) // ) // .mapValues(_.size) // println((quads.toSeq, quads.values.sum)) @main def Day21(part: Int) = part match case 1 => part1 case 2 => part2