day06_part1.py (3787B)
1 #!/usr/bin/env python 2 """Advent of Code 2021, day 6 (part 1): simulate an exponentially growing 3 school of lanternfish using an initial list of fish and their ages""" 4 5 # For this puzzle, the most important decision is the data structure to use to 6 # represent the school of fish. Since there's exponential growth, any scheme 7 # that uses an element for each fish could quickly get out of hand. Since the 8 # set of possible ages is quite small, it's much more efficient to use counters 9 # for each age. I'm using a pandas Series because of course I am (but also, I'm 10 # confronting my aversion to pandas index attributes head on), but a simple 11 # dict would work fine here too, or even a list if you're careful with updating 12 # indices. 13 14 import functools 15 from utils import get_puzzle_input, convert_int_line_to_series 16 17 def convert_input_to_series(input_string): 18 """Parse the input (a sequence of fish with their age in days) and return a 19 Series, where the indices represent fish ages (in days) and the values 20 represent the count of fish that age""" 21 return convert_int_line_to_series(input_string).value_counts() 22 23 # For this implementation, we'll advance the fish in age and spawn new fish all 24 # in one step--even though, conceptually, it would be nice to separate these 25 # steps--because that way it'll be easier to separate the fish that are ready 26 # to spawn from the babies who aren't. 27 28 def step_one_day(fish_counts): 29 """Given a Series of fish counts, simulate one day of ageing/growth of the 30 school, and return a new Series""" 31 # Once again, I'm trying to program more functionally, which means I'm 32 # treating data structures as immutable (outside the function) even when it 33 # might be more efficient to update things in-place. Theoretically we're 34 # trading efficiency for "safety", although I'm perfectly aware that for 35 # these puzzles it really doesn't matter. But inside the actual function, I 36 # am editing things in-place, so that's a bit of a fail. 37 # Implementation note: I looked at a couple methods of updating the axis 38 # and was surprised by the results. 39 # %timeit fish_counts.set_axis((fish_counts.index - 1) % 9) 40 #> 305 µs ± 6.05 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) 41 # %timeit fish_counts.rename(lambda x: (x - 1) % 9) 42 #> 93.1 µs ± 2.19 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) 43 updated_fish_counts = fish_counts.rename(lambda x: (x - 1) % 9) 44 # We're cheating a bit here. Rather than resetting an adult's counter back 45 # to 6 and then adding babies with a counter set to 8, we're resetting 46 # adults' counters to 8 (representing the babies) and then adding the 47 # adults back into the count. That's why we take the counter value modulo 9 48 # instead of the obvious 7. 49 if 8 in updated_fish_counts: 50 if 6 in updated_fish_counts: 51 updated_fish_counts[6] += updated_fish_counts[8] 52 else: 53 updated_fish_counts[6] = updated_fish_counts[8] 54 return updated_fish_counts 55 56 def count_fish_after(fish_counts, num_days): 57 """Given a Series of fish counts and a number of days, return the total 58 number of fish in the school as an integer""" 59 return int( 60 functools.reduce(lambda x, y: step_one_day(x), 61 range(num_days), 62 fish_counts) 63 .sum() 64 ) 65 66 def solve_puzzle(input_string): 67 """Return the numeric solution to the puzzle""" 68 return count_fish_after(convert_input_to_series(input_string), 80) 69 70 def main(): 71 """Run when the file is called as a script""" 72 assert solve_puzzle("3,4,3,1,2\n") == 5934 73 print("Number of lanternfish after 80 days:", 74 solve_puzzle(get_puzzle_input(6))) 75 76 if __name__ == "__main__": 77 main()