Day 18
prelude
https://adventofcode.com/2024/day/18
import {
add,
clone,
distance,
eq,
find,
HashMap,
inputDay,
munge,
} from "./lib/utilities.js"
const test = munge(`5,4
4,2
4,5
3,0
2,1
6,3
2,4
1,5
0,6
3,3
2,6
5,1
1,2
5,5
2,5
6,5
1,4
0,4
6,4
1,1
6,1
1,0
0,5
1,6
2,0`)
const testmap = [...new Array(7)].map(x => [...new Array(7)].map(x => "."))
const input = await inputDay(18)
const inputmap = [...new Array(71)].map(x => [...new Array(71)].map(x => "."))
const N = [-1, 0]
const E = [0, 1]
const S = [1, 0]
const W = [0, -1]
const WALL = "#"
const FLOOR = "."
const start = [0, 0]
part 1
Copying and modifying my Dijkstra's (actually A*
) implementation from day 16, which I haven't posted because my implementation there gives slightly wrong answers! Nevertheless, it works fine on today's.
function inbounds(pt, w, h) {
return pt[0] >= 0 && pt[0] < h && pt[1] >= 0 && pt[1] < w
}
// Search with Dijkstra's algorithm
//
// https://www.redblobgames.com/pathfinding/a-star/introduction.html#dijkstra
function search(map, start, end, debug = false, debugId = undefined) {
// priority queue, pairs of [cost, point]
const frontier = [[0, start]]
// the path and the cost for the path
const cameFrom = new HashMap([[start, null]])
const costSoFar = new HashMap([[start, 0]])
while (frontier.length > 0) {
// there's no heap in the js standard library, so we'll just sort our list
// every time through the loop. We want the lowest cost coming last so we
// can pop it off the end
frontier.sort((a, b) => a[0] < b[0])
const [, pt] = frontier.pop()
if (eq(pt, end)) break
// for each valid (inbounds, non-wall) neighbor
for (const next of [N, E, S, W]
.map(d => add(pt, d))
.filter(pt => inbounds(pt, map[0].length, map.length))
.filter(([y, x]) => map[y][x] != WALL)) {
const newCost = costSoFar.get(pt) + 1
// if the new cost is lower than the current cost, or there isn't a cost
// yet, add it to the path
if (newCost < (costSoFar.get(next) ?? Number.MAX_VALUE)) {
// add the cost to the front of the array, as it's likely to be more
// expensive than what's at the end of the array; it'll get sorted in a
// second
frontier.unshift([newCost + distance(next, end), next])
// update the cost for the cell
costSoFar.set(next, newCost)
// add how we got to the cell
cameFrom.set(next, pt)
}
}
}
if (debug) showdebug(map, cameFrom, costSoFar, start, end, debugId)
return [costSoFar, cameFrom]
}
Now we can write a function to get the path length for a given number of obstacles
function pathlen(map, rocks, n, debug = false, debugId = undefined) {
const m = clone(map)
for (const [x, y] of rocks.slice(0, n)) {
m[y][x] = WALL
}
let end = [m.length - 1, m[0].length - 1]
let res = search(m, [0, 0], end, debug, debugId)
return res[0].get(end)
}
display(pathlen(testmap, test, 12))
display(pathlen(inputmap, input, 1024, true))
And get some nice debug output
function showdebug(map, cameFrom, costSoFar, start, end, id = "mapCanvas") {
const width = 640
const height = 640
const mapw = map[0].length
const maph = map.length
const colw = width / mapw
const rowh = height / maph
const ctx = document.getElementById(id).getContext("2d")
ctx.clearRect(0, 0, width, height)
for (const pt of cameFrom) {
const [y, x] = pt[0].split(",").map(Number)
ctx.fillStyle = "#444"
ctx.fillRect(x * colw, y * rowh, colw, rowh)
}
let pt = cameFrom.get(end)
while (pt) {
const [y, x] = pt
ctx.fillStyle = "#D35FB7"
ctx.fillRect(x * colw, y * rowh, colw, rowh)
pt = cameFrom.get(pt)
}
for (let row = 0; row < maph; row++) {
for (let col = 0; col < mapw; col++) {
ctx.strokeRect(col * colw, row * rowh, colw, rowh)
if (map[row][col] == WALL) {
ctx.fillStyle = "#4269d0"
ctx.fillRect(col * colw, row * rowh, colw, rowh)
}
if ([start, end].some(pt => eq(pt, [row, col]))) {
ctx.fillStyle = "#FEFE62"
ctx.fillRect(col * colw, row * rowh, colw, rowh)
}
}
}
}
part 2
For part 2, iterate through the stones until we find one that doesn't have a path
// the starting point was found by trial & error, I don't want to spend your
// CPU cycles calculating it, but I ran it from 1024
for (let i = 2860; i < input.length; i++) {
if (!pathlen(inputmap, input, i)) {
display(input[i - 1].join(","))
break
}
}
// let's display the maze before and after the final brick
pathlen(inputmap, input, 2861, true, "mapPart2")
pathlen(inputmap, input, 2862, true, "mapPart2b")