Day 20
prelude
https://adventofcode.com/2024/day/20
import {
add,
distance,
eq,
find,
grid,
HashMap,
inputDay,
range,
} from "./lib/utilities.js"
const test = grid(`###############
#...#...#.....#
#.#.#.#.#.###.#
#S#...#.#.#...#
#######.#.#.###
#######.#.#...#
#######.#.###.#
###..E#...#...#
###.#######.###
#...###...#...#
#.#####.#.###.#
#.#...#.#.#...#
#.#.#.#.#.#.###
#...#...#...###
###############
`)
display(test)
const input = await inputDay(20, { parser: grid })
const NN = [-2, 0]
const N = [-1, 0]
const NE = [-1, 1]
const E = [0, 1]
const EE = [0, 2]
const SE = [1, 1]
const S = [1, 0]
const SS = [2, 0]
const SW = [1, -1]
const W = [0, -1]
const WW = [0, -2]
const NW = [-1, -1]
const WALL = "#"
const FLOOR = "."
const END = "E"
part 1
I already have a function to do a search through a maze and show me the cost to the end from any point along the path, so the important insight is that the savings of any cheat is the difference between the cost of the point we start the cheat from and the end.
That means that we only need to do pathfinding once, then for every point on the path, find every cheat, and check its value.
Let's start with a function to find every valid cheat from one given point on a map
/* for every possible direction of a 2-ps cheat, return the point if it is
* valid */
function findCheats(map, pt) {
return [NN, N, NE, E, EE, SE, S, SS, SW, W, WW, NW]
.map(d => add(pt, d))
.filter(([y, x]) => [FLOOR, END].includes(map[y]?.[x]))
}
Now do pathfinding on the map, and for each point on the path get every cheat. For every cheat, add one to the number of valid cheats if the difference in cost is greater than the threshold.
I've elided the search function because it's exactly the same as day 18's
function race(map, cheatF = findCheats, threshold = 99, debug = false) {
const start = find(map, "S")
const end = find(map, "E")
const [cost, cameFrom] = search(map, start, end, debug)
let sum = 0
for (const [pt] of cameFrom) {
const [y, x] = pt.split(",").map(Number)
if (eq([y, x], end)) continue
sum += cheatF(map, [y, x], 2)
.map(
([yy, xx]) =>
cost.get([yy, xx]) - cost.get([y, x]) - distance([y, x], [yy, xx]),
)
.filter(x => x > threshold).length
}
return sum
}
display(race(test, findCheats, 12, true))
display(race(input, findCheats, 99, true))
part 2
I generated each valid possible cheat vector with this code, and saved it to lib/day20points.js
display(
JSON.stringify(
range(-20, 21)
.flatMap(y => range(-20, 21).map(x => [y, x]))
.filter(
([y, x]) => !(y == 0 && x == 0) && Math.abs(y) + Math.abs(x) <= 20,
),
),
)
Now we write a function to find cheats just like in part 1, but with our larger set of cheat vectors
import { points } from "./lib/day20points.js"
function findCheats2(map, pt) {
return points
.map(d => add(pt, d))
.filter(([y, x]) => [FLOOR, END].includes(map[y]?.[x]))
}
And finally, search over the larger cheat vector space in exactly the same way
display(race(test, findCheats2, 69))
display(race(input, findCheats2, 99, true))