Skip to content

Commit 8c275b9

Browse files
committed
Added first iterations on day 2021-23
1 parent 2ad007e commit 8c275b9

File tree

5 files changed

+7137
-0
lines changed

5 files changed

+7137
-0
lines changed

2021/23-Amphipod.v1.py

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
# -------------------------------- Input data ---------------------------------------- #
2+
import os, grid, graph, dot, assembly, re, itertools, copy, functools
3+
from collections import Counter, deque, defaultdict
4+
from functools import reduce
5+
import heapq
6+
7+
from compass import *
8+
9+
10+
# This functions come from https://github.com/mcpower/adventofcode - Thanks!
11+
def lmap(func, *iterables):
12+
return list(map(func, *iterables))
13+
14+
15+
def ints(s: str):
16+
return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano!
17+
18+
19+
def positive_ints(s: str):
20+
return lmap(int, re.findall(r"\d+", s)) # thanks mserrano!
21+
22+
23+
def floats(s: str):
24+
return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s))
25+
26+
27+
def positive_floats(s: str):
28+
return lmap(float, re.findall(r"\d+(?:\.\d+)?", s))
29+
30+
31+
def words(s: str):
32+
return re.findall(r"[a-zA-Z]+", s)
33+
34+
35+
test_data = {}
36+
37+
test = 1
38+
test_data[test] = {
39+
"input": """#############
40+
#...........#
41+
###B#C#B#D###
42+
#A#D#C#A#
43+
#########""",
44+
"expected": ["12521", "Unknown"],
45+
}
46+
47+
test = "real"
48+
input_file = os.path.join(
49+
os.path.dirname(__file__),
50+
"Inputs",
51+
os.path.basename(__file__).replace(".py", ".txt"),
52+
)
53+
test_data[test] = {
54+
"input": open(input_file, "r+").read(),
55+
"expected": ["Unknown", "Unknown"],
56+
}
57+
58+
59+
# -------------------------------- Control program execution ------------------------- #
60+
61+
case_to_test = 1
62+
part_to_test = 1
63+
64+
# -------------------------------- Initialize some variables ------------------------- #
65+
66+
puzzle_input = test_data[case_to_test]["input"]
67+
puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1]
68+
puzzle_actual_result = "Unknown"
69+
70+
71+
# This was the very first attempt to solve it
72+
# It tries to parse the input, the run A* on it to find possible movements
73+
# Basically it's wayyy too slow and buggy
74+
75+
76+
# -------------------------------- Actual code execution ----------------------------- #
77+
78+
dot.Dot.sort_value = dot.Dot.sorting_map["xy"]
79+
80+
81+
class NewGrid(grid.Grid):
82+
def text_to_dots(self, text, ignore_terrain="", convert_to_int=False):
83+
self.dots = {}
84+
85+
y = 0
86+
self.amphipods = {}
87+
self.position_to_rooms = []
88+
nb_amphipods = []
89+
for line in text.splitlines():
90+
for x in range(len(line)):
91+
if line[x] not in ignore_terrain:
92+
value = line[x]
93+
position = x - y * 1j
94+
95+
if value == " ":
96+
continue
97+
98+
if value in "ABCD":
99+
self.position_to_rooms.append(position)
100+
if value in nb_amphipods:
101+
UUID = value + "2"
102+
else:
103+
UUID = value + "1"
104+
nb_amphipods.append(value)
105+
self.amphipods[UUID] = dot.Dot(self, position, value)
106+
107+
value = "."
108+
109+
self.dots[position] = dot.Dot(self, position, value)
110+
# self.dots[position].sort_value = self.dots[position].sorting_map['xy']
111+
if value == ".":
112+
self.dots[position].is_waypoint = True
113+
y += 1
114+
115+
116+
class StateGraph(graph.WeightedGraph):
117+
amphipod_state = ["A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2"]
118+
119+
def a_star_search(self, start, end=None):
120+
"""
121+
Performs a A* search
122+
123+
This algorithm is appropriate for "One source, multiple targets"
124+
It takes into account positive weigths / costs of travelling.
125+
Negative weights will make the algorithm fail.
126+
127+
The exploration path is a mix of Dijkstra and Greedy BFS
128+
It uses the current cost + estimated cost to determine the next element to consider
129+
130+
Some cases to consider:
131+
- If Estimated cost to complete = 0, A* = Dijkstra
132+
- If Estimated cost to complete <= actual cost to complete, it is exact
133+
- If Estimated cost to complete > actual cost to complete, it is inexact
134+
- If Estimated cost to complete = infinity, A* = Greedy BFS
135+
The higher Estimated cost to complete, the faster it goes
136+
137+
:param Any start: The start vertex to consider
138+
:param Any end: The target/end vertex to consider
139+
:return: True when the end vertex is found, False otherwise
140+
"""
141+
current_distance = 0
142+
frontier = [(0, start, 0)]
143+
heapq.heapify(frontier)
144+
self.distance_from_start = {start: 0}
145+
self.came_from = {start: None}
146+
self.visited = [tuple(dot.position for dot in start)]
147+
148+
i = 0
149+
while frontier: # and i < 5:
150+
i += 1
151+
priority, vertex, current_distance = heapq.heappop(frontier)
152+
print(len(frontier), priority, current_distance)
153+
154+
neighbors = self.neighbors(vertex)
155+
if not neighbors:
156+
continue
157+
158+
for neighbor, weight in neighbors.items():
159+
# We've already checked that node, and it's not better now
160+
if neighbor in self.distance_from_start and self.distance_from_start[
161+
neighbor
162+
] <= (current_distance + weight):
163+
continue
164+
165+
if any(
166+
equivalent_position in self.visited
167+
for equivalent_position in self.equivalent_positions(neighbor)
168+
):
169+
continue
170+
171+
# Adding for future examination
172+
priority = current_distance + self.estimate_to_complete(neighbor, end)
173+
# print (vertex, neighbor, current_distance, priority)
174+
heapq.heappush(
175+
frontier, (priority, neighbor, current_distance + weight)
176+
)
177+
178+
# Adding for final search
179+
self.distance_from_start[neighbor] = current_distance + weight
180+
self.came_from[neighbor] = vertex
181+
self.visited.append(tuple(dot.position for dot in neighbor))
182+
183+
if self.state_is_final(neighbor):
184+
return self.distance_from_start[neighbor]
185+
186+
# print (len(frontier))
187+
188+
return end in self.distance_from_start
189+
190+
def neighbors(self, state):
191+
if self.state_is_final(state):
192+
return None
193+
194+
neighbors = {}
195+
for i, current_dot in enumerate(state):
196+
amphipod_code = self.amphipod_state[i]
197+
dots = self.area_graph.edges[current_dot]
198+
for dot, cost in dots.items():
199+
new_state = list(state)
200+
new_state[i] = dot
201+
new_state = tuple(new_state)
202+
# print ('Checking', amphipod_code, 'moved from', state[i], 'to', new_state[i])
203+
if self.state_is_valid(state, new_state, i):
204+
neighbors[new_state] = (
205+
cost * self.amphipods[amphipod_code].movement_cost
206+
)
207+
# print ('Movement costs', cost * self.amphipods[amphipod_code].movement_cost)
208+
209+
return neighbors
210+
211+
def state_is_final(self, state):
212+
for i, position in enumerate(state):
213+
amphipod_code = self.amphipod_state[i]
214+
amphipod = self.amphipods[amphipod_code]
215+
216+
if not position in self.room_to_positions[amphipod.terrain]:
217+
return False
218+
return True
219+
220+
def state_is_valid(self, state, new_state, changed):
221+
# Duplicate = 2 amphipods in the same place
222+
if len(set(new_state)) != len(new_state):
223+
# print ('Duplicate amphipod', new_state[changed])
224+
return False
225+
226+
# Check amphipod is not in wrong room
227+
if new_state[i].position in self.position_to_rooms:
228+
room = self.position_to_rooms[new_state[i].position]
229+
# print ('Amphipod may be in wrong place', new_state)
230+
amphipod = self.amphipod_state[i]
231+
if room == self.amphipods[amphipod].initial_room:
232+
return True
233+
else:
234+
# print ('Amphipod is in wrong place', new_state)
235+
return False
236+
237+
return True
238+
239+
def estimate_to_complete(self, state, target_vertex):
240+
distance = 0
241+
for i, dot in enumerate(state):
242+
amphipod_code = self.amphipod_state[i]
243+
amphipod = self.amphipods[amphipod_code]
244+
245+
if not dot.position in self.room_to_positions[amphipod.terrain]:
246+
room_positions = self.room_to_positions[amphipod.terrain]
247+
targets = [self.dots[position] for position in room_positions]
248+
distance += (
249+
min(
250+
self.area_graph.all_edges[dot][target]
251+
if target in self.area_graph.all_edges[dot]
252+
else 10 ** 6
253+
for target in targets
254+
)
255+
* amphipod.movement_cost
256+
)
257+
258+
return distance
259+
260+
def equivalent_positions(self, state):
261+
state_positions = [dot.position for dot in state]
262+
positions = [
263+
tuple([state_positions[1]] + [state_positions[0]] + state_positions[2:]),
264+
tuple(
265+
state_positions[0:2]
266+
+ [state_positions[3]]
267+
+ [state_positions[2]]
268+
+ state_positions[4:]
269+
),
270+
tuple(
271+
state_positions[0:4]
272+
+ [state_positions[5]]
273+
+ [state_positions[4]]
274+
+ state_positions[6:]
275+
),
276+
tuple(state_positions[0:6] + [state_positions[7]] + [state_positions[6]]),
277+
]
278+
279+
for i in range(4):
280+
position = tuple(
281+
state_positions[:i]
282+
+ state_positions[i + 1 : i]
283+
+ state_positions[i + 2 :]
284+
)
285+
positions.append(position)
286+
287+
return positions
288+
289+
290+
if part_to_test == 1:
291+
area_map = NewGrid()
292+
area_map.text_to_dots(puzzle_input)
293+
294+
position_to_rooms = defaultdict(list)
295+
room_to_positions = defaultdict(list)
296+
area_map.position_to_rooms = sorted(
297+
area_map.position_to_rooms, key=lambda a: (a.real, a.imag)
298+
)
299+
for i in range(4):
300+
position_to_rooms[area_map.position_to_rooms[2 * i]] = "ABCD"[i]
301+
position_to_rooms[area_map.position_to_rooms[2 * i + 1]] = "ABCD"[i]
302+
room_to_positions["ABCD"[i]].append(area_map.position_to_rooms[2 * i])
303+
room_to_positions["ABCD"[i]].append(area_map.position_to_rooms[2 * i + 1])
304+
# Forbid to use the dot right outside the room
305+
area_map.dots[area_map.position_to_rooms[2 * i + 1] + 1j].is_waypoint = False
306+
area_map.position_to_rooms = position_to_rooms
307+
area_map.room_to_positions = room_to_positions
308+
309+
# print (list(dot for dot in area_map.dots if area_map.dots[dot].is_waypoint))
310+
311+
for amphipod in area_map.amphipods:
312+
area_map.amphipods[amphipod].initial_room = area_map.position_to_rooms[
313+
area_map.amphipods[amphipod].position
314+
]
315+
area_map.amphipods[amphipod].movement_cost = 10 ** (
316+
ord(area_map.amphipods[amphipod].terrain) - ord("A")
317+
)
318+
319+
area_graph = area_map.convert_to_graph()
320+
area_graph.all_edges = area_graph.edges
321+
area_graph.edges = {
322+
dot: {
323+
neighbor: distance
324+
for neighbor, distance in area_graph.edges[dot].items()
325+
if distance <= 2
326+
}
327+
for dot in area_graph.vertices
328+
}
329+
print(len(area_graph.all_edges))
330+
331+
# print (area_graph.vertices)
332+
# print (area_graph.edges)
333+
334+
state_graph = StateGraph()
335+
state_graph.area_graph = area_graph
336+
state_graph.amphipods = area_map.amphipods
337+
state_graph.position_to_rooms = area_map.position_to_rooms
338+
state_graph.room_to_positions = area_map.room_to_positions
339+
state_graph.dots = area_map.dots
340+
341+
state = tuple(
342+
area_map.dots[area_map.amphipods[amphipod].position]
343+
for amphipod in sorted(area_map.amphipods.keys())
344+
)
345+
# print ('area_map.amphipods', area_map.amphipods)
346+
347+
print("state", state)
348+
# print ('equivalent', state_graph.equivalent_positions(state))
349+
print("estimate", state_graph.estimate_to_complete(state, None))
350+
351+
print(state_graph.a_star_search(state))
352+
353+
# In the example, A is already in the right place
354+
# In all other cases, 1 anphipod per group has to go to the bottom, so 1 move per amphipod
355+
356+
357+
else:
358+
for string in puzzle_input.split("\n"):
359+
if string == "":
360+
continue
361+
362+
363+
# -------------------------------- Outputs / results --------------------------------- #
364+
365+
print("Case :", case_to_test, "- Part", part_to_test)
366+
print("Expected result : " + str(puzzle_expected_result))
367+
print("Actual result : " + str(puzzle_actual_result))
368+
# Date created: 2021-12-23 08:11:43.693421

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy