How to solve Advent of Code 2024 - Day 2 with Python

How to solve Advent of Code 2024 - Day 2 with Python

It's the most festive time of the year - Advent of Code! The highlight of every coder's December. ๐ŸŽ„โ„๏ธ๐Ÿ’ป๐ŸŽ‰
But before we get right into the first day and my solution for this day's puzzle, here is the info on how I approach this whole event (where my code is, what editor I use, etc):

Advent of Code 2024 - Info Post
Itโ€™s the most festive time of the year - Advent of Code! The highlight of every coderโ€™s December ๐ŸŽ„โ„๏ธ๐Ÿ’ป๐ŸŽ‰ Hereโ€™s everything about my participation, I will link to this from every daysโ€™ post. What even IS Advent of Code??!! Good question, my friend. Here is this yearโ€™s about page: https:

Day 2 Puzzle

On my blog here, I will go through the puzzle and my way of solving it. If you want, you can jump directly to the code solution here:
GitHub .py script

Here is the challenge, if you want to read the full puzzle:
Advent of Code 2024 - Day 2

We get to the first location where we hope to find the missing Chief Historian: the Red-Nosed Reindeer nuclear fusion/fission plant - which was part of a puzzle in 2015, so if you have been following the lore that long this might mean something to you. The historian is nowhere to be seen. However, the local elves there seem to remember us fondly because they immediately ask for our help analyzing some unusual data from the Red-Nosed reactor. The unusual data consists of many reports, each containing a list of levels. The engineers are trying to determine which reports are safe based on specific criteria for increasing or decreasing levels.


Part 1

Day 2 gives you an example input text of records per line:

# Example input
7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9

We need to find lines are either decreasing at every step or increasing at every step. The change between any two adjacent numbers has to be between 1 and 3.

Solution Approach:

  • consider only the differences - transform the line of numbers into a line of differences (1 4 3 becomes 3 -1)
  • check all 3 properties
    • Property 1: all differences are positive
    • Property 2: all differences are negative
    • Property 3: all absolute differences are between 1 and 3
  • if (Prop 1 OR 2) AND Prop 3 are fulfilled, the record/row is safe

Key Code Snippets:

  • Input Parsing:
# Split line into int-list
def convert_line_to_report(line: str) -> list[int]:
    line = line.strip()
    return [int(value) for value in line.split()]
  • Core Logic/Is the report safe?:
def is_report_safe(report: list[int]) -> bool:
    diffs = [report[i] - report[i - 1] for i in range(1, len(report))]
    all_increasing = all(diff > 0 for diff in diffs)
    all_decreasing = all(diff < 0 for diff in diffs)

    if not (all_increasing or all_decreasing):
        return False

    all_distances_safe = all(abs(diff) >= 1 and abs(diff) <= 3 for diff in diffs)
    return all_distances_safe

Full Solution for Part 1:

def solve_part_1(mode: str = "example"):
    file_lines = read_input_file(day=2, mode=mode)
    records = [convert_line_to_report(line) for line in file_lines]
    safe_records = [record for record in records if is_report_safe(record)]
    return len(safe_records)

Part 2

In Part 2, a record is additionally allowed one un-safe step. So, if you would remove one number (any number), would it then be considered safe?

Solution Approach:
My solution is brute-force:

  • create all sub-records (remove one number for each)
  • check every sub-record for safeness

Key Code Snippets:

Create and Check Sub-Records:
Uses the method is_report_safe from Part 1 for each sub-record.

def is_report_safe_v2(report: list[int]) -> bool:
    if is_report_safe(report):
        return True
    for i in range(0, len(report)):
        sub_report = report[:i] + report[i + 1 :]
        if is_report_safe(sub_report):
            return True
    return False

Full Solution for Part 2:

def solve_part_2(mode: str = "example"):
    file_lines = read_input_file(day=2, mode=mode)
    records = [convert_line_to_report(line) for line in file_lines]
    safe_records = [record for record in records if is_report_safe_v2(record)]
    return len(safe_records)


Conclusion

Part 2 was frustrating - my solution is definitely not the best, but in the quick hours in between my work and private obligations, I couldn't think of anything better.

I'm convinced there has to be a nicer method to create the sub-records than slicing. And similarly I think computing the differences wasn't ideal for part 2 because it meant I needed to compute all sub-lists and if the rows had been much longer, this brute-force method would have gotten problematic really quickly...

I'm proud that I stayed strong though and didn't ask AI how to solve it better.