-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathday06.py
217 lines (165 loc) · 6.11 KB
/
day06.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
"""Day 6: Lanternfish."""
from __future__ import annotations
from collections import Counter
from functools import cache
from typing import Final, Optional
import numpy as np
from advent_of_code.checks import check_answer, check_example
from advent_of_code.cli_output import print_single_answer
from advent_of_code.data import get_data_path
DAY: Final[int] = 6
_ex_lanternfish_ages: Final[str] = "3,4,3,1,2"
class LanternFish:
"""A single lanternfish."""
age: int
def __init__(self, age: int = 8) -> None:
"""Create a new lanternfish.
Args:
age (int, optional): Age of the lanternfish. Defaults to 8.
"""
self.age = age
return None
def new_day(self) -> Optional[LanternFish]:
"""Adavance the age of the fish by one day.
Returns:
Optional[LanternFish]: If the lanternfish reproduced, its offspring is
returned.
"""
if self.age == 0:
self.age = 6
return LanternFish()
else:
self.age -= 1
return None
def __str__(self) -> str:
"""Human-readable description of the lanternfish."""
return str(self.age)
def __repr__(self) -> str:
"""Human-readable description of the lanternfish."""
return str(self)
class LanternFishPopulation:
"""Population of lanternfish."""
day: int
lanternfishes: list[LanternFish]
def __init__(self, lanternfishes: list[LanternFish]) -> None:
"""Create a population of lanternfish.
Args:
lanternfishes (list[LanternFish]): Population of lanternfish.
"""
self.day = 0
self.lanternfishes = lanternfishes
return None
def new_day(self) -> None:
"""Advance the population's age by one day."""
self.day += 1
new_fishes: list[LanternFish] = []
for fish in self.lanternfishes:
new_fish = fish.new_day()
if new_fish is not None:
new_fishes.append(new_fish)
self.lanternfishes += new_fishes
return None
def advance(self, days: int) -> None:
"""Advance the population by a set number of days.
Args:
days (int): Number of days.
"""
for _ in range(days):
self.new_day()
def __len__(self) -> int:
"""Size of the population."""
return len(self.lanternfishes)
def __str__(self) -> str:
"""Human-readable description of the population."""
return f"After {self.day:3d} days: {len(self)} fish"
def __repr__(self) -> str:
"""Human-readable description of the population."""
return str(self)
def show(self) -> None:
"""Display a population of fish ages."""
fishes = ", ".join([str(f) for f in self.lanternfishes])
print(f"After {self.day:3d} days: {fishes}")
# ---- Data parsing ---- #
def _parse_lanternfish_ages_str(age_str: str) -> list[LanternFish]:
return [LanternFish(int(x)) for x in age_str.strip().split(",")]
def _get_lanternfish_ages_data() -> LanternFishPopulation:
fish: list[LanternFish] = []
with open(get_data_path(DAY), "r") as file:
for line in file:
fish += _parse_lanternfish_ages_str(line)
return LanternFishPopulation(fish)
# ---- Part 2 ---- #
def _split_up_days(n_days: int, by: int) -> list[int]:
if n_days < by:
return [n_days]
split_days = [by] * (n_days // by)
rem = n_days % by
if rem > 0:
split_days += [rem]
return split_days
@cache
def age_fish(fish: int, days: int) -> np.ndarray:
"""Age a single fish (memoized).
Args:
fish (int): Fish to age.
days (int): Number of days to age the fish.
Returns:
np.ndarray: Resulting population of lanterfish.
"""
ary = np.asarray([fish], dtype=int)
for _ in range(days):
ary = ary - 1
idx = ary == -1
ary[idx] = 6
ary = np.hstack([ary, np.zeros(np.sum(idx), dtype=int) + 8])
return ary
def age_a_population(
population: LanternFishPopulation, days: int, day_split: int = 128
) -> int:
"""Age a population of lanternfish.
While this is not a readable as the nice abstraction above, it is **far** more
efficient and takes way less time to run for larger values of `days`.
Args:
population (LanternFishPopulation): Population of lanternfish.
days (int): Days to age.
day_split (int, optional): Split of day (recommend a value below 150 and an even
split of the total number of days). Defaults to 128 (to go well with 256 days).
Returns:
int: Number of fish after the number of days.
"""
fishes = np.array([f.age for f in population.lanternfishes], dtype=int)
fish_age_count: Counter = Counter(fishes)
for days_chunk in _split_up_days(days, day_split):
new_fish_ages: Counter = Counter()
for age, n_fish in fish_age_count.items():
new_fish_ages += Counter(
{ # type: ignore
a: n * n_fish for a, n in Counter(age_fish(age, days_chunk)).items()
}
)
fish_age_count = new_fish_ages
return sum(list(fish_age_count.values()))
def main() -> None:
"""Run code for day 6 challenge."""
# Part 1.
ex_fishes = LanternFishPopulation(_parse_lanternfish_ages_str(_ex_lanternfish_ages))
fishes = _get_lanternfish_ages_data()
ex_fishes.advance(80)
check_example(5934, len(ex_fishes))
fishes.advance(80)
n_fish = len(fishes)
print_single_answer(DAY, 1, n_fish)
check_answer(351092, n_fish, DAY, 1)
# Part 2.
ex_fishes = LanternFishPopulation(_parse_lanternfish_ages_str(_ex_lanternfish_ages))
ex_n_fish = age_a_population(ex_fishes, 80, day_split=40)
check_example(5934, ex_n_fish)
ex_n_fish = age_a_population(ex_fishes, 256, day_split=128)
check_example(26984457539, ex_n_fish)
fishes = _get_lanternfish_ages_data()
n_fish = age_a_population(fishes, 256, day_split=128)
print_single_answer(DAY, 2, n_fish)
check_answer(1595330616005, n_fish, DAY, 2)
return None
if __name__ == "__main__":
main()