Advent of Code 2021: Day 3

Advent of Code 2021 - Day 3

Day 3 puzzle description

Day 3 puzzle description can be found on my Github.

My solution in Python

from collections import Counter
from functools import partial
from typing import List


def binary_str_to_int(s: str) -> int:
    return int(s, 2)


def get_data() -> List[str]:
    with open("aoc-day-3-input.txt", "rt") as f:
        data = [x.replace("\n", "") for x in f.readlines()]
    return data


def solution(input_data: List[str]) -> int:
    # sanity check for input data - all numbers must be of same length
    assert len(set([len(line) for line in input_data])) == 1  # noqa

    num_length = len(input_data[0])

    gamma_rate_str = ""
    epsilon_rate_str = ""

    for i in range(num_length):
        most_common = Counter([line[i] for line in input_data]).most_common()
        gamma_rate_str += most_common[0][0]
        epsilon_rate_str += most_common[1][0]

    gamma_rate = binary_str_to_int(gamma_rate_str)
    epsilon_rate_str = binary_str_to_int(epsilon_rate_str)

    return gamma_rate * epsilon_rate_str


def get_life_support_rating(nums: List[str]) -> int:
    # trying to use more functional programming style
    def generic_bit_criteria(mc_index: int, default: str, c: Counter) -> str:
        mc = c.most_common()
        if mc[0][1] == mc[1][1]:
            return default
        return mc[mc_index][0]

    oxygen_generator_bit_criteria = partial(generic_bit_criteria, 0, "1")
    co2_scrubber_bit_criteria = partial(generic_bit_criteria, -1, "0")

    def calculate_rate_by_bit_criteria(
        numbers: List[str], bit_criteria, ix: int = 0
    ) -> str:
        if len(numbers) == 1:
            return numbers[0]
        dominating_bit = bit_criteria(Counter([n[ix] for n in numbers]))
        filtered_numbers = list(filter(lambda x: x[ix] == dominating_bit, numbers))
        return calculate_rate_by_bit_criteria(filtered_numbers, bit_criteria, ix + 1)

    oxygen_rate = binary_str_to_int(
        calculate_rate_by_bit_criteria(nums, oxygen_generator_bit_criteria)
    )
    co2_scrubber_rate = binary_str_to_int(
        calculate_rate_by_bit_criteria(nums, co2_scrubber_bit_criteria)
    )

    return oxygen_rate * co2_scrubber_rate


if __name__ == "__main__":

    input_data = get_data()
    print(solution(input_data))
    print(get_life_support_rating(test_data))
    print(get_life_support_rating(input_data))

Quick recap

I used collections.Counter to counter frequency of 0s and 1s. Its API exposes most_common() method which made calculating most/least common bit very easy.

Part 2 solution was inspired a bit by a book that I'm currently reading: Functional Programming Simplified by Alvin Alexander - I used partial from functools module - it allowed me to take common logic of oxygen and CO2 scrubber rate to one generic function and then create their actual implementions with partial.

Other functional approach I took was to use recursion (in calculate_rate_by_bit_criteria). It was very satisfying since switching traditional for loops for recursive algorithms feels still unintuitive to me.

And that's it for day 3 of Advent of Code 2021.

Take care :-),
Kuba

Thanks for reading the article, I really appreciate it! Have you heard about Braintrust - the first decentralized talent network? Whether you're a freelancer looking for a job, an employer looking for hiring talents, or you just have a wide network of connections - there's something for you there!

Go check it out and register with below link (yeah - it's my referral link and it's free - no hidden costs):

Registration link