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)