comparison day17.rb @ 39:0e17e4bd97a9 default tip

Rewrite as four-dimensional route finding The grid isn't just a 2D grid. The constraints make it 4D: * X * Y * Last direction * Number of steps in that direction By tracking all four dimensions, we can find the shortest route for _all_ combinations of the constraint. Previously, we were dropping routes that were currently longer but ended up shorter because they could take subsequent steps that other routes couldn't.
author IBBoard <dev@ibboard.co.uk>
date Sun, 22 Sep 2024 11:30:53 +0100
parents 8e92cb172e6b
children
comparison
equal deleted inserted replaced
38:8e92cb172e6b 39:0e17e4bd97a9
9 end 9 end
10 10
11 file = ARGV[0] 11 file = ARGV[0]
12 12
13 Coord = Struct.new(:x, :y) 13 Coord = Struct.new(:x, :y)
14 Route = Struct.new(:steps, :distance) 14 Node = Struct.new(:coords, :direction, :num_prev_steps)
15 Routes = Struct.new(:routes, :min_distance) 15 RouteNode = Struct.new(:node, :cost, :previous_node)
16
17 module Directions
18 UP = 0
19 LEFT = 1
20 DOWN = 2
21 RIGHT = 3
22 end
23
24 $up = Coord.new(0, -1)
25 $left = Coord.new(-1, 0)
26 $down = Coord.new(0, 1)
27 $right = Coord.new(1, 0)
28 directions = [$up, $left, $down, $right]
29
30 city_map = File.open(file, "r").each_line(chomp: true).map{ |line| line.each_char.map(&:to_i)}
31
32 routes = city_map.map {|row| row.map { (0..3).map { (0..2).map {nil}}}}
33
34 $width = city_map[0].length
35 $height = city_map.length
36
16 Colour = Struct.new(:r, :g, :b) 37 Colour = Struct.new(:r, :g, :b)
17 38
18 city_map = File.open(file, "r").each_line(chomp: true).map{ |line| line.each_char.map(&:to_i)} 39 start_space = Coord.new(0, 0)
19
20 $height = city_map.length
21 $width = city_map[0].length
22
23 start_space = Coord.new(0,0)
24 end_space = Coord.new($width - 1, $height - 1) 40 end_space = Coord.new($width - 1, $height - 1)
25 41
26 visited = {} 42 class PQueue
27 routes = { start_space => Routes.new([Route.new([start_space], 0)], 0)} 43 def initialize()
28 44 # Placeholder at the root to simplify calculations
29 unvisited = Set.new 45 @objects = [nil]
30 unvisited << start_space 46 end
31 #(0...city_map.length).each {|y| (0...city_map[0].length).each {|x| unvisited << Coord.new(x,y)}} 47
32 48 def push(element)
33 $up = Coord.new(0, -1) 49 new_idx = @objects.size
34 $down = Coord.new(0, 1) 50 @objects << element
35 $left = Coord.new(-1, 0) 51 _reorder_insert(new_idx)
36 $right = Coord.new(1, 0) 52 end
53
54 def _reorder_insert(idx)
55 parent_idx = idx / 2
56 if idx > 1 and @objects[parent_idx].cost > @objects[idx].cost
57 _swap(idx, parent_idx)
58 _reorder_insert(parent_idx)
59 end
60 end
61
62 def _swap(idx1, idx2)
63 @objects[idx2], @objects[idx1] = @objects[idx1], @objects[idx2]
64 end
65
66 def pop()
67 # Swap for easy popping without moving everything
68 _swap(1, @objects.size - 1)
69 priority_obj = @objects.pop
70 _reorder_remove(1)
71 priority_obj
72 end
73
74 def _reorder_remove(idx)
75 child1_idx = idx * 2
76 child2_idx = child1_idx + 1
77 last_idx = @objects.size - 1
78
79 if child1_idx <= last_idx
80 child1_val = @objects[child1_idx].cost
81 child2_val = child2_idx <= last_idx ? @objects[child2_idx].cost : Float::INFINITY
82 child_idx, child_val = child1_val < child2_val ? [child1_idx, child1_val] : [child2_idx, child2_val]
83
84 if @objects[idx].cost >= child_val
85 _swap(idx, child_idx)
86 _reorder_remove(child_idx)
87 end
88 end
89 end
90
91 def empty?
92 @objects.size <= 1
93 end
94
95 def to_s
96 @objects.map {|val| "#{val.coords.x},#{val.coords.y}=#{val.cost}" if val }
97 end
98 end
37 99
38 # Our route is constrained to "four consecutive spaces". Zig-zag through the middle 100 # Our route is constrained to "four consecutive spaces". Zig-zag through the middle
39 # obeys that rule and would be a worst case score. 101 # obeys that rule and it's either the shortest route _or_ we won't explore a longer route
40 max_score = city_map.each_with_index.map {|row, i| i < row.length - 1 ? [row[i], row[i+1]] : [row[i]]}.flatten.reduce(:+) 102 max_score = city_map.each_with_index.map {|row, i| i < row.length - 1 ? [row[i], row[i+1]] : [row[i]]}.flatten.reduce(:+)
41 103
42 $colours = (0..max_score).map do |i| 104 $colours = (0..max_score).map do |i|
43 # Use 300° because we don't want to be confused and loop back to red 105 angle = (i.to_f / max_score) * 360
44 angle = (i.to_f / max_score) * 300
45 if angle <= 60 106 if angle <= 60
46 r = 0xff 107 r = 0xff
47 g = 0xff * angle / 60 108 g = 0xff * angle / 60
48 b = 0 109 b = 0
49 elsif angle <= 120 110 elsif angle <= 120
56 b = 0xff * (angle - 120) / 60 117 b = 0xff * (angle - 120) / 60
57 elsif angle <= 240 118 elsif angle <= 240
58 r = 0 119 r = 0
59 g = 0xff * (240 - angle) / 60 120 g = 0xff * (240 - angle) / 60
60 b = 0xff 121 b = 0xff
61 else 122 elsif angle <= 300
62 r = 0xff * (240 - angle) / 60 123 r = 0xff * (240 - angle) / 60
63 g = 0 124 g = 0
64 b = 0xff 125 b = 0xff
126 else
127 r = 0xff
128 g = 0
129 b = 0xff * (360 - angle) / 60
65 end 130 end
66 Colour.new(r.to_i, g.to_i, b.to_i) 131 Colour.new(r.to_i, g.to_i, b.to_i)
67 end 132 end
68 133
69 134 def update_map(new_node, map)
70 def get_neighbours(city_map, x, y) 135 up = $height - new_node.node.coords.y
71 [$up, $down, $left, $right].map {|coord| Coord.new(coord.x + x, coord.y + y)}.filter {|coord| coord.x >= 0 and coord.y >= 0 and coord.x < city_map[0].length and coord.y < city_map.length}
72 end
73
74 def steps_since_turn(route)
75 if route.steps.length < 2
76 return 1
77 end
78 # puts "#{route}"
79 x_steps = 0
80 y_steps = 0
81 x_turned = false
82 y_turned = false
83 step_range = ..([6, route.length].max)
84 route.steps.reverse[step_range].each_cons(2) do |step_n, step_m|
85 x_diff = (step_n.x - step_m.x).abs
86 y_diff = (step_n.y - step_m.y).abs
87 if x_diff > 0 and ! x_turned
88 x_steps += 1
89 else
90 x_turned = true
91 end
92 if y_diff > 0 and ! y_turned
93 y_steps += 1
94 else
95 y_turned = true
96 end
97 end
98 (y_steps * 10) + x_steps
99 end
100
101 def allowed_routes(city_map, routes, cur_node, next_node)
102 next_step_distance = city_map[next_node.y][next_node.x]
103 candidate_routes = routes[cur_node].routes.map {|route| Route.new(route.steps + [next_node], route.distance + next_step_distance)}
104 valid_routes = candidate_routes.filter do |route|
105 looped = route.steps.count(next_node) > 1
106 if route.steps.length < 5
107 ! looped
108 else
109 step_range = route.steps[-5..-2]
110 !(step_range.all?{|step| step.x == next_node.x} or step_range.all?{|step| step.y == next_node.y}) and !looped
111 end
112 end
113 valid_routes = valid_routes + routes[next_node].routes unless routes[next_node].nil?
114 valid_routes = valid_routes.group_by {|route| steps_since_turn(route)}.values.map {|routes| routes.sort {|a,b| a.distance <=> b.distance}[0]}
115 Routes.new(valid_routes, valid_routes.map(&:distance).min)
116 end
117
118 def get_distance(routes, node)
119 if routes.key?(node)
120 routes[node].min_distance
121 else
122 Float::INFINITY
123 end
124 end
125
126 def update_map(new_node, map, routes)
127 routes_to_cell = routes[new_node]
128 up = $height - new_node.y
129 # We printed a new line after the map, so we're already in column 0 and go X forward (right) 136 # We printed a new line after the map, so we're already in column 0 and go X forward (right)
130 forward = new_node.x 137 forward = new_node.node.coords.x
131 if routes_to_cell 138 colour = $colours[new_node.cost]
132 colour = $colours[routes_to_cell.min_distance] 139 cell = map[new_node.node.coords.y][new_node.node.coords.x]
133 cell = map[new_node.y][new_node.x] 140 #puts [cell, new_node, $height, $width, up, forward, colour]
134 #puts [cell, new_node, $height, $width, up, forward, colour] 141 print "\e[s"
135 print "\e[s" 142 if forward > 0
136 if forward > 0 143 print "\e[#{forward}C"
137 print "\e[#{forward}C" 144 end
138 end 145 if up > 0
139 if up > 0 146 print "\e[#{up}A"
140 print "\e[#{up}A" 147 end
141 end 148 print "\e[48;2;#{colour.r};#{colour.g};#{colour.b}m#{cell}\e[0m\e[u"
142 print "\e[48;2;#{colour.r};#{colour.g};#{colour.b}m#{cell}\e[0m\e[u"
143 end
144 end 149 end
145 150
146 puts "#{city_map.map {|row| row.join("")}.join("\n")}" 151 puts "#{city_map.map {|row| row.join("")}.join("\n")}"
147 # sleep(5) # For screen recording 152 # sleep(5) # For screen recording
148 153
149 until unvisited.empty? 154 unvisited = PQueue.new
150 #puts "Start loop: #{unvisited}" 155 # Fake two starting steps from outside so that we use the "turn" logic to get
151 #puts routes 156 # routes with zero previous steps
152 min_node = nil 157 start_node_1 = RouteNode.new(Node.new(start_space, Directions::RIGHT, 0), 0, nil)
153 min_node_direct_distance = Float::INFINITY 158 start_node_2 = RouteNode.new(Node.new(start_space, Directions::DOWN, 0), 0, nil)
154 unvisited.each do |node| 159 unvisited.push(start_node_1)
155 x_dist = end_space.x - node.x 160 unvisited.push(start_node_2)
156 y_dist = end_space.y - node.y 161 visited = Set.new
157 direct_distance = x_dist + y_dist 162 candidate_node = nil
158 node_route_distance = get_distance(routes, node) + direct_distance 163
159 min_node_route_distance = get_distance(routes, min_node) + min_node_direct_distance 164 def is_shorter?(cur_shortest, new_node)
160 #puts "Node? #{min_node} - #{node_route_distance} < #{min_node_route_distance} || #{direct_distance} < #{min_node_direct_distance}" 165 not cur_shortest or cur_shortest.cost > new_node.cost
161 #sleep(1) 166 end
162 if min_node.nil? || node_route_distance < min_node_route_distance #|| (node_route_distance == min_node_route_distance && direct_distance < min_node_direct_distance) 167
163 min_node = node 168 def is_valid?(x, y)
164 min_node_direct_distance = direct_distance 169 0 <= x and x < $width and 0 <= y and y < $height
165 end 170 end
166 end 171
167 #puts "Unvisited: #{unvisited.length}" 172 while not unvisited.empty?
168 #puts "Min node: #{min_node} from #{unvisited.length}" 173 candidate_node = unvisited.pop
169 update_map(min_node, city_map, routes) 174 candidate_node_node = candidate_node.node
170 175
171 break if not routes.has_key?(min_node) or min_node == end_space 176 if visited.include?(candidate_node_node)
172 177 # Longer route to an already visited node
173 get_neighbours(city_map, min_node.x, min_node.y).each do |neighbour| 178 next
174 new_routes = allowed_routes(city_map, routes, min_node, neighbour) 179 end
175 if new_routes.routes.length > 0 180 visited.add(candidate_node_node)
176 routes[neighbour] = new_routes 181
177 unvisited << neighbour if !visited[neighbour] 182 candidate_coords = candidate_node_node.coords
178 end 183 update_map(candidate_node, city_map)
179 end 184
180 185
181 visited[min_node] = true 186 if candidate_coords == end_space
182 unvisited = unvisited.delete(min_node) 187 break
183 #puts "Delete #{min_node}" 188 end
184 #a = $stdin.gets("\n") 189
185 end 190 current_direction = candidate_node_node.direction
186 191
187 best_route = routes[end_space].routes.sort {|a, b| a.distance <=> b.distance}[0] 192 # We can always turn
188 193 anticlockwise = (current_direction + 1) % 4
189 best_route.steps.each do |step| 194 clockwise = (anticlockwise + 2) % 4
190 cell = city_map[step.y][step.x] 195
191 up = $height - step.y 196 next_steps = [
192 forward = step.x 197 [anticlockwise, 0],
193 print "\e[s" 198 [clockwise, 0]
194 if forward > 0 199 ]
195 print "\e[#{forward}C" 200 next_steps.append([current_direction, candidate_node_node.num_prev_steps + 1]) if candidate_node_node.num_prev_steps < 4
196 end 201
197 if up > 0 202 next_steps.each do |direction, num_prev_steps|
198 print "\e[#{up}A" 203 dir = directions[direction]
199 end 204 new_x = candidate_coords.x + dir.x
200 print "\e[1;30m\e[1;47m#{cell}\e[0m\e[u" 205 new_y = candidate_coords.y + dir.y
201 end 206 if is_valid?(new_x, new_y)
202 207 next_num_prev_steps = num_prev_steps + 1
203 puts best_route.distance 208 cur_shortest = routes[new_y][new_x][direction][next_num_prev_steps]
204 209 if is_shorter?(cur_shortest, candidate_node)
205 #puts "#{city_map.map {|row| row.join("")}.join("\n")}" 210 new_shortest = RouteNode.new(Node.new(Coord.new(new_x, new_y), direction, next_num_prev_steps), candidate_node.cost + city_map[new_y][new_x], candidate_node)
206 211 routes[new_y][new_x][direction][next_num_prev_steps] = new_shortest
207 212 unvisited.push(new_shortest)
208 #puts "#{routes[end_space]}" 213 end
214 end
215 end
216 end
217
218 if candidate_node
219 route_node = candidate_node
220 coords = []
221 while route_node
222 coords.append(route_node.node.coords)
223 route_node = route_node.previous_node
224 end
225 coords.each do |step|
226 cell = city_map[step.y][step.x]
227 up = $height - step.y
228 forward = step.x
229 print "\e[s"
230 if forward > 0
231 print "\e[#{forward}C"
232 end
233 if up > 0
234 print "\e[#{up}A"
235 end
236 print "\e[1;30m\e[1;47m#{cell}\e[0m\e[u"
237 end
238 puts candidate_node.cost
239 end