Day 20
file:test/Day20Spec.jl
using AOC2024.Day20: read_input, Pos, walk_maze, find_cheats, trace_back, part2
let test_txt = """
###############
#...#...#.....#
#.#.#.#.#.###.#
#S#...#.#.#...#
#######.#.#.###
#######.#.#...#
#######.#.###.#
###..E#...#...#
###.#######.###
#...###...#...#
#.#####.#.###.#
#.#...#.#.#...#
#.#.#.#.#.#.###
#...#...#...###
###############"""
= read_input(IOBuffer(test_txt))
maze @test maze.start_tile == Pos(2, 4)
= part2(maze)
hist = sort(filter(((k, v),) -> k >= 50, hist)) |> stack
hist_arr = [50 52 54 56 58 60 62 64 66 68 70 72 74 76;
target 32 31 29 39 25 23 20 19 12 14 12 22 4 3]
@test hist_arr == target
end
file:src/Day20.jl
module Day20
using DataStructures
const Pos = CartesianIndex{2}
const PosDir = CartesianIndex{3}
pos(pd::PosDir) = Pos(pd[1], pd[2])
dir(pd::PosDir) = pd[3]
posdir(p::Pos, d::Int) = CartesianIndex(p[1], p[2], d)
struct Maze
::Matrix{Char}
map::Pos
start_tile::Pos
end_tileend
function read_input(io::IO)
= io |> readlines |> stack
map = findfirst(isequal('S'), map)
start_tile = findfirst(isequal('E'), map)
end_tile = '.'
map[start_tile] = '.'
map[end_tile] return Maze(map, start_tile, end_tile)
end
function grid_dijkstra(
::Type{T}, size::NTuple{Dim,Int},
::Vector{CartesianIndex{Dim}}, istarget::F1,
start::F2, dist_func::F3) where {T,Dim,F1,F2,F3}
neighbours
= falses(size...)
visited = fill(typemax(T), size)
distance = PriorityQueue{CartesianIndex{Dim},T}()
queue = Array{CartesianIndex{Dim},Dim}(undef, size)
prev
for s in start
= zero(T)
distance[s] enqueue!(queue, s, zero(T))
end
= nothing
current while !isempty(queue)
= dequeue!(queue)
current istarget(current) && return (distance=distance, route=prev, target=current)
&& continue
visited[current] for loc in neighbours(current)
&& continue
visited[loc] = distance[current] + dist_func(current, loc)
d if d < distance[loc]
= d
distance[loc] = current
prev[loc] = d
queue[loc] end
end
= true
visited[current] end
return nothing
end
const PosDist = Tuple{Pos,Int}
function find_cheats(m::Maze, start::Pos)
= falses(size(m.map))
visited = Queue{PosDist}()
queue enqueue!(queue, (start, 0))
= PosDist[]
result
while !isempty(queue)
= dequeue!(queue)
p, d += 1
d > 20 && return result
d
for nb in NB
= p + nb
q checkbounds(Bool, m.map, q) || continue
&& continue
visited[q] = true
visited[q] if m.map[q] == '.'
push!(result, (q, d))
end
# q == m.end_tile && continue
enqueue!(queue, (q, d))
end
end
return result
end
function trace_back(prev, start, target)
= [target]
route = target
current while current != start
= prev[current]
current push!(route, current)
end
reverse(route)
end
const NB = [Pos(1, 0), Pos(0, 1), Pos(-1, 0), Pos(0, -1)]
function walk_maze(m::Maze)
= [m.start_tile]
start is_target(p::Pos) = p == m.end_tile
neighbours(p::Pos) = filter(q->m.map[q] != '#', p .+ NB)
dist_func(p::Pos, q::Pos) = 1
grid_dijkstra(Int, size(m.map), start, is_target, neighbours, dist_func)
end
function part2(maze::Maze)
= walk_maze(maze)
g = trace_back(g.route, maze.start_tile, g.target)
route = Dict{Int, Int}()
hist
for pos in route
= find_cheats(maze, pos)
cheats for (q, d) in cheats
= g.distance[q] - g.distance[pos] - d
saved = get(hist, saved, 0) + 1
hist[saved] end
end
return hist
end
function main(io::IO)
= read_input(io)
maze = walk_maze(maze)
g = trace_back(g.route, maze.start_tile, g.target)
route = 0
part1 for pos in route
for nb in NB
checkbounds(Bool, maze.map, pos+2nb) || continue
+nb] == '#' || continue
maze.map[pos+2nb] == '.' || continue
maze.map[pos+2nb] - g.distance[pos] >= 102 || continue
g.distance[pos+= 1
part1 end
end
= 0
part2 for pos in route
= find_cheats(maze, pos)
cheats for (q, d) in cheats
- g.distance[pos] - d >= 100 || continue
g.distance[q] += 1
part2 end
end
return part1, part2
end
end