Advent of Code 2024

thoughts and solutions

 

Day 16

file:test/Day16Spec.jl
# add tests
file:src/Day16.jl
module Day16

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
    map::Matrix{Char}
    start_tile::Pos
    end_tile::Pos
end

function read_input(io::IO)
    map = io |> readlines |> stack
    start_tile = findfirst(isequal('S'), map)
    end_tile = findfirst(isequal('E'), map)
    map[start_tile] = '.'
    map[end_tile] = '.'
    return Maze(map, start_tile, end_tile)
end

function grid_dijkstra(
    ::Type{T}, size::NTuple{Dim,Int},
    start::Vector{CartesianIndex{Dim}}, istarget::F1,
    neighbours::F2, dist_func::F3) where {T,Dim,F1,F2,F3}

    visited = falses(size...)
    distance = fill(typemax(T), size)
    queue = PriorityQueue{CartesianIndex{Dim},T}()
    prev = Array{CartesianIndex{Dim},Dim}(undef, size)

    for s in start
        distance[s] = zero(T)
        enqueue!(queue, s, zero(T))
    end
    current = nothing
    while !isempty(queue)
        current = dequeue!(queue)
        istarget(current) && break
        visited[current] && continue
        for loc in neighbours(current)
            visited[loc] && continue
            d = distance[current] + dist_func(current, loc)
            if d < distance[loc]
                distance[loc] = d
                prev[loc] = current
                queue[loc] = d
            end
        end
        visited[current] = true
    end
    (distance=distance, route=prev, target=current)
end

function trace_back_all(gd, start, neighbours, dist_func)
    visited = falses(size(gd.distance)...)
    stack = [gd.target]
    while !isempty(stack)
        pt = pop!(stack)
        visited[pt] && continue
        visited[pt] = true
        pt == start && continue
        optimal = gd.route[pt]
        for nb in neighbours(pt)
            if gd.distance[pt] == gd.distance[nb] + dist_func(nb, pt)
                push!(stack, nb)
            end
        end
    end
    return visited
end

const NB = [Pos(1, 0), Pos(0, 1), Pos(-1, 0), Pos(0, -1)]
const EAST = 1

function main(io::IO)
    maze = read_input(io)
    is_target(a::PosDir) = pos(a) == maze.end_tile
    path_tile(a::PosDir) = maze.map[pos(a)] == '.'
    neighbours(a::PosDir) = filter(path_tile,
        [posdir(pos(a) + NB[dir(a)], dir(a)),
         posdir(pos(a), mod1(dir(a) + 1, 4)),
         posdir(pos(a), mod1(dir(a) - 1, 4))])
    dist_func(a::PosDir, b::PosDir) = abs(a[1]-b[1]) + abs(a[2]-b[2]) +
        min(mod(a[3] - b[3], 4), mod(b[3] - a[3], 4)) * 1000

    gd = grid_dijkstra(Int, (size(maze.map)..., 4),
        [posdir(maze.start_tile, EAST)],
        is_target, neighbours, dist_func)

    part1 = gd.distance[gd.target]

    rev_neighbours(a::PosDir) = filter(path_tile,
        [posdir(pos(a) - NB[dir(a)], dir(a)),
         posdir(pos(a), mod1(dir(a) + 1, 4)),
         posdir(pos(a), mod1(dir(a) - 1, 4))])

    trace = trace_back_all(gd, posdir(maze.start_tile, EAST),
        rev_neighbours, dist_func)

    part2 = reduce(|, trace, dims=3) |> sum

    return part1, part2
end

end