Skip to content

Commit 31ad1fa

Browse files
Play around with some graph algos
1 parent cba2dda commit 31ad1fa

File tree

7 files changed

+190
-223
lines changed

7 files changed

+190
-223
lines changed

2015/22/solver.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import heapq
12
from dataclasses import dataclass
23
from typing import Optional, Self
34

4-
from aoc import answer, search
5+
from aoc import answer
56
from aoc.parser import Parser
67

78

@@ -11,6 +12,9 @@ class Stats:
1112
attack: int = 0
1213
armor: int = 0
1314

15+
def dead(self) -> bool:
16+
return self.hp <= 0
17+
1418
def add(self, other: Self) -> Self:
1519
return type(self)(
1620
hp=self.hp + other.hp,
@@ -81,7 +85,7 @@ class Game:
8185
spells: frozenset[Spell]
8286

8387
def get_moves(self) -> list[tuple[int, Self]]:
84-
if self.player.hp <= 0:
88+
if self.player.dead():
8589
return []
8690
return [
8791
(spell.effect().cost, self.move(spell))
@@ -144,11 +148,23 @@ def play_game(hp: int, attack: int, damage: int) -> Optional[int]:
144148
damage=Stats(hp=damage),
145149
spells=frozenset(),
146150
)
147-
return search.bfs_complete(
148-
(0, game),
149-
lambda current: current.enemy.hp <= 0,
150-
lambda current: current.get_moves(),
151-
)
151+
return dijkstra(game)
152+
153+
154+
def dijkstra(start: Game) -> Optional[int]:
155+
queue: list[tuple[int, Game]] = [(0, start)]
156+
seen: set[Game] = set()
157+
while len(queue) > 0:
158+
mana_used, game = heapq.heappop(queue)
159+
if game in seen:
160+
continue
161+
seen.add(game)
162+
if game.enemy.dead():
163+
return mana_used
164+
for cost, next_game in game.get_moves():
165+
if next_game not in seen:
166+
heapq.heappush(queue, (mana_used + cost, next_game))
167+
return None
152168

153169

154170
if __name__ == "__main__":

2016/02/solver.py

+44-41
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,58 @@
11
from aoc import answer
22
from aoc.grid import Grid
33
from aoc.parser import Parser
4-
from aoc.point import Direction, Point, PointHelper
4+
from aoc.point import Point
5+
6+
DIRECTIONS: dict[str, Point] = dict(
7+
U=(0, -1),
8+
D=(0, 1),
9+
L=(-1, 0),
10+
R=(1, 0),
11+
)
512

613

714
@answer.timer
815
def main() -> None:
9-
answer.part1("47978", get_code([[7, 8, 9], [4, 5, 6], [1, 2, 3]]))
10-
answer.part2(
11-
"659AD",
12-
get_code(
13-
[
14-
["*", "*", "D", "*", "*"],
15-
["*", "A", "B", "C", "*"],
16-
[5, 6, 7, 8, 9],
17-
["*", 2, 3, 4, "*"],
18-
["*", "*", 1, "*", "*"],
19-
]
20-
),
21-
)
22-
23-
24-
def get_code(pattern: list[list[int | str]]) -> str:
25-
phone, position = create_phone(pattern)
26-
code = ""
27-
for instruction in Parser().lines():
28-
position = follow(phone, position, instruction)
29-
code += str(phone[position])
30-
return code
31-
32-
33-
def create_phone(pattern: list[list[int | str]]) -> tuple[Grid, Point]:
34-
phone = dict()
35-
start = None
16+
instructions: list[str] = Parser().lines()
17+
keypad_1: list[list[str]] = [
18+
["1", "2", "3"],
19+
["4", "5", "6"],
20+
["7", "8", "9"],
21+
]
22+
answer.part1("47978", get_code(instructions, keypad_1))
23+
keypad_2: list[list[str]] = [
24+
["*", "*", "1", "*", "*"],
25+
["*", "2", "3", "4", "*"],
26+
["5", "6", "7", "8", "9"],
27+
["*", "A", "B", "C", "*"],
28+
["*", "*", "D", "*", "*"],
29+
]
30+
answer.part2("659AD", get_code(instructions, keypad_2))
31+
32+
33+
def get_code(instructions: list[str], keypad: list[list[str]]) -> str:
34+
phone: Grid[str] = create_phone(keypad)
35+
position: Point = {digit: location for location, digit in phone.items()}["5"]
36+
37+
code: list[str] = []
38+
for instruction in instructions:
39+
for direction in instruction:
40+
dx, dy = DIRECTIONS[direction]
41+
new_position: Point = (position[0] + dx, position[1] + dy)
42+
if new_position in phone:
43+
position = new_position
44+
code.append(phone[position])
45+
return "".join(code)
46+
47+
48+
def create_phone(pattern: list[list[str]]) -> Grid[str]:
49+
phone: Grid[str] = dict()
3650
for y, row in enumerate(pattern):
3751
for x, value in enumerate(row):
38-
point = (x, y)
52+
point: Point = (x, y)
3953
if value != "*":
4054
phone[point] = value
41-
if value == 5:
42-
start = point
43-
assert start is not None
44-
return phone, start
45-
46-
47-
def follow(phone: Grid, position: Point, instruction: str) -> Point:
48-
for direction in instruction:
49-
new_position = PointHelper.go(position, Direction.from_str(direction))
50-
if new_position in phone:
51-
position = new_position
52-
return position
55+
return phone
5356

5457

5558
if __name__ == "__main__":

2016/13/solver.py

+21-8
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,20 @@
88

99
@dataclass(frozen=True)
1010
class Maze:
11-
grid: Grid
11+
grid: Grid[bool]
1212
favorite_number: int
1313

1414
def get_adjacent(self, point: Point) -> list[Point]:
15-
result = set()
15+
result: set[Point] = set()
1616
for adjacent in PointHelper.neighbors(point):
1717
if adjacent[0] >= 0 and adjacent[1] >= 0:
1818
if adjacent not in self.grid:
19-
self.grid[adjacent] = self.is_wall(adjacent)
19+
self.grid[adjacent] = self.is_wall(*adjacent)
2020
if not self.grid[adjacent]:
2121
result.add(adjacent)
2222
return list(result)
2323

24-
def is_wall(self, point: Point) -> bool:
25-
x, y = point
24+
def is_wall(self, x: int, y: int) -> bool:
2625
value = (x * x) + (3 * x) + (2 * x * y) + y + y * y
2726
value += self.favorite_number
2827
value = bin(value)[2:]
@@ -33,9 +32,23 @@ def is_wall(self, point: Point) -> bool:
3332
@answer.timer
3433
def main() -> None:
3534
maze = Maze(grid=dict(), favorite_number=Parser().integer())
36-
start, goal = (1, 1), (31, 39)
37-
answer.part1(92, search.bfs(start, goal, maze.get_adjacent))
38-
answer.part2(124, len(search.reachable(start, 50, maze.get_adjacent)))
35+
start: Point = (1, 1)
36+
answer.part1(92, search.bfs(start, (31, 39), maze.get_adjacent))
37+
answer.part2(124, len(reachable(start, 50, maze)))
38+
39+
40+
def reachable(start: Point, maximum: int, maze: Maze) -> set[Point]:
41+
queue: list[tuple[int, Point]] = [(0, start)]
42+
seen: set[Point] = set()
43+
while len(queue) > 0:
44+
length, position = queue.pop(0)
45+
if position in seen:
46+
continue
47+
seen.add(position)
48+
for adjacent in maze.get_adjacent(position):
49+
if adjacent not in seen and length < maximum:
50+
queue.append((length + 1, adjacent))
51+
return seen
3952

4053

4154
if __name__ == "__main__":

2016/17/solver.py

+36-32
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,50 @@
11
import hashlib
22

3-
from aoc import answer, search
3+
from aoc import answer
44
from aoc.parser import Parser
5-
from aoc.point import Direction, Point, PointHelper
5+
from aoc.point import Point
66

7-
DIRECTIONS: list[str] = ["U", "D", "L", "R"]
7+
DIRECTIONS: list[tuple[str, Point]] = [
8+
("U", (0, 1)),
9+
("D", (0, -1)),
10+
("L", (-1, 0)),
11+
("R", (1, 0)),
12+
]
13+
14+
LEGAL_HASH: list[str] = ["b", "c", "d", "e", "f"]
815

916

1017
@answer.timer
1118
def main() -> None:
12-
code = Parser().string()
13-
paths = search.bfs_paths(((-3, 3), code), (0, 0), get_adjacent)
14-
answer.part1("DDRLRRUDDR", pull_path(code, paths[0]))
15-
answer.part2(556, len(pull_path(code, paths[-1])))
16-
17-
18-
def get_adjacent(item: tuple[Point, str]) -> list[tuple[Point, str]]:
19-
point, code = item
20-
hashed = hash(code)
19+
code: str = Parser().string()
20+
paths: list[str] = bfs(code, (-3, 3), (0, 0))
21+
answer.part1("DDRLRRUDDR", paths[0])
22+
answer.part2(556, len(paths[-1]))
23+
24+
25+
def bfs(code: str, start: Point, end: Point) -> list[str]:
26+
queue: list[tuple[Point, str]] = [(start, "")]
27+
paths: list[str] = []
28+
while len(queue) > 0:
29+
point, path = queue.pop(0)
30+
if point == end:
31+
paths.append(path)
32+
else:
33+
for adjacent in get_adjacent(code, point, path):
34+
queue.append(adjacent)
35+
return paths
36+
37+
38+
def get_adjacent(code: str, point: Point, path: str) -> list[tuple[Point, str]]:
39+
hashed: str = hashlib.md5(str.encode(code + path)).hexdigest()
2140
result: list[tuple[Point, str]] = []
22-
for i, name in enumerate(DIRECTIONS):
23-
next_point = PointHelper.go(point, Direction.from_str(name))
24-
if is_legal(next_point) and unlocked(hashed[i]):
25-
result.append((next_point, code + name))
41+
for i, (symbol, direction) in enumerate(DIRECTIONS):
42+
x, y = (point[0] + direction[0], point[1] + direction[1])
43+
in_bounds: bool = x >= -3 and x <= 0 and y <= 3 and y >= 0
44+
if in_bounds and hashed[i] in LEGAL_HASH:
45+
result.append(((x, y), path + symbol))
2646
return result
2747

2848

29-
def is_legal(p: Point) -> bool:
30-
return p[0] >= -3 and p[0] <= 0 and p[1] <= 3 and p[1] >= 0
31-
32-
33-
def unlocked(value: str) -> bool:
34-
return value in ["b", "c", "d", "e", "f"]
35-
36-
37-
def hash(value: str) -> str:
38-
return hashlib.md5(str.encode(value)).hexdigest()[:4]
39-
40-
41-
def pull_path(code: str, value: str) -> str:
42-
return value[len(code) :]
43-
44-
4549
if __name__ == "__main__":
4650
main()

2017/12/solver.py

+33-15
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,58 @@
11
from collections import defaultdict
2+
from dataclasses import dataclass
23
from typing import Optional
34

4-
from aoc import answer, search
5+
from aoc import answer
56
from aoc.parser import Parser
67

78

9+
@dataclass(frozen=True)
10+
class Graph:
11+
graph: dict[str, set[str]]
12+
13+
def connected(self, start: str) -> set[str]:
14+
queue: list[str] = [start]
15+
seen: set[str] = set()
16+
while len(queue) > 0:
17+
current = queue.pop()
18+
if current in seen:
19+
continue
20+
seen.add(current)
21+
for adjacent in self.graph[current]:
22+
if adjacent not in seen:
23+
queue.append(adjacent)
24+
return seen
25+
26+
def get_ungrouped(self, grouped: set[str]) -> Optional[str]:
27+
all_keys: set[str] = set(self.graph.keys())
28+
remainder: set[str] = all_keys - grouped
29+
return None if len(remainder) == 0 else next(iter(remainder))
30+
31+
832
@answer.timer
933
def main() -> None:
1034
graph = get_graph()
11-
connected_to_0 = search.connected(graph, "0")
35+
36+
connected_to_0: set[str] = graph.connected("0")
1237
answer.part1(306, len(connected_to_0))
1338

1439
heads: set[str] = set(["0"])
15-
grouped = connected_to_0
16-
head = get_ungrouped(graph, grouped)
40+
grouped: set[str] = connected_to_0
41+
head: Optional[str] = graph.get_ungrouped(grouped)
1742
while head is not None:
1843
heads.add(head)
19-
connected = search.connected(graph, head)
20-
grouped |= connected
21-
head = get_ungrouped(graph, grouped)
44+
grouped |= graph.connected(head)
45+
head = graph.get_ungrouped(grouped)
2246
answer.part2(200, len(heads))
2347

2448

25-
def get_graph() -> dict[str, set[str]]:
49+
def get_graph() -> Graph:
2650
graph: dict[str, set[str]] = defaultdict(set)
2751
for line in Parser().lines():
2852
start, ends = line.split(" <-> ")
2953
for end in ends.split(", "):
3054
graph[start].add(end)
31-
return graph
32-
33-
34-
def get_ungrouped(graph: dict[str, set[str]], grouped: set[str]) -> Optional[str]:
35-
all_keys = set(graph.keys())
36-
remainder = all_keys - grouped
37-
return None if len(remainder) == 0 else next(iter(remainder))
55+
return Graph(graph=graph)
3856

3957

4058
if __name__ == "__main__":

0 commit comments

Comments
 (0)