Skip to content

Commit d8109bc

Browse files
committed
feat: aoc 2022 day 12 solution + tests
1 parent 62fa42c commit d8109bc

File tree

5 files changed

+312
-0
lines changed

5 files changed

+312
-0
lines changed

_2022/data/day12/puzzle_input.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
abcccccccccccccccccaaaaaaaaccccccacccaaccccccccccccccccccaaaaaaaaaacccccccccccccccccccccccccccccccaaaaaaccccccccccccccccccccccccccccccccccaaaaa
2+
abccccccccccccccccccaaaaacccccccaaaaaaacccccccccccaaccaaaaaaaaaaaaaccccccccccccccccccccccccccccccccaaaaacccccccccccccccccccccccccccccccccaaaaaa
3+
abccccccccccccaaccccaaaaaacccccccaaaaaaaaccccccacaaaccaaaaaaaaaaaaaaaccccccccccccccccccaaacccccccaaaaaaaccccccccccccccccaaaccccccccccccccaaaaaa
4+
abccccccccacccaaccccaaaaaacccccccaaaaaaaaaccccaaaaaaaaacaaaaaaaaaaaaacccccccccccccccccccaacccccccaaaaaaaacccccccccccccccaaaccccccccccccccaccaaa
5+
abaacccccaaaaaaaccccaaaccacccccccaaaaaaaaaccccaaaaaaaaccccaaaaaaaaaaaccccccccccccccccaacaaaaaccccaaaaaaaacccccccccccccccaaacccccccccccccccccaaa
6+
abaaccccccaaaaaaaacccccccccccccccaaaaaaaaccccccaaaaaacccccaaaacaaaaccccccccccccccccccaaaaaaaaccccccaaacaccccccccccccccccaaakccaaaccccccccccccaa
7+
abaaacccccaaaaaaaaaccccccccccccccaaaaaaacccccccaaaaaccccccaaaccaaaaccccccccccccaacacccaaaaaccccccccaaacccccccccccccacacckkkkkkkaacccccccccccccc
8+
abaaacccccaaaaaaaaaccccccccccccccaccaaaaaccccccaaaaaacccccaaacaaaccccccccccccccaaaaccccaaaaacccccccccccccccccccccccaaaakkkkkkkkkacccaaaccaccccc
9+
abacacccccaaaaaaaccccccccccccccccccccaaaaaaaccccccaaccccccaaaaaaaaccccccccccccaaaaacccaaacaacccccccccccccccccccccccaajkkkkppkkkkccccaaaaaaccccc
10+
abacccccccaaaaaaacccccccccccccccccccaaaaaaaaccccccccccccccccaaaaaaccccccccccccaaaaaacccaacccccccccccccccccccccccccccjjkkooppppkllccccaaaaaccccc
11+
abccccccccaccaaaccccccccccccccccccccaaaaaaaacccccccccccccccccaaaaaccccccccccccacaaaacccccccccccccccccccccccccccccjjjjjjoooppppklllcacaaaaaccccc
12+
abcccaacccccccaaacccccccccccccccccccaaaaaaacccccccccccccccccaaaaacccccccccccccccaacaccccccccccccccccccccccccccjjjjjjjjoooopuppplllcccccaaaacccc
13+
abcccaacccccccccccccccccaaacccccccccccaaaaaaccccccaaaaacccccaaaaaccccccccccccaaacaaacccccaaaccccccccccccccccijjjjjjjjooouuuuuppllllcccccaaacccc
14+
abaaaaaaaaccccccccccccccaaaaccccccccccaacaaaccccccaaaaaccccccccccccccccccccccaaaaaaacccccaaacacccccccccccccciijjoooooooouuuuuppplllllccccaccccc
15+
abaaaaaaaaccccccccccccccaaaaccccccccccaacccccccccaaaaaacccccccccccccccccccccccaaaaaacccaaaaaaaacccccccccccciiiqqooooooouuuxuuuppplllllccccccccc
16+
abccaaaaccccccccccccccccaaaccccccccccccccccccccccaaaaaacccccccccccccccccccccccaaaaaaaccaaaaaaaacccccccccccciiiqqqqtttuuuuxxxuupppqqllllmccccccc
17+
abcaaaaacccaaaccccccccccccccccccccccccaccccccccccaaaaaacccccccccccccccccccccaaaaaaaaaaccaaaaaaccccccccccccciiiqqqtttttuuuxxxuuvpqqqqmmmmccccccc
18+
abcaacaaaccaaacaaccccccccccccccccccccaaaacaaaccccccaacccaaaaacccccccccccccccaaaaaaaaaacccaaaaacccaaaccccccciiiqqttttxxxxxxxyuvvvvqqqqmmmmcccccc
19+
abcacccaaccaaaaaaccccccccccccccccccccaaaaaaaacccccccccccaaaaacccccccccccccccaaacaaacccccaaaaaaccaaaacccccaaiiiqqtttxxxxxxxxyyvvvvvvqqqmmmdddccc
20+
abcccccccaaaaaaaccccccccccccccccccccccaaaaaaaaacccccccccaaaaaaccccccccccccccccccaaaccccccaacccccaaaacccaaaaiiiqqqttxxxxxxxyyyyyyvvvqqqmmmdddccc
21+
SbccccccccaaaaaccccccccaacaaccccccccaaaaaaaaaaccccccccccaaaaaaccccccccccccaaacccaaccccccccccccccaaaacccaaaaaiiiqqtttxxxxEzzyyyyvvvvqqqmmmdddccc
22+
abaccccccccaaaaacccccccaaaaacccccccaaaaaaaaaaaccccccccccaaaaaaccccccccccaaaaaacccccccccccccccccccccccccaaaaaiiiqqqtttxxxyyyyyyvvvvqqqmmmdddcccc
23+
abaacccccccaacaaaccccccaaaaaacccccccaaaaaaaaaaccccccccccccaaacccccccccccaaaaaaccccccccccccccccccccccccccaaaahhhqqqqttxxyyyyyyvvvvqqqmmmddddcccc
24+
abaccccccccaaccccccccccaaaaaacccaacaaccaaaaaaaaaccccccccccccccccccccccccaaaaaaccccccccccccccccccccccccccaaaachhhqqtttxwyyyyyywvrqqqmmmmdddccccc
25+
abaaaccccccccccccccccccaaaaaacccaaaaaccaaaaacaaaccccccccccccccccccccccccaaaaaccccaaaaccccaaaccccccccccccccccchhhppttwwwywwyyywwrrrnmmmdddcccccc
26+
abaaaccccccccccccccccccccaaaccccaaaaaacaaaaaaaaaccccccccaaacccccccccccccaaaaaccccaaaaccccaaaccccccccccccccccchhpppsswwwwwwwwywwrrrnnndddccccccc
27+
abaaacccccccccccccccccccccccccccaaaaaacccaaaaaacccccccccaaaaacccccaacccccccccccccaaaacaaaaaaaaccccccccccccccchhpppsswwwwsswwwwwrrrnneeddccccccc
28+
abaccccccccaaaacccccccccccccccccaaaaaaccccaaaaaaaacccccaaaaaaccaacaaacccccccccccccaaccaaaaaaaaccccccccccccccchhpppssssssssrwwwwrrrnneeecaaccccc
29+
abaccccccccaaaacccccccccccccccccccaaaccccaaaaaaaaacccccaaaaaaccaaaaaccccccccccccccccccccaaaaacccccccccccccccchhpppssssssssrrrwrrrnnneeeaaaccccc
30+
abcccccccccaaaacccccccccccccccccccccccccaaaaaaaaaaccccccaaaaacccaaaaaacccccccccccccccccaaaaaacccccccccccccccchhpppppsssooorrrrrrrnnneeeaaaccccc
31+
abcccccccccaaaccccccccccccccccccccccccccaaacaaacccccccccaacaacaaaaaaaacccccccccccccccccaaaaaacaaccccccccccccchhhppppppoooooorrrrnnneeeaaaaacccc
32+
abccccccccccccccccccccccccccccccccccccccccccaaaccaaaacccccccccaaaaacaaccccaacccccccccacaaaaaacaaccccccccccccchhhgpppppoooooooonnnnneeeaaaaacccc
33+
abcccccccaacccccccccccccccccccccccccccccccccaaacaaaaaccccccccccacaaaccccccaacccccccccaacaaaaaaaaaaacccccaaccccgggggggggggfooooonnneeeeaaaaacccc
34+
abcccccccaaacaaccccccccccccaacccccccccccccccccccaaaaaaccccaacccccaaacccaaaaaaaaccccccaaaaacaaaaaaaaccccaaacccccggggggggggfffooonneeeecaaacccccc
35+
abcccccccaaaaaaccccaacccccaaacccccccccccccccccccaaaaaaccccaaaccccccccccaaaaaaaacccccccaaaaaccaaaaccccaaaaaaaacccggggggggfffffffffeeeecaaccccccc
36+
abcccccaaaaaaaccaaaaacaaaaaaacccccccccccccccccccaaaaacccccaaaacccaaccccccaaaacccccccaaaaaaaacaaaaacccaaaaaaaaccccccccccaaaffffffffecccccccccccc
37+
abcaaacaaaaaaacccaaaaaaaaaaaaaaaccccccccccccccccccaaacccccaaaacaaaacaacccaaaaaccccccaaaaaaaaaaaaaaccccaaaaaacccccccccccaaacaafffffccccccccccaaa
38+
abaaaacccaaaaaaccaaaaacaaaaaaaaaccccccccccccaaacccccccccccaaaaaaaaacaaccaaacaacccccccccaacccaaccaaccccaaaaaaccccccccccaaaaccaaacccccccccccccaaa
39+
abaaaacccaacaaacaaaaacccaaaaaaacccccccccccccaaaacccccccccaaaaaaaaaaaaaccaacccacccccccccaacccccccccccccaaaaaaccccccccccaaacccccccccccccccccccaaa
40+
abcaaacccaacccccccaaaccaaaaaacccccccccccccccaaaaccccccaaaaaaaaaaaaaaaaaaccccccccccccccccccccccccccccccaaccaaccccccccccaaaccccccccccccccccaaaaaa
41+
abcccccccccccccccccccccaaaaaaaccccccccccccccaaacccccccaaaaaaaaaaaaaaaaaacccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccaaaaaa

_2022/solutions/day12.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"""Day 12: Hill Climbing Algorithm
2+
3+
This module provides the solution for Advent of Code 2022 - Day 12.
4+
5+
It implements pathfinding on a heightmap grid to find the shortest route
6+
from low elevation to high elevation with movement constraints.
7+
8+
The module contains a Coordinate class for position tracking and a Solution
9+
class that inherits from SolutionBase for solving the hill climbing puzzle.
10+
"""
11+
12+
from collections import deque
13+
from typing import ClassVar
14+
15+
from aoc.models.base import SolutionBase
16+
17+
18+
class Coordinate:
19+
"""Represent a 2D grid position with x (column) and y (row) coordinates.
20+
21+
Uses standard (x, y) convention where x is horizontal and y is vertical.
22+
Provides equality comparison and hashing for use in sets and dictionaries
23+
during pathfinding operations.
24+
"""
25+
26+
def __init__(self, x: int, y: int):
27+
"""Initialize coordinate with column and row position.
28+
29+
Args:
30+
x: Column index (horizontal position)
31+
y: Row index (vertical position)
32+
"""
33+
self.x = x
34+
self.y = y
35+
36+
def __repr__(self) -> str:
37+
"""Return string representation of coordinate."""
38+
return f"Coordinate(x={self.x}, y={self.y})"
39+
40+
def __eq__(self, other: object) -> bool:
41+
"""Check equality based on x and y values."""
42+
if not isinstance(other, Coordinate):
43+
return NotImplemented
44+
45+
return self.x == other.x and self.y == other.y
46+
47+
def __hash__(self) -> int:
48+
"""Generate hash for use in sets and dictionaries."""
49+
return hash((self.x, self.y))
50+
51+
52+
class Solution(SolutionBase):
53+
"""Find shortest path on heightmap with elevation climbing constraints.
54+
55+
This solution implements breadth-first search (BFS) to find the shortest
56+
path on a grid where each cell has an elevation (a-z). Part 1 finds the
57+
shortest path from a single start point (S) to the end point (E). Part 2
58+
finds the shortest path from any low elevation point ('a' or 'S') to the end.
59+
60+
Movement is constrained: you can only move to adjacent cells (up, down, left,
61+
right) if the destination elevation is at most 1 higher than current elevation.
62+
"""
63+
64+
DIRECTIONS: ClassVar[list[tuple[int, int]]] = [
65+
(1, 0), # right
66+
(0, -1), # up
67+
(-1, 0), # left
68+
(0, 1), # down
69+
]
70+
71+
def find_start_end(self, grid: list[list[str]]) -> tuple[Coordinate, Coordinate]:
72+
"""Locate start (S) and end (E) positions in the heightmap.
73+
74+
Args:
75+
grid: 2D grid of elevation characters
76+
77+
Returns
78+
-------
79+
tuple[Coordinate, Coordinate]: Start and end coordinates
80+
81+
Raises
82+
------
83+
ValueError: If start or end position cannot be found
84+
"""
85+
start: Coordinate | None = None
86+
end: Coordinate | None = None
87+
88+
for y, row in enumerate(grid):
89+
for x, cell in enumerate(row):
90+
if cell == "S":
91+
start = Coordinate(x, y)
92+
93+
elif cell == "E":
94+
end = Coordinate(x, y)
95+
96+
if start and end:
97+
return start, end
98+
99+
if not start or not end:
100+
err_msg = "Could not find start or end position"
101+
raise ValueError(err_msg)
102+
103+
return start, end
104+
105+
def get_height(self, grid: list[list[str]], coord: Coordinate) -> int:
106+
"""Get numeric elevation value for a coordinate.
107+
108+
Converts characters to elevation values: 'a'=97, 'z'=122.
109+
Special cases: 'S' is treated as 'a', 'E' is treated as 'z'.
110+
111+
Args:
112+
grid: 2D grid of elevation characters
113+
coord: Position to check
114+
115+
Returns
116+
-------
117+
int: ASCII value representing elevation (97-122)
118+
"""
119+
value = grid[coord.y][coord.x]
120+
if value == "S":
121+
return ord("a")
122+
123+
if value == "E":
124+
return ord("z")
125+
126+
return ord(value)
127+
128+
def bfs(self, grid: list[list[str]], start: Coordinate, end: Coordinate) -> int:
129+
"""Run breadth-first search from start to end coordinate.
130+
131+
Uses BFS to find the shortest path while respecting elevation constraints:
132+
can only move to cells that are at most 1 elevation higher.
133+
134+
Args:
135+
grid: 2D grid of elevation characters
136+
start: Starting coordinate
137+
end: Target coordinate
138+
139+
Returns
140+
-------
141+
int: Number of steps in shortest path, or -1 if no path exists
142+
"""
143+
n_rows, n_cols = len(grid), len(grid[0])
144+
145+
queue = deque([(start, 0)])
146+
visited = {start}
147+
148+
while queue:
149+
position, steps = queue.popleft()
150+
151+
if position == end:
152+
return steps
153+
154+
for dx, dy in self.DIRECTIONS:
155+
new_x = position.x + dx
156+
new_y = position.y + dy
157+
new_position = Coordinate(new_x, new_y)
158+
159+
if not (0 <= new_x < n_cols and 0 <= new_y < n_rows):
160+
continue
161+
162+
if new_position in visited:
163+
continue
164+
165+
current_height = self.get_height(grid, position)
166+
new_height = self.get_height(grid, new_position)
167+
if new_height > current_height + 1:
168+
continue
169+
170+
visited.add(new_position)
171+
queue.append((new_position, steps + 1))
172+
173+
return -1
174+
175+
def part1(self, data: list[str]) -> int:
176+
"""Find shortest path from marked start (S) to end (E).
177+
178+
Searches for the minimum number of steps needed to reach the best
179+
signal location (E) from the starting position (S) while respecting
180+
elevation climbing constraints.
181+
182+
Args:
183+
data: List of strings representing the heightmap grid
184+
185+
Returns
186+
-------
187+
int: Minimum number of steps from S to E
188+
"""
189+
grid = [list(row) for row in data]
190+
start, end = self.find_start_end(grid)
191+
192+
return self.bfs(grid, start, end)
193+
194+
def part2(self, data: list[str]) -> int:
195+
"""Find shortest path from any lowest elevation point to end (E).
196+
197+
Identifies the best hiking trail by finding the shortest path from
198+
any cell at elevation 'a' (including 'S') to the end point (E).
199+
Tests all possible low-elevation starting points.
200+
201+
Args:
202+
data: List of strings representing the heightmap grid
203+
204+
Returns
205+
-------
206+
int: Minimum number of steps from any 'a' elevation cell to E,
207+
or -1 if no valid path exists
208+
"""
209+
grid = [list(row) for row in data]
210+
_, end = self.find_start_end(grid)
211+
212+
starts = [
213+
Coordinate(x, y)
214+
for y, row in enumerate(grid)
215+
for x, cell in enumerate(row)
216+
if cell in ["a", "S"]
217+
]
218+
219+
valid_paths = [steps for start in starts if (steps := self.bfs(grid, start, end)) != -1]
220+
221+
return min(valid_paths) if valid_paths else -1
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Sabqponm
2+
abcryxxl
3+
accszExk
4+
acctuvwj
5+
abdefghi
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Sabqponm
2+
abcryxxl
3+
accszExk
4+
acctuvwj
5+
abdefghi

_2022/tests/test_12.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Test suite for Day 12: Hill Climbing Algorithm
2+
3+
This module contains tests for the Day 12 solution, which finds shortest paths
4+
on a heightmap with elevation climbing constraints. The tests verify:
5+
6+
1. Part 1: Finding shortest path from marked start (S) to end (E)
7+
2. Part 2: Finding shortest path from any low elevation point to end
8+
"""
9+
10+
from aoc.models.tester import TestSolutionUtility
11+
12+
13+
def test_day12_part1() -> None:
14+
"""Test finding shortest path from start marker to end.
15+
16+
This test runs the solution for Part 1 of the puzzle against the
17+
provided test input and compares the result with the expected output.
18+
"""
19+
TestSolutionUtility.run_test(
20+
year=2022,
21+
day=12,
22+
is_raw=False,
23+
part_num=1,
24+
expected=31,
25+
)
26+
27+
28+
def test_day12_part2() -> None:
29+
"""Test finding shortest path from any low elevation point.
30+
31+
This test runs the solution for Part 2 of the puzzle against the
32+
provided test input and compares the result with the expected output.
33+
"""
34+
TestSolutionUtility.run_test(
35+
year=2022,
36+
day=12,
37+
is_raw=False,
38+
part_num=2,
39+
expected=29,
40+
)

0 commit comments

Comments
 (0)