Day 2: Red-Nosed Reports
This post walks through solving Advent of Code 2024 - Day 2 Problem in Clojure.
The Advent of Code excitement continues with Day 2! As we dive deeper into the festive puzzle gauntlet, the challenges grow trickier, and the story gets even more delightfully chaotic. It’s time to sharpen our problem-solving wits and tackle another step in saving Christmas — one line of code at a time!
1. Levels are either entirely increasing or decreasing.
2. The difference between adjacent levels is between 1 and 3 (inclusive).
For example, a report like [7 6 4 2 1] is safe as it decreases consistently within the allowed range, while [1 2 7 8 9] is unsafe due to an increase of 5.
The goal is to count how many reports meet these safety criteria.
To dive deeper into the details of the Day 2 challenge, check out the official problem description.
Day 2 ramps up the complexity with a reactor safety puzzle, challenging us to analyze data and identify safe reports. Let’s dive into the details and solve this intriguing problem step by step!
Below clojure code reads reactor data from input.txt
, splits it into lines, and converts each line into a list of numbers. For each list, it creates a pair: the original list and the differences between consecutive numbers. These pairs are stored in data
, while data-diff
extracts just the differences for further analysis.
(ns aoc.2024.d2.d2
(:require [clojure.string :as s]))
(defn make-pair [coll]
[coll (map - (rest coll) (butlast coll))])
(def data
(->> "input.txt"
slurp
s/split-lines
(map #(map read-string (s/split % #"\s+")))
(map make-pair)))
(def data-diff (map second data))
The earlier code generates data-diff
, a sequence of level differences for each report. Now, the goal is to filter and count the reports that meet the safety criteria.
The make-pair
function takes a list of values as input and returns a pair consisting of:
- The original list of levels (coll).
- The list of differences between consecutive levels in the input list.
How it works:
By creating two shifted versions of the input list, the differences between consecutive elements are calculated.
(rest coll)
: Removes the first element.(butlast coll)
: Removes the last element.(map - ...)
: Calculates the difference between corresponding elements of the two shifted lists.
Example:
For a report [7 6 4 2 1]
:
rest
: [6 4 2 1].butlast
: [7 6 4 2].(map - ...)
: Calculates [6-7, 4-6, 2-4, 1-2], resulting in[-1 -2 -2 -1]
.
The function returns the pair [[7 6 4 2 1] [-1 -2 -2 -1]]
.
Purpose:
This function is critical for the problem because, it combines the original report and its differences into a single structure. This pairing allows us to analyze both the levels and their differences simultaneously, which is essential for checking safety and identifying invalid levels that can be removed.
The goal is to check whether the level differences for each report fit within the allowable ranges and then count the reports that are safe.
(defn valid-pos? [coll] (every? #(and (>= % 1) (<= % 3)) coll))
(defn valid-neg? [coll] (every? #(and (<= % -1) (>= % -3)) coll))
Above code clearly checks rule 2 of the problem, ensuring that the differences between adjacent levels fall within the specified range. But how does it check if the report is either entirely increasing or decreasing for rule 1?
Instead of explicitly tracking the direction, the code uses the differences between consecutive levels, which inherently show if the sequence is increasing or decreasing.
-
Increasing Report
: The report is considered increasing if all consecutive differences are positive and fall within the range of 1 to 3. -
Decreasing Report
: The report is considered decreasing if all consecutive differences are negative and fall within the range of -1 to -3.
For example, with a report of [7 6 4 2 1], the differences would be [-1 -2 -2 -1]. Since all the differences are negative, it is a decreasing report.
However, for a report like [7 6 4 5 1], the differences would be [-1 -2 1 -1]. The presence of a positive 1 indicates an increase, which breaks the condition for the report to be entirely decreasing.
Thus, by transforming the sequence into differences, the code efficiently checks if the report is either entirely increasing or decreasing, making it safe or unsafe.
(+ (count (filter #(valid-pos? %) data-diff))
(count (filter #(valid-neg? %) data-diff)))
By adding these two counts together, the code gives the total number of reports that meet the safety criteria (either all increasing or all decreasing). This is the final result for the problem: the number
of safe reports.
And just when you think you’ve got the reports all figured out, part two throws in a curveball, proving once again that Advent of Code is full of surprises and challenges you never see coming!
Our goal is to count how many reports are safe after considering those that can be made safe by removing a single “bad” level.
In this part of the problem, we aim to determine if a report can be made safe by removing just one “bad” level. Unlike earlier, where we worked directly with the differences between levels, we now focus on reports with exactly one invalid level.
Why the Shift in Approach?
Previously, we checked if a report was entirely valid by ensuring its differences were within the allowed range. However, now we handle a nuanced case: reports that are almost safe but have one invalid level that, if removed, makes them valid.
To address this, we work with pairs of data:
- The original report (list of levels).
- The differences between consecutive levels.
This pairing provides the necessary context to identify and remove the specific level causing the report to be unsafe, enabling us to check if the remaining sequence becomes valid. This shift lets us handle reports that are close to meeting the safety criteria.
Here’s where the make-pair
function shines, creating pairs of each report and its differences. These pairs allow us to pinpoint “almost safe” reports and determine if removing a single “bad” level can make them safe.
The following code is a helper function used to remove the element at the nth index in a report’s list of levels.
(defn remove-nth [coll index]
(vec (remove #(= (nth coll index) %) coll)))
We also need two additional helper functions, similar to valid-pos?
and valid-neg?
, but tailored to check individual values instead of collections. These functions are designed to validate whether a single difference falls within the allowed range for increasing or decreasing sequences.
(defn valid-pos-val? [val] (and (>= val 1) (<= val 3)))
(defn valid-neg-val? [val] (and (<= val -1) (>= val -3)))
-
valid-pos-val?
: Checks if a given value is between 1 and 3, which represents a valid positive difference for an increasing sequence. -
valid-neg-val?
: Checks if a given value is between -1 and -3, which represents a valid negative difference for a decreasing sequence.
These functions are used to verify that individual differences in the sequence comply with the safety rules of the problem.
(def almost-increasing-reports
(filter #(= 1 (count
(remove valid-pos-val? (second %))))
data))
(def almost-decreasing-reports
(filter #(= 1 (count
(remove valid-neg-val? (second %))))
data))
Above code identifies reports that can potentially become valid by removing exactly one invalid level, grouping them into almost-increasing and almost-decreasing categories.
-
almost-increasing-reports
: This is a collection of reports where, if we remove one invalid positive difference (i.e., a difference that is not between 1 and 3), the report becomes valid (increasing sequence). -
almost-decreasing-reports
: Similarly, this collection contains reports where removing one invalid negative difference (i.e., a difference not between -1 and -3) makes the report valid (decreasing sequence).
The key idea here is that we don’t know which report might be safe after removing a “bad” level. By filtering the reports, we’re narrowing down the set to reports that have exactly one invalid level. This is crucial because, for the Problem Dampener, we only need to check reports that are almost safe (i.e., they have exactly one bad level) rather than all reports.
Instead of checking all reports and trying to remove every level, we first filter out only those reports that have exactly one bad level. This significantly reduces the number of reports we need to process, making our solution more efficient.
By filtering for reports with exactly one bad level, we’re focusing on the reports that can potentially be made safe by removing just one level. This ensures we’re not wasting time on reports that are either completely valid or completely invalid, as they’re already handled by other parts of the code.
(defn safe-increasing? [datum]
(keep-indexed (fn [idx val]
(when (not (valid-pos-val? val))
(valid-pos?
(second (make-pair
(remove-nth (first datum) (inc idx)))))))
(second datum)))
(defn safe-decreasing? [datum]
(keep-indexed (fn [idx val]
(when (not (valid-neg-val? val))
(valid-neg?
(second (make-pair
(remove-nth (first datum) (inc idx)))))))
(second datum)))
The core logic of safe-increasing?
and safe-decreasing?
lies in identifying “bad” levels in a report. These functions check if removing a single invalid level can make the report valid (either increasing or decreasing). If any “bad” level’s removal results in a valid sequence, the function returns true.
Here’s an explanation of what the code does:
-
(second datum)
: Accesses the list of differences for the report. -
(keep-indexed (fn [idx val] ...)
: Processes each difference alongside its index. -
(not (valid-pos-v? val))
: Identifies invalid differences not meeting the criteria for an increasing sequence (1 to 3) or decreasing sequence (-1 to -3). -
valid-pos?
: Validates the updated sequence after removing the “bad” level.
The logic above effectively pinpoints invalid levels and test if their removal transforms the report into a valid sequence.
(count (filter true?
(flatten (concat
(map safe-increasing? almost-increasing-reports)
(map safe-decreasing? almost-decreasing-reports)))))
The given code calculates the total number of reports that are safe after considering the possibility of removing a single “bad” level.
Here’s the full source code for the solution, which combines all the steps we’ve discussed:
(ns aoc.2024.d2.d2
(:require [clojure.string :as s]))
(defn make-pair [coll]
[coll (map - (rest coll) (butlast coll))])
(def data
(->> "input.txt"
slurp
s/split-lines
(map #(map read-string (s/split % #"\s+")))
(map make-pair)))
(def data-diff (map second data))
(defn remove-nth [coll index]
(vec (remove #(= (nth coll index) %) coll)))
(defn valid-pos? [coll] (every? #(and (>= % 1) (<= % 3)) coll))
(defn valid-neg? [coll] (every? #(and (<= % -1) (>= % -3)) coll))
;; Part 1
(+ (count (filter #(valid-pos? %) data-diff))
(count (filter #(valid-neg? %) data-diff)))
(defn valid-pos-val? [val] (and (>= val 1) (<= val 3)))
(defn valid-neg-val? [val] (and (<= val -1) (>= val -3)))
(def almost-increasing-reports
(filter #(= 1 (count
(remove valid-pos-val? (second %))))
data))
(def almost-decreasing-reports
(filter #(= 1 (count
(remove valid-neg-val? (second %))))
data))
(defn safe-increasing? [datum]
(keep-indexed (fn [idx val]
(when (not (valid-pos-val? val))
(valid-pos?
(second (make-pair
(remove-nth (first datum) (inc idx)))))))
(second datum)))
(defn safe-decreasing? [datum]
(keep-indexed (fn [idx val]
(when (not (valid-neg-val? val))
(valid-neg?
(second (make-pair
(remove-nth (first datum) (inc idx)))))))
(second datum)))
;; Part 2
(count (filter true?
(flatten (concat
(map safe-increasing? almost-increasing-reports)
(map safe-decreasing? almost-decreasing-reports)))))
And just like the unfolding chaos of Day 2 in Advent of Code — where reports wobble between safety and danger — we’ve tackled the challenge of spotting patterns, calculating differences, and dealing with almost-safe reports. A touch of Clojure wizardry later, we’ve counted the safe ones. Just when we thought we had it all figured out, the “Problem Dampener” stepped in, adding a curveball that forced us to reevaluate our approach and handle reports with a touch more complexity.
Our solution for Part 2 isn’t giving the correct output to Advent of Code, and we suspect something might be missing or incorrect in our approach. If you notice any issues or potential mistakes in the logic, please feel free to let us know in the comments.
But as we wrap this up, Day 3 looms on the horizon—ready to test our problem-solving mettle once again!