Skip to content

Commit 414a7c6

Browse files
committed
solve day 23 part 2
1 parent 8d7f5e6 commit 414a7c6

File tree

2 files changed

+161
-71
lines changed

2 files changed

+161
-71
lines changed

input/day23.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#############
22
#...........#
33
###B#C#C#B###
4+
###D#C#B#A###
5+
###D#B#A#C###
46
###D#D#A#A###
57
#############

src/day23.py

+159-71
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
import abc
12
from dataclasses import dataclass, field
23
from heapq import heappop, heappush
34
import time
45
from typing import Dict, List, Set, Tuple
56

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+
613
class Amphipod:
714
cost = { 'A': 1, 'B': 10, 'C': 100, 'D': 1000 }
815

@@ -53,54 +60,56 @@ def get_next(self, amphipod: Amphipod, pos: Tuple):
5360
break
5461
return State(amphipods)
5562

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-
6263
@dataclass(order = True)
6364
class Move:
6465
score: int
65-
state: State = field(compare=False)
66-
66+
state: State = field(compare = False)
6767

68-
class Map:
68+
class Map(metaclass = abc.ABCMeta):
6969
SHIFTS = [(0, 1), (1, 0), (-1, 0), (0, -1)]
70+
ENTRANCE = [(3,1), (5,1), (7,1), (9,1)]
7071

7172
def __init__(self, map: List[str]) -> None:
72-
self.score = 30000
7373
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)
7985

80-
dst_1 = []
86+
def _define_final_state(self) -> State:
87+
dst = []
8188
for key, values in self.rooms.items():
8289
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
95100

101+
102+
def is_final(self, state: State) -> bool:
103+
return state.hash == self.final_state.hash
104+
96105
def occupied_by(self, pos: Tuple, state: State):
97106
if self.map[pos[1]][pos[0]] == '#':
98107
return None
99108
for amph in state.amphipods:
100109
if amph.pos == pos:
101110
return amph
102111
return None
103-
112+
104113
def dump_state(self, state: State):
105114
img = []
106115
for row in self.map:
@@ -111,9 +120,8 @@ def dump_state(self, state: State):
111120
img[amph.pos[1]][amph.pos[0]] = amph.name[0]
112121
for row in img:
113122
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):
117125
vis = set()
118126
vis.add(src)
119127
que = [(src, 0)]
@@ -130,9 +138,47 @@ def dfs(self, src: tuple, dst: tuple, state: State):
130138
vis.add(next)
131139
que.append((next, length + 1))
132140
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+
}
133149

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):
134172

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]:
136182
allowed: List[Tuple] = []
137183
rooms = self.rooms[amph.name]
138184
rooms_count = len(rooms)
@@ -152,8 +198,8 @@ def allowed_moves(self, amph: Amphipod, state: State):
152198
# otherwise add to hall
153199
for x in range(1, len(self.map[0]) - 1):
154200
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)
157203
if length is not None:
158204
allowed.append((length, dst))
159205

@@ -165,55 +211,96 @@ def allowed_moves(self, amph: Amphipod, state: State):
165211
other = self.occupied_by(rooms[1], state)
166212
if other is None:
167213
for i in range(2):
168-
length = self.dfs(amph.pos, rooms[i], state)
214+
length = self.distance(amph.pos, rooms[i], state)
169215
if length != None:
170216
allowed.append((length, rooms[i]))
171217
elif other.name == amph.name:
172-
length = self.dfs(amph.pos, rooms[0], state)
218+
length = self.distance(amph.pos, rooms[0], state)
173219
if length != None:
174220
allowed.append((length, rooms[0]))
175221
return allowed
176222

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):
184224

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
195232

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
210292

211293
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()
214301

215302
def part_2(raw):
216-
pass
303+
return MapPart_2(raw).search()
217304

218305
raw = []
219306
with open("../input/day23.txt") as istream:
@@ -224,4 +311,5 @@ def part_2(raw):
224311
# takes 280.8341495990753s
225312
print('Part_1: {}, takes {}s'.format(part_1(raw), time.time() - begin))
226313
begin = time.time()
314+
# takes 141.15754175186157s
227315
print('Part_2: {}, takes {}s'.format(part_2(raw), time.time() - begin))

0 commit comments

Comments
 (0)