@@ -8,7 +8,147 @@ def part_one(input)
88 end
99
1010 def part_two ( input )
11- 0
11+ sim = DayTwo . new ( input )
12+ results = sim . run!
13+
14+ top_results = results . select { |k , v | k >= 100 }
15+ top_results . values . sum
16+ end
17+
18+ class DayTwo
19+ #Shamelessly stolen from day 16
20+ DIRECTIONS = [
21+ [ 0 , -1 ] , # North
22+ [ 1 , 0 ] , # East
23+ [ 0 , 1 ] , # South
24+ [ -1 , 0 ] # West
25+ ]
26+
27+ TURN_COST = 1
28+ MOVE_COST = 1
29+
30+ def initialize ( input )
31+ @grid = input . split ( "\n " )
32+ @cols = @grid [ 0 ] . size
33+ @rows = @grid . size
34+
35+ @start = find_cell ( 'S' )
36+ @finish = find_cell ( 'E' )
37+ end
38+
39+ def to_s
40+ @grid . map { |r | r . join ( '' ) }
41+ end
42+
43+ def n_step_reachable ( x , y , n = 2 )
44+ result = [ ]
45+ first_step = neighbours ( x , y )
46+ first_step . each do |dx , dy |
47+ second_step = neighbours ( dx , dy )
48+ second_step . each do |ddx , ddy |
49+ ## ddx,ddy is reachable in 2 steps
50+ result << [ ddx , ddy ] if @grid [ ddy ] [ ddx ] != '#'
51+ end
52+ end
53+ result
54+ end
55+
56+ def run!
57+ distances_start = bfs_distances ( @start )
58+ distances_finish = bfs_distances ( @finish )
59+
60+ normal_time = distances_start [ @finish [ 1 ] ] [ @finish [ 0 ] ]
61+ return 0 if normal_time == Float ::INFINITY
62+
63+ # Get only cells that are reachable from both start and finish
64+ valid_cells = [ ]
65+ # puts @grid.inspect
66+ ( 0 ...@rows ) . each do |y |
67+ ( 0 ...@cols ) . each do |x |
68+ next if @grid [ y ] [ x ] == '#'
69+ if distances_start [ y ] [ x ] != Float ::INFINITY && distances_finish [ y ] [ x ] != Float ::INFINITY
70+ valid_cells << [ x , y ]
71+ end
72+ end
73+ end
74+
75+ # Index cells by row to speed lookups:
76+ valid_cells . sort_by! { |x , y | [ x , y ] }
77+ by_row = Hash . new { |h , k | h [ k ] = [ ] }
78+ valid_cells . each { |x , y | by_row [ y ] << [ x , y ] }
79+
80+ max_distance = 20
81+ savings_count = Hash . new ( 0 )
82+
83+ valid_cells . each do |x , y |
84+ da = distances_start [ y ] [ x ]
85+
86+ # Limit range of rows
87+ min_y = [ 0 , y - max_distance ] . max
88+ max_y = [ @rows - 1 , y + max_distance ] . min
89+
90+ ( min_y ..max_y ) . each do |ry |
91+ by_row [ ry ] . each do |dx , dy |
92+ m_dist = ( x - dx ) . abs + ( y - dy ) . abs
93+ next if m_dist > max_distance
94+ db = distances_finish [ dy ] [ dx ]
95+
96+ cheat_time = da + m_dist + db
97+ savings = normal_time - cheat_time
98+ if savings > savings_count [ [ x , y , dx , dy ] ]
99+ savings_count [ savings ] += 1 if savings > 0
100+ end
101+ end
102+ end
103+ end
104+
105+ # puts savings_count.inspect
106+ savings_count
107+ end
108+
109+ def neighbours ( x , y )
110+ [ [ x -1 , y ] , [ x +1 , y ] , [ x , y -1 ] , [ x , y +1 ] ] . select { |nx , ny | nx >= 0 && nx < @cols && ny >= 0 && ny < @rows }
111+ end
112+
113+ def bfs_distances ( start )
114+ distances = Array . new ( @rows ) { Array . new ( @cols , Float ::INFINITY ) }
115+ distances [ start [ 1 ] ] [ start [ 0 ] ] = 0
116+ queue = [ start ]
117+
118+ head = 0
119+ while head < queue . size
120+ x , y = queue [ head ]
121+ head += 1
122+
123+ neighbours ( x , y ) . each do |dx , dy |
124+ next if @grid [ dy ] [ dx ] == '#'
125+ next if distances [ dy ] [ dx ] < Float ::INFINITY
126+
127+ distances [ dy ] [ dx ] = distances [ y ] [ x ] + 1
128+ queue << [ dx , dy ]
129+ end
130+ end
131+
132+ distances
133+ end
134+
135+ def can_move? ( x , y )
136+ return false if x < 0 || x >= @cols
137+ return false if y < 0 || y >= @rows
138+ return false if @grid [ y ] [ x ] == '#'
139+ true
140+ end
141+
142+
143+ def find_cell ( ch )
144+ @grid . each_with_index do |row , y |
145+ x = row . index ( ch )
146+ if x
147+ return [ x , y ]
148+ end
149+ end
150+ nil
151+ end
12152 end
13153
14154 class DayOne
0 commit comments