Day 2: Red-Nosed Reports

Author

Kasia Kedzierska

Published

December 2, 2024

Modified

December 5, 2024

The first place to search for the Chief Historian is the Red-Nosed Reindeer Nuclear Fusion/Fission Plant. While the historians scour the facility for clues, we are tasked with assisting the engineers in analyzing unusual data generated by the reactor.

The data is presented as a matrix of integers, where each row represents a separate report consisting of consecutive levels.

For the full details of the task, you can visit the official Advent of Code 2024 website.

To set the scene, I asked ChatGPT to generate an image of the plant. Here’s the result:

Part 1: Counting Safe Reports

Our first task is to analyze the reports and identify how many are safe. Each report is represented as a list of integers, where:
- A report is safe if all levels are either gradually increasing or gradually decreasing.
- The difference between consecutive levels must be at least 1 and at most 3.

We need to determine the number of safe reports in the data.

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

Safety Analysis:
- Report 1 (7 6 4 2 1): Safe. The levels are consistently decreasing with differences of 1 or 2.
- Report 2 (1 2 7 8 9): Unsafe. The jump from 2 to 7 is 5.
- Report 3 (9 7 6 2 1): Unsafe. The drop from 6 to 2 is 4.
- Report 4 (1 3 2 4 5): Unsafe. The sequence alternates between increasing and decreasing.
- Report 5 (8 6 4 4 1): Unsafe. The repeated 4 violates the safety rule.
- Report 6 (1 3 6 7 9): Safe. The levels are consistently increasing with differences within the valid range of 1-3.

Result: Out of the six reports, 2 are safe.

from misc.helper import verify_answer

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
"""

example_answer_one = 2

First, I define the helper function to check the sign of a number.

def sign(x) -> int:
    """Return the sign of x"""
    return (x > 0) - (x < 0)

Then, I’ll write a function to check if a report is safe by verifying if the differences between consecutive levels are within the valid range, and the levels are either consistently increasing or decreasing.

def test_sequence(report: list[int]) -> bool:
    """
    Check if a sequence is valid based on safety rules.

    A sequence is valid if it is strictly increasing or decreasing,
    with consecutive differences in the range [1, 3].
    """
    for l1, l2 in zip(report[:-1], report[1:]):
        if abs(l1 - l2) > 3 or abs(l1 - l2) == 0:
            return False
        elif sign(l1 - l2) != sign(report[0] - report[1]):
            return False
    return True

Next, I’ll implement a function to read the reports from the input file, or a string (for easire testing).

from pathlib import Path


def read_input(input_source: str | Path) -> list[int]:
    """Read input file"""
    if Path(input_source).is_file():
        with open(input_source, "r") as f:
            return [list(map(int, line.split())) for line in f]
    else:
        return [
            list(map(int, seq.split()))
            for seq in input_source.strip().split("\n")
        ]

And finally, I’ll write a function to put it all together and count the safe reports.

def part_one(input_source: str | Path):
    """Count the number of valid sequences"""
    reports = read_input(input_source)
    return sum(test_sequence(report) for report in reports)


verify_answer(
    part_one,
    example_input,
    example_answer_one,
)
✔️ That's right! The answer is 2.

Since the test is correct, I’ll run the function on the actual input data.

%time
part_one("../inputs/day_02.txt")
CPU times: user 1 μs, sys: 0 ns, total: 1 μs
Wall time: 4.05 μs
526

That’s the right answer! You are one gold star ⭐ closer to finding the Chief Historian.

Part 2: Adjusting reports with the Problem Dampener

In this task, we expand the definition of a safe report to include those that can be made safe by removing up to one level from the sequence. This means that even if a report initially fails the safety criteria, it can still be considered safe if removing one level resolves the issue.

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

Adjusted Safety Analysis:
- Report 1 (7 6 4 2 1): Safe. No changes needed.
- Report 2 (1 2 7 8 9): Still unsafe. No single level can be removed to make it safe.
- Report 3 (9 7 6 2 1): Still unsafe. No single level can be removed to make it safe.
- Report 4 (1 3 2 4 5): Safe if the level 3 is removed, resulting in 1 2 4 5.
- Report 5 (8 6 4 4 1): Safe if the level 4 (third position) is removed, resulting in 8 6 4 1.
- Report 6 (1 3 6 7 9): Safe. No changes needed.

Result: With this adjustment, the number of safe reports increases from 2 to 4.

Key Changes:
- A Problem Dampener allows for the removal of a single problematic level to validate an otherwise unsafe report.
- This provides additional flexibility in assessing reactor data, ensuring that slightly flawed reports can still be deemed safe.

In this solution, I will need to modify the approach to test all possible subsequences for unsafe reports and check if removing one level makes them safe.

example_answer_two = 4


def part_two(input_source: str | Path):
    """Count the number of valid sequences"""
    reports = read_input(input_source)
    safe = 0
    for report in reports:
        if test_sequence(report):
            safe += 1
        else:
            for i in range(len(report)):
                if test_sequence(report[:i] + report[i + 1 :]):
                    safe += 1
                    break
    return safe


verify_answer(
    part_two,
    example_input,
    example_answer_two,
)
✔️ That's right! The answer is 4.
%time
part_two("../inputs/day_02.txt")
CPU times: user 3 μs, sys: 0 ns, total: 3 μs
Wall time: 5.01 μs
566

That’s the right answer! You are one gold star ⭐ closer to finding the Chief Historian.