Skip to content

Commit fa1a816

Browse files
Merge pull request #27 from marcelblijleven/2021_day_12
solution: add 2021 day 12
2 parents 7067865 + 18b29b7 commit fa1a816

File tree

3 files changed

+270
-0
lines changed

3 files changed

+270
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
vp-BY
2+
ui-oo
3+
kk-IY
4+
ij-vp
5+
oo-start
6+
SP-ij
7+
kg-uj
8+
ij-UH
9+
SP-end
10+
oo-IY
11+
SP-kk
12+
SP-vp
13+
ui-ij
14+
UH-ui
15+
ij-IY
16+
start-ui
17+
IY-ui
18+
uj-ui
19+
kk-oo
20+
IY-start
21+
end-vp
22+
uj-UH
23+
ij-kk
24+
UH-end
25+
UH-kk
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from collections import defaultdict
2+
from typing import List, Set, DefaultDict
3+
4+
from adventofcode.util.console import console
5+
from adventofcode.util.exceptions import SolutionNotFoundException
6+
from adventofcode.util.helpers import solution_timer
7+
from adventofcode.util.input_helpers import get_input_for_day
8+
9+
PathDict = DefaultDict[str, list[str]]
10+
11+
12+
def is_big_cave(cave: str) -> bool:
13+
return cave.upper() == cave
14+
15+
16+
def is_small_cave(cave: str) -> bool:
17+
return cave.lower() == cave
18+
19+
20+
def default_dict_factory() -> List[str]:
21+
return []
22+
23+
24+
def get_paths(input_data: List[str]) -> PathDict:
25+
path_dict: PathDict = defaultdict(default_dict_factory)
26+
27+
for line in input_data:
28+
a, b = line.split('-')
29+
path_dict[a].append(b)
30+
path_dict[b].append(a)
31+
32+
return path_dict
33+
34+
35+
class CaveExplorer3000:
36+
def __init__(self, paths: PathDict, limit_small_caves: bool = False) -> None:
37+
self.paths = paths
38+
self.limit_small_caves = limit_small_caves
39+
self.path_count = 0
40+
41+
def traverse(self, cave: str, visited: Set[str]) -> int:
42+
if cave == 'end':
43+
return 1
44+
elif cave == 'start' and cave in visited:
45+
return 0
46+
47+
if cave in visited and is_small_cave(cave):
48+
return 0
49+
50+
visited.add(cave)
51+
paths = self.paths[cave]
52+
return sum(self.traverse(path, visited.copy()) for path in paths)
53+
54+
def traverse_part_two(self, cave: str, visited: Set[str], revisit=True) -> int:
55+
if cave == 'end':
56+
return 1
57+
elif cave == 'start' and cave in visited:
58+
return 0
59+
60+
if cave in visited and is_small_cave(cave):
61+
if not revisit:
62+
return 0
63+
64+
revisit = False
65+
66+
visited.add(cave)
67+
paths = self.paths[cave]
68+
return sum(self.traverse_part_two(path, visited.copy(), revisit) for path in paths)
69+
70+
def traverse_with_print(self, cave: str, visited: Set[str], path_flow: str = '') -> int:
71+
if path_flow == '':
72+
path_flow = f'{cave}'
73+
else:
74+
path_flow += f' -> {cave}'
75+
76+
if cave == 'end':
77+
console.print(path_flow)
78+
return 1
79+
elif cave == 'start' and cave in visited:
80+
return 0
81+
82+
if cave in visited and is_small_cave(cave):
83+
return 0
84+
85+
visited.add(cave)
86+
paths = self.paths[cave]
87+
return sum(self.traverse_with_print(path, visited.copy(), path_flow) for path in paths)
88+
89+
90+
@solution_timer(2021, 12, 1)
91+
def part_one(input_data: List[str]):
92+
path_dict = get_paths(input_data)
93+
cave_explorer = CaveExplorer3000(path_dict)
94+
answer = cave_explorer.traverse('start', set())
95+
96+
if not answer:
97+
raise SolutionNotFoundException(2021, 12, 1)
98+
99+
return answer
100+
101+
102+
@solution_timer(2021, 12, 2)
103+
def part_two(input_data: List[str]):
104+
path_dict = get_paths(input_data)
105+
cave_explorer = CaveExplorer3000(path_dict, limit_small_caves=True)
106+
answer = cave_explorer.traverse_part_two('start', set())
107+
108+
if not answer:
109+
raise SolutionNotFoundException(2021, 12, 2)
110+
111+
return answer
112+
113+
114+
if __name__ == '__main__':
115+
data = get_input_for_day(2021, 12)
116+
part_one(data)
117+
part_two(data)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import pytest
2+
import pytest_mock
3+
4+
from adventofcode.year_2021.day_12_2021 import part_two, part_one, is_big_cave, is_small_cave, get_paths, \
5+
CaveExplorer3000
6+
7+
test_input = [
8+
'start-A',
9+
'start-b',
10+
'A-c',
11+
'A-b',
12+
'b-d',
13+
'A-end',
14+
'b-end',
15+
]
16+
17+
test_input_slightly_larger = [
18+
'dc-end',
19+
'HN-start',
20+
'start-kj',
21+
'dc-start',
22+
'dc-HN',
23+
'LN-dc',
24+
'HN-end',
25+
'kj-sa',
26+
'kj-HN',
27+
'kj-dc',
28+
]
29+
30+
test_input_even_larger = [
31+
'fs-end',
32+
'he-DX',
33+
'fs-he',
34+
'start-DX',
35+
'pj-DX',
36+
'end-zg',
37+
'zg-sl',
38+
'zg-pj',
39+
'pj-he',
40+
'RW-he',
41+
'fs-DX',
42+
'pj-RW',
43+
'zg-RW',
44+
'start-pj',
45+
'he-WI',
46+
'zg-he',
47+
'pj-fs',
48+
'start-RW',
49+
]
50+
51+
52+
@pytest.mark.parametrize(['cave', 'expected'], [
53+
('A', True),
54+
('AB', True),
55+
('a', False),
56+
('ab', False),
57+
('Ab', False),
58+
])
59+
def test_is_big_cave(cave, expected):
60+
assert is_big_cave(cave) == expected
61+
62+
63+
@pytest.mark.parametrize(['cave', 'expected'], [
64+
('A', False),
65+
('AB', False),
66+
('a', True),
67+
('ab', True),
68+
('Ab', False),
69+
])
70+
def test_is_small_cave(cave, expected):
71+
assert is_small_cave(cave) == expected
72+
73+
74+
def test_get_paths():
75+
input_data = [
76+
'start-A',
77+
'start-b',
78+
'A-c',
79+
'A-b',
80+
'b-d',
81+
'A-end',
82+
'b-end',
83+
]
84+
85+
assert dict(get_paths(input_data)) == {'A': ['start', 'c', 'b', 'end'],
86+
'b': ['start', 'A', 'd', 'end'],
87+
'c': ['A'],
88+
'd': ['b'],
89+
'end': ['A', 'b'],
90+
'start': ['A', 'b']}
91+
92+
93+
@pytest.mark.parametrize(['input_data', 'expected'], [
94+
(test_input, 10),
95+
(test_input_slightly_larger, 19),
96+
(test_input_even_larger, 226)
97+
])
98+
def test_cave_explorer_traverse(input_data, expected):
99+
paths = get_paths(input_data)
100+
cave_explorer = CaveExplorer3000(paths)
101+
assert cave_explorer.traverse('start', set()) == expected
102+
103+
104+
@pytest.mark.parametrize(['input_data', 'expected'], [
105+
(test_input, 36),
106+
(test_input_slightly_larger, 103),
107+
(test_input_even_larger, 3509)
108+
])
109+
def test_cave_explorer_traverse_part_two(input_data, expected):
110+
paths = get_paths(input_data)
111+
cave_explorer = CaveExplorer3000(paths, limit_small_caves=True)
112+
assert cave_explorer.traverse_part_two('start', set()) == expected
113+
114+
115+
def test_cave_explorer_traverse_with_print(mocker: pytest_mock.MockerFixture):
116+
mock_console = mocker.patch('adventofcode.year_2021.day_12_2021.console')
117+
paths = get_paths(test_input)
118+
cave_explorer = CaveExplorer3000(paths)
119+
assert cave_explorer.traverse_with_print('start', set()) == 10
120+
mock_console.print.assert_called()
121+
122+
123+
def test_part_one():
124+
assert part_one(test_input) == 10
125+
126+
127+
def test_part_two():
128+
assert part_two(test_input) == 36

0 commit comments

Comments
 (0)