1
+ import abc
1
2
from dataclasses import dataclass , field
2
3
from heapq import heappop , heappush
3
4
import time
4
5
from typing import Dict , List , Set , Tuple
5
6
7
+ def assign_or_modify (d : dict , key , value ):
8
+ if key not in d :
9
+ d [key ] = value
10
+ else :
11
+ d [key ] += value
12
+
6
13
class Amphipod :
7
14
cost = { 'A' : 1 , 'B' : 10 , 'C' : 100 , 'D' : 1000 }
8
15
@@ -53,54 +60,56 @@ def get_next(self, amphipod: Amphipod, pos: Tuple):
53
60
break
54
61
return State (amphipods )
55
62
56
- def assign_or_modify (d : dict , key , value ):
57
- if key not in d :
58
- d [key ] = value
59
- else :
60
- d [key ] += value
61
-
62
63
@dataclass (order = True )
63
64
class Move :
64
65
score : int
65
- state : State = field (compare = False )
66
-
66
+ state : State = field (compare = False )
67
67
68
- class Map :
68
+ class Map ( metaclass = abc . ABCMeta ) :
69
69
SHIFTS = [(0 , 1 ), (1 , 0 ), (- 1 , 0 ), (0 , - 1 )]
70
+ ENTRANCE = [(3 ,1 ), (5 ,1 ), (7 ,1 ), (9 ,1 )]
70
71
71
72
def __init__ (self , map : List [str ]) -> None :
72
- self .score = 30000
73
73
self .map = map
74
- self .rooms = {
75
- 'A' : [(3 ,2 ), (3 ,3 )],
76
- 'B' : [(5 ,2 ), (5 ,3 )],
77
- 'C' : [(7 ,2 ), (7 ,3 )],
78
- 'D' : [(9 ,2 ), (9 ,3 )]}
74
+ self .rooms : Dict [str , List [Tuple ]] = self ._define_rooms ()
75
+ self .initial_state : State = self ._define_initial_state ()
76
+ self .final_state : State = self ._define_final_state ()
77
+
78
+ def _define_initial_state (self ):
79
+ amphipods = []
80
+ for y in range (len (self .map )):
81
+ for x in range (len (self .map [0 ])):
82
+ if self .map [y ][x ] != '.' and self .map [y ][x ] != '#' :
83
+ amphipods .append (Amphipod (self .map [y ][x ], (x , y )))
84
+ return State (amphipods )
79
85
80
- dst_1 = []
86
+ def _define_final_state (self ) -> State :
87
+ dst = []
81
88
for key , values in self .rooms .items ():
82
89
for v in values :
83
- dst_1 .append (Amphipod (key , v ))
84
- self .final_1 = State (dst_1 )
85
- self .entrance = [(3 ,1 ), (5 ,1 ), (7 ,1 ), (9 ,1 )]
86
- amphipods = []
87
- for y in range (len (map )):
88
- for x in range (len (map [0 ])):
89
- if map [y ][x ] != '.' and map [y ][x ] != '#' :
90
- amphipods .append (Amphipod (map [y ][x ], (x , y )))
91
- self .initial_state = State (amphipods )
92
-
93
- def completed_1 (self , state : State ):
94
- return state .hash == self .final_1 .hash
90
+ dst .append (Amphipod (key , v ))
91
+ return State (dst )
92
+
93
+ @abc .abstractmethod
94
+ def _define_rooms (self ) -> Dict [str , List [Tuple ]]:
95
+ pass
96
+
97
+ @abc .abstractmethod
98
+ def allowed_moves (self , amph : Amphipod , state : State ) -> List [Tuple ]:
99
+ pass
95
100
101
+
102
+ def is_final (self , state : State ) -> bool :
103
+ return state .hash == self .final_state .hash
104
+
96
105
def occupied_by (self , pos : Tuple , state : State ):
97
106
if self .map [pos [1 ]][pos [0 ]] == '#' :
98
107
return None
99
108
for amph in state .amphipods :
100
109
if amph .pos == pos :
101
110
return amph
102
111
return None
103
-
112
+
104
113
def dump_state (self , state : State ):
105
114
img = []
106
115
for row in self .map :
@@ -111,9 +120,8 @@ def dump_state(self, state: State):
111
120
img [amph .pos [1 ]][amph .pos [0 ]] = amph .name [0 ]
112
121
for row in img :
113
122
print ('' .join (row ))
114
-
115
-
116
- def dfs (self , src : tuple , dst : tuple , state : State ):
123
+
124
+ def distance (self , src : tuple , dst : tuple , state : State ):
117
125
vis = set ()
118
126
vis .add (src )
119
127
que = [(src , 0 )]
@@ -130,9 +138,47 @@ def dfs(self, src: tuple, dst: tuple, state: State):
130
138
vis .add (next )
131
139
que .append ((next , length + 1 ))
132
140
return None
141
+
142
+ def search (self ):
143
+ opened = []
144
+ initial = self .initial_state
145
+ heappush (opened , Move (0 , initial ))
146
+ vis = {
147
+ initial .hash : 0 # state - score
148
+ }
133
149
150
+ while len (opened ) > 0 :
151
+ top = heappop (opened )
152
+ score = top .score
153
+ state = top .state
154
+
155
+ if self .is_final (state ):
156
+ return score
157
+
158
+ for amph in state .amphipods :
159
+ if amph .moved >= 2 :
160
+ continue
161
+ for (length , dst ) in self .allowed_moves (amph , state ):
162
+ cost = length * amph .cost
163
+ next_state = state .get_next (amph , dst )
164
+ if (next_state .hash not in vis
165
+ or vis [next_state .hash ] > score + cost
166
+ ):
167
+ assign_or_modify (vis , next_state .hash , score + cost )
168
+ heappush (opened , Move (score + cost , next_state ))
169
+ return None
170
+
171
+ class MapPart_1 (Map ):
134
172
135
- def allowed_moves (self , amph : Amphipod , state : State ):
173
+ def _define_rooms (self ) -> Dict [str , List [Tuple ]]:
174
+ rooms = {
175
+ 'A' : [(3 ,2 ), (3 ,3 )],
176
+ 'B' : [(5 ,2 ), (5 ,3 )],
177
+ 'C' : [(7 ,2 ), (7 ,3 )],
178
+ 'D' : [(9 ,2 ), (9 ,3 )] }
179
+ return rooms
180
+
181
+ def allowed_moves (self , amph : Amphipod , state : State ) -> List [Tuple ]:
136
182
allowed : List [Tuple ] = []
137
183
rooms = self .rooms [amph .name ]
138
184
rooms_count = len (rooms )
@@ -152,8 +198,8 @@ def allowed_moves(self, amph: Amphipod, state: State):
152
198
# otherwise add to hall
153
199
for x in range (1 , len (self .map [0 ]) - 1 ):
154
200
dst = (x , 1 )
155
- if dst not in self . entrance :
156
- length = self .dfs (amph .pos , dst , state )
201
+ if dst not in Map . ENTRANCE :
202
+ length = self .distance (amph .pos , dst , state )
157
203
if length is not None :
158
204
allowed .append ((length , dst ))
159
205
@@ -165,55 +211,96 @@ def allowed_moves(self, amph: Amphipod, state: State):
165
211
other = self .occupied_by (rooms [1 ], state )
166
212
if other is None :
167
213
for i in range (2 ):
168
- length = self .dfs (amph .pos , rooms [i ], state )
214
+ length = self .distance (amph .pos , rooms [i ], state )
169
215
if length != None :
170
216
allowed .append ((length , rooms [i ]))
171
217
elif other .name == amph .name :
172
- length = self .dfs (amph .pos , rooms [0 ], state )
218
+ length = self .distance (amph .pos , rooms [0 ], state )
173
219
if length != None :
174
220
allowed .append ((length , rooms [0 ]))
175
221
return allowed
176
222
177
- def search (self ):
178
- opened = []
179
- initial = self .initial_state
180
- heappush (opened , Move (0 , initial ))
181
- vis = {
182
- initial .hash : 0 # state - score
183
- }
223
+ class MapPart_2 (Map ):
184
224
185
- while len (opened ) > 0 :
186
- top = heappop (opened )
187
- score = top .score
188
- state = top .state
189
- if len (opened ) % 30000 == 0 :
190
- print (len (vis ), len (opened ), score )
191
- self .dump_state (state )
192
-
193
- if self .completed_1 (state ):
194
- return score
225
+ def _define_rooms (self ) -> Dict [str , List [Tuple ]]:
226
+ rooms = {
227
+ 'A' : [(3 ,2 ), (3 ,3 ), (3 ,4 ), (3 ,5 )],
228
+ 'B' : [(5 ,2 ), (5 ,3 ), (5 ,4 ), (5 ,5 )],
229
+ 'C' : [(7 ,2 ), (7 ,3 ), (7 ,4 ), (7 ,5 )],
230
+ 'D' : [(9 ,2 ), (9 ,3 ), (9 ,4 ), (9 ,5 )] }
231
+ return rooms
195
232
196
- for amph in state .amphipods :
197
- if amph .moved >= 2 :
198
- continue
199
- for (length , dst ) in self .allowed_moves (amph , state ):
200
- cost = length * amph .cost
201
- if score + cost > 18500 :
202
- continue
203
- next_state = state .get_next (amph , dst )
204
- if (next_state .hash not in vis
205
- or vis [next_state .hash ] > score + cost
206
- ):
207
- assign_or_modify (vis , next_state .hash , score + cost )
208
- heappush (opened , Move (score + cost , next_state ))
209
- return None
233
+ def allowed_moves (self , amph : Amphipod , state : State ) -> List [Tuple ]:
234
+ allowed : List [Tuple ] = []
235
+ rooms = self .rooms [amph .name ]
236
+ rooms_count = len (rooms )
237
+ assert rooms_count == 4
238
+
239
+ if amph .pos [1 ] != 1 :
240
+ # in room
241
+ # do I even need to move?
242
+ at_room = None
243
+ for i in range (rooms_count ):
244
+ if rooms [i ] == amph .pos :
245
+ at_room = i
246
+ break
247
+ if at_room != None :
248
+ # already at one of possible destination
249
+ need_to_move = False
250
+ for i in range (at_room + 1 , rooms_count ):
251
+ occupant = self .occupied_by (rooms [i ], state )
252
+ if occupant == None or occupant .name != amph .name :
253
+ need_to_move = True
254
+ break
255
+ if need_to_move == False :
256
+ # no need to move, already at it's place
257
+ return allowed
258
+ # otherwise add to hall
259
+ for x in range (1 , len (self .map [0 ]) - 1 ):
260
+ dst = (x , 1 )
261
+ if dst not in Map .ENTRANCE :
262
+ length = self .distance (amph .pos , dst , state )
263
+ if length is not None :
264
+ allowed .append ((length , dst ))
265
+ else :
266
+ # from hall to room
267
+ length = self .distance (amph .pos , rooms [0 ], state )
268
+ if length == None :
269
+ # can't reach room
270
+ return []
271
+ first_occupied_room_index = None
272
+ for i in range (1 , rooms_count ):
273
+ occupant = self .occupied_by (rooms [i ], state )
274
+ if occupant != None and occupant .name != amph .name :
275
+ return []
276
+ if occupant != None :
277
+ first_occupied_room_index = i
278
+ break
279
+ if first_occupied_room_index != None :
280
+ # confirm that there is no other occupant with other name
281
+ # or free space
282
+ for i in range (first_occupied_room_index + 1 , rooms_count ):
283
+ occupant = self .occupied_by (rooms [i ], state )
284
+ if occupant == None or occupant .name != amph .name :
285
+ return []
286
+ else :
287
+ first_occupied_room_index = rooms_count
288
+ # add empty rooms
289
+ for i in range (first_occupied_room_index ):
290
+ allowed .append ((length + i , rooms [i ]))
291
+ return allowed
210
292
211
293
def part_1 (raw ):
212
- map = Map (raw )
213
- return map .search ()
294
+ # convert part 2 input to part 1 inpot
295
+ map : List [str ] = []
296
+ for i in range (len (raw )):
297
+ if i == 3 or i == 4 :
298
+ continue
299
+ map .append (raw [i ])
300
+ return MapPart_1 (map ).search ()
214
301
215
302
def part_2 (raw ):
216
- pass
303
+ return MapPart_2 ( raw ). search ()
217
304
218
305
raw = []
219
306
with open ("../input/day23.txt" ) as istream :
@@ -224,4 +311,5 @@ def part_2(raw):
224
311
# takes 280.8341495990753s
225
312
print ('Part_1: {}, takes {}s' .format (part_1 (raw ), time .time () - begin ))
226
313
begin = time .time ()
314
+ # takes 141.15754175186157s
227
315
print ('Part_2: {}, takes {}s' .format (part_2 (raw ), time .time () - begin ))
0 commit comments