advent_of_code_2022

My (attempted) solutions to the 2022 Advent of Code
git clone https://git.eamoncaddigan.net/advent_of_code_2022.git
Log | Files | Refs | README

day_24.jl (7150B)


      1 #!/usr/bin/env julia
      2 # https://adventofcode.com/2022/day/24
      3 using AdventOfCode
      4 using DataStructures
      5 
      6 example = readlines(IOBuffer("""
      7     #.######
      8     #>>.<^<#
      9     #.<..<<#
     10     #>v.><>#
     11     #<^v^^>#
     12     ######.#"""))
     13 input = readlines("data/day_24.txt")
     14 
     15 # Each node in our graph is represented by a three-tuple of time, y, and x
     16 Node = Tuple{Int,Int,Int}
     17 
     18 # The blizzard map is represented by 4 boolean matrices, each indicating the
     19 # blizzards at each location moving in a specific direction. We also bundle
     20 # with it the current timestep represented by the map
     21 mutable struct BlizzardMap
     22     blizzards::Array{Bool, 3}
     23     timestep::Int
     24 end
     25 
     26 """
     27 Read the puzzle input and return the blizzard positions.
     28 """
     29 function BlizzardMap(input::Vector{<:AbstractString})
     30     maparray = zeros(Bool,
     31         length(input)-2,
     32         length(input[1])-2,
     33         4
     34     )
     35 
     36     # Hardcoding the start and end positions, but checking that things look ok
     37     @assert input[1][2] == '.'
     38     @assert input[end][end-1] == '.'
     39 
     40     # Reading the rest of the map
     41     for (i, line) = enumerate(input[2:end-1])
     42         linechars = split(line, "")[2:end-1]
     43         maparray[i,:,1] = linechars .== "^"
     44         maparray[i,:,2] = linechars .== "v"
     45         maparray[i,:,3] = linechars .== "<"
     46         maparray[i,:,4] = linechars .== ">"
     47     end
     48 
     49     BlizzardMap(maparray, 0)
     50 end
     51 
     52 Base.size(blizzardmap::BlizzardMap, dim::Int) = size(blizzardmap.blizzards, dim)
     53 
     54 """
     55 Set the state of the blizzard map to a given time.
     56 """
     57 function setmaptime!(blizzardmap::BlizzardMap, newtime::Int)
     58     timeoffset = newtime - blizzardmap.timestep
     59     blizzardmap.timestep = newtime
     60 
     61     if timeoffset == 0
     62         blizzardmap
     63     else
     64         newmap = similar(blizzardmap.blizzards)
     65         for y = 1:size(blizzardmap, 1), x = 1:size(blizzardmap, 2)
     66             newmap[y, x, 1] = blizzardmap.blizzards[
     67                 1+mod(y-1+timeoffset, size(blizzardmap, 1)), x, 1
     68             ]
     69             newmap[y, x, 2] = blizzardmap.blizzards[
     70                 1+mod(y-1-timeoffset, size(blizzardmap, 1)), x, 2
     71             ]
     72             newmap[y, x, 3] = blizzardmap.blizzards[
     73                 y, 1+mod(x-1+timeoffset, size(blizzardmap, 2)), 3
     74             ]
     75             newmap[y, x, 4] = blizzardmap.blizzards[
     76                 y, 1+mod(x-1-timeoffset, size(blizzardmap, 2)), 4
     77             ]
     78         end
     79         blizzardmap.blizzards = newmap
     80         blizzardmap
     81     end
     82 end
     83 
     84 """
     85 Return true iff the node is the goal node
     86 """
     87 function isgoal(node::Node, blizzardmap::BlizzardMap)
     88     node[2] == size(blizzardmap, 1) + 1 &&
     89         node[3] == size(blizzardmap, 2)
     90 end
     91 
     92 """
     93 Return the neighbors (if any) of a given node in the blizzard map.
     94 
     95 This changes the time of the blizzard map.
     96 """
     97 function get_neighbors(node::Node, blizzardmap::BlizzardMap)::Vector{Node}
     98     if node[2] == size(blizzardmap, 1) && node[3] == size(blizzardmap, 2)
     99         # One tiny optimization/hack I'll use here is to only return the goal
    100         # node if we're next to it already.
    101         return([(node[1]+1, node[2]+1, node[3])])
    102     elseif isgoal(node, blizzardmap)
    103         # This is just defensive programming; if I do everything right before I
    104         # call this function, this branch will never be visited. Nevertheless!
    105         return([node])
    106     end
    107 
    108     setmaptime!(blizzardmap, node[1]+1)
    109     # nextmap shows where blizzards are, ie where we can't step
    110     # it's still a 3D array, but the third dimension length == 1
    111     nextmap = reduce(|, blizzardmap.blizzards, dims = 3)
    112     neighbors = Vector{Node}()
    113 
    114     if node[2] == 0 && node[3] == 1
    115         # We're at the start, where we always have the option to stay
    116         push!(neighbors, node .+ (1, 0, 0))
    117         !nextmap[1, 1, 1] && push!(neighbors, node .+ (1, 1, 0))
    118     else
    119         for (Δy, Δx) = [(-1, 0), (1, 0), (0, -1), (0, 1), (0, 0)]
    120             (t, y, x) = node .+ (1, Δy, Δx)
    121             if y > 0 && x > 0 && 
    122                     y <= size(blizzardmap, 1) &&
    123                     x <= size(blizzardmap, 2) &&
    124                     !nextmap[y, x, 1]
    125                 push!(neighbors, (t, y, x))
    126             end
    127         end
    128     end
    129     neighbors
    130 end
    131 
    132 """
    133 Return the manhattan distance from the node to the goal.
    134 """
    135 function manhattandist(node::Node, blizzardmap::BlizzardMap)
    136     isgoal(node, blizzardmap) ? 0 :
    137         size(blizzardmap, 1) - node[2] + size(blizzardmap, 2) - node[3] + 1
    138 end
    139 
    140 """
    141 Reconstruct the path in reverse-chronological order.
    142 """
    143 function reconstruct_path(camefrom::Dict{Node, Node}, current::Node)
    144     totalpath = [current]
    145     while current ∈ keys(camefrom)
    146         current = camefrom[current]
    147         push!(totalpath, current)
    148     end
    149     totalpath
    150 end
    151 
    152 """
    153 Find the shortest path from the beginning to end of the map.
    154 
    155 This looks like A* search (I even cribbed from wikipedia's pseudocode, again),
    156 but I think it just reduces to a slightly smarter breadth-first search. Might
    157 need to fix that depending on the non-example input and part 2 requirements.
    158 This also changes blizzardmap.
    159 """
    160 function navigatemap!(blizzardmap::BlizzardMap, h::Function = manhattandist)
    161     start = (0, 0, 1)
    162     camefrom = Dict{Node, Node}()
    163 
    164     # This is usually a map giving the lowest known cost path from start to
    165     # each known node. Since the first element of each node codes that
    166     # implicitly, we can just use a set.
    167     gscore = Set([start])
    168 
    169     # The set of discovered nodes
    170     openset = PriorityQueue{Node, Int}(start => h(start, blizzardmap))
    171 
    172     while !isempty(openset)
    173         current = dequeue!(openset)
    174 
    175         if isgoal(current, blizzardmap)
    176             setmaptime!(blizzardmap, current[1])
    177             return reconstruct_path(camefrom, current)
    178         end
    179 
    180         for neighbor = get_neighbors(current, blizzardmap)
    181             if neighbor ∉ gscore
    182                 # We haven't found a path through this node before
    183                 openset[neighbor] = neighbor[1] + h(neighbor, blizzardmap)
    184                 camefrom[neighbor] = current
    185             end
    186         end
    187     end
    188     nothing
    189 end
    190 
    191 function part_1(input)
    192     navigatemap!(BlizzardMap(input))[1][1]
    193 end
    194 @assert part_1(example) == 18
    195 @info part_1(input)
    196 
    197 """
    198 Flip a map, as if you are turning around and retracing your steps.
    199 
    200 As hacks go, I'm not sure where this approach lands between 'clever' and
    201 'hideous', but if we can flip the map around, then solving part 2 is just a
    202 matter of solving part 1 two additional times.
    203 """
    204 function flipmap!(blizzardmap::BlizzardMap)
    205     newmap = similar(blizzardmap.blizzards)
    206 
    207     for y = 1:size(blizzardmap, 1), x = 1:size(blizzardmap, 2)
    208         oldy = size(blizzardmap, 1) - y + 1
    209         oldx = size(blizzardmap, 2) - x + 1
    210         for z = 1:4
    211             oldz = z % 2 == 1 ? z + 1 : z - 1
    212             newmap[y, x, z] = blizzardmap.blizzards[oldy, oldx, oldz]
    213         end
    214     end
    215 
    216     blizzardmap.blizzards = newmap
    217     blizzardmap.timestep = 0
    218     blizzardmap
    219 end
    220 
    221 function part_2(input)
    222     blizzardmap = BlizzardMap(input)
    223     steps = 0
    224     for i = 1:3
    225         steps += navigatemap!(blizzardmap)[1][1]
    226         i < 3 && flipmap!(blizzardmap)
    227     end
    228     steps
    229 end
    230 @assert part_2(example) == 54
    231 @info part_2(input)