@@ -26,6 +26,69 @@ def solve(part: int, data: str) -> int:
2626 if k .isalpha ():
2727 maps ["." ] |= v
2828
29+ if part == 1 :
30+ return p1 (maps )
31+ if part == 2 :
32+ return p2 (maps )
33+
34+
35+ def p2 (maps ):
36+ points = ["S" ] + sorted (i for i in maps if i .isalpha () and i != "S" ) + ["S" ]
37+ deltas = {p : - 1 for p in maps ["." ]} | {p : - 2 for p in maps ["-" ]} | {p : 1 for p in maps ["+" ]}
38+
39+ def is_cluster (start ):
40+ x , y = start
41+
42+ clumps = [
43+ [(x , y ), (x + 1 , y ), (x , y + 1 ), (x + 1 , y + 1 )],
44+ [(x , y ), (x - 1 , y ), (x , y + 1 ), (x - 1 , y + 1 )],
45+ [(x , y ), (x + 1 , y ), (x , y - 1 ), (x + 1 , y - 1 )],
46+ [(x , y ), (x - 1 , y ), (x , y - 1 ), (x - 1 , y - 1 )],
47+ ]
48+ return any (all (i in maps ["+" ] for i in clump ) for clump in clumps )
49+
50+ logging .debug ("Build clusters" )
51+ clusters = {i for i in maps ["+" ] if is_cluster (i )}
52+ logging .debug ("Done building clusters. Found %d positions." , len (clusters ))
53+
54+ legs = {}
55+
56+ for start , end in zip (points , points [1 :]):
57+ logging .debug (f"Compute routes { start } -{ end } " )
58+ dest = maps [end ].copy ().pop ()
59+ pos = maps [start ].copy ().pop ()
60+ best = {pos : (0 , False , frozenset ())}
61+ todo = {(pos , False )}
62+ for steps in range (1 , 500 ):
63+ next_todo = set ()
64+ for position , hit_cluster in todo :
65+ seen = best [position ][2 ]
66+ next_seen = frozenset (seen | {position })
67+ for d in DIRECTIONS :
68+ next_pos = (position [0 ] + d [0 ], position [1 ] + d [1 ])
69+ if next_pos not in deltas or next_pos in seen :
70+ continue
71+ next_altitude = best [position ][0 ] + deltas [next_pos ]
72+ next_hit = hit_cluster or next_pos in clusters
73+ if next_pos not in best or (position not in best [next_pos ][2 ] and (next_altitude > best [next_pos ][0 ] or (next_altitude == best [next_pos ][0 ] and next_hit ))):
74+ best [next_pos ] = (next_altitude , next_hit , next_seen )
75+ next_todo .add ((next_pos , next_hit ))
76+ todo = next_todo
77+ if not todo :
78+ break
79+ legs [end ] = (steps ,) + best [dest ][:2 ]
80+
81+ log (f"{ legs = } " )
82+ steps = sum (i [0 ] for i in legs .values ())
83+ delta = - min (sum (i [1 ] for i in legs .values ()), 0 )
84+ log (f"{ steps = } , { delta = } " )
85+ # assert any(i[2] for i in legs.values()) or delta == 0
86+ if delta % 2 :
87+ delta += 1
88+ return steps + delta
89+
90+
91+ def p1 (maps ):
2992 def is_cluster (start ):
3093 todo = {
3194 ((start [0 ] + x , start [1 ] + y ), (x , y ))
@@ -45,112 +108,33 @@ def is_cluster(start):
45108 todo .add ((next_pos , (x , y )))
46109 return False
47110
48- log ("Build clusters" )
111+ logging . debug ("Build clusters" )
49112 clusters = {i for i in maps ["+" ] if is_cluster (i )}
50- log ("Done building clusters. Found %d positions." , len (clusters ))
113+ logging . debug ("Done building clusters. Found %d positions." , len (clusters ))
51114
52-
53- if part == 1 :
54- return p1 (maps , clusters )
55- if part == 2 :
56- return p2 (maps , clusters )
57-
58-
59- def manhattan (a , b ):
60- return abs (a [0 ] - b [0 ]) + abs (a [1 ] - b [1 ])
61-
62-
63- def p2 (maps , clusters ):
64- points = ["S" ] + sorted (i for i in maps if i .isalpha () and i != "S" ) + ["S" ]
65- deltas = {p : - 1 for p in maps ["." ]} | {p : - 2 for p in maps ["-" ]} | {p : 1 for p in maps ["+" ]}
66-
67- legs = {}
68- improvements = collections .defaultdict (dict )
69-
70- for start , end in zip (points , points [1 :]):
71- log (f"Compute routes { start } -{ end } " )
72- dest = maps [end ].copy ().pop ()
73- pos = maps [start ].copy ().pop ()
74- q = queue .PriorityQueue ()
75- seen = {(pos , direction ) for direction in DIRECTIONS }
76- min_step_count = None
77- max_altitude = None
78- for direction in DIRECTIONS :
79- # score (manhattan + steps), steps, altitude, pos, direction
80- q .put ((manhattan (pos , dest ), 0 , 0 , pos , direction ))
81- while not q .empty ():
82- _ , altitude , steps , position , direction = q .get ()
83- if position == dest :
84- if min_step_count is None :
85- min_step_count = steps
86- max_altitude = altitude
87- else :
88- if steps == min_step_count :
89- max_altitude = max (max_altitude , altitude )
90- else :
91- improvements [end ][steps - min_step_count ] = max (improvements .get (steps - min_step_count , 0 ), altitude - max_altitude )
92- continue
93-
94- if q .qsize () > 10_000_000 :
95- break
96-
97- steps += 1
98- if min_step_count and steps > min_step_count + 10 :
99- break
100- for d in DIRECTIONS - {(- direction [0 ], - direction [1 ])}:
101- next_pos = (position [0 ] + d [0 ], position [1 ] + d [1 ])
102- if (next_pos , d ) in seen or next_pos not in deltas :
103- continue
104- next_altitude = altitude + deltas [next_pos ]
105- q .put ((manhattan (next_pos , dest ) + steps , next_altitude , steps , next_pos , d ))
106- legs [end ] = (min_step_count , max_altitude )
107- improvements [end ][0 ] = 0
108-
109- steps = sum (i [0 ] for i in legs .values ())
110- delta = sum (i [1 ] for i in legs .values ())
111- log (f"{ steps = } , { delta = } , { improvements = } " )
112-
113- if delta >= 0 :
114- return steps
115- delta = - delta
116- can_do = set ()
117- for modifications in itertools .product (* [((k , v ) for k , v in a .items () if k == 0 or v != 0 ) for a in improvements .values ()]):
118- extra_steps = sum (i [0 ] for i in modifications )
119- extra_elevation = sum (i [1 ] for i in modifications )
120- if extra_elevation >= delta :
121- can_do .add (extra_steps )
122- return min (can_do ) + steps
123-
124-
125- def p1 (maps , clusters ):
126115 deltas = {p : - 1 for p in maps ["." ]} | {p : - 2 for p in maps ["-" ]} | {p : 1 for p in maps ["+" ]}
127-
128116 altitude = 1000
129- direction = (0 , 1 ) # arbitrary but ought to work in p1
130117 position = maps ["S" ].pop ()
131118
132- # score = -1 * (steps_remaining + altitude)
133- q = queue .PriorityQueue ()
134- q .put ((- (altitude + 100 ), altitude , position , direction , 100 ))
135- seen = {position }
136-
137- while not q .empty ():
138- score , altitude , position , direction , steps_left = q .get ()
139- if position in clusters :
140- return altitude + steps_left
141- if steps_left == 0 :
142- return altitude
143- next_steps_left = steps_left - 1
144- for d in DIRECTIONS - {(- direction [0 ], - direction [1 ])}:
145- next_pos = (position [0 ] + d [0 ], position [1 ] + d [1 ])
146- if next_pos in seen or next_pos not in deltas :
147- continue
148- if clusters :
149- seen .add (next_pos )
150- next_altitude = altitude + deltas [next_pos ]
151- if next_altitude < 0 :
152- continue
153- q .put ((- (next_altitude + next_steps_left ), next_altitude , next_pos , d , next_steps_left ))
119+ best = {position : 0 }
120+ seen = set ()
121+ todo = {position }
122+ for steps in range (100 ):
123+ next_todo = set ()
124+ for position in todo :
125+ for d in DIRECTIONS :
126+ next_pos = (position [0 ] + d [0 ], position [1 ] + d [1 ])
127+ if next_pos in seen or next_pos not in deltas :
128+ continue
129+ next_altitude = best [position ] + deltas [next_pos ]
130+ next_todo .add (next_pos )
131+ if next_pos not in best or next_altitude > best [next_pos ]:
132+ best [next_pos ] = next_altitude
133+ seen |= next_todo
134+ todo = next_todo
135+ found = todo & clusters
136+ if found :
137+ return max (best [i ] for i in found ) + 100 - steps + 1000
154138
155139
156140TEST_DATA = [
@@ -212,7 +196,7 @@ def p1(maps, clusters):
212196]
213197TESTS = [
214198 # (1, TEST_DATA[0], 1045),
215- # (2, TEST_DATA[1], 24),
199+ (2 , TEST_DATA [1 ], 24 ),
216200 (2 , TEST_DATA [2 ], 78 ),
217201 (2 , TEST_DATA [3 ], 206 ),
218202 # (3, TEST_DATA[2], None),
0 commit comments