Skip to content
    Banner image

    Advent of Code 2024 - Part 1

    Authored on December 21, 2024 by Aaron Christopher.

    7 min read
    --- views

    Introduction

    Advent of Code is a calendar of short programming puzzles that may be completed in any programming language and are appropriate for a range of skill levels. It is used for practice problems, university coursework, company training, speed contests, interview preparation, and peer challenge. You can learn new programming languages or hone your abilities by solving the puzzles, which are presented every day in December.

    I'll be working through the Rust challenges on this blog. I'll be discussing my methodology and the code I employed to solve the puzzles.

    Day 1: Reconciling Lists

    The challenges this year kicked off with an interesting problem: comparing two lists of numbers to calculate differences and similarities.

    Part 1: Measuring Total Distance

    We are tasked with pairing the smallest numbers from two lists and measuring the differences, summing them up across all pairs.

    Example: Given these two lists:

    Left List: [3, 4, 2, 1, 3, 3] Right List: [4, 3, 5, 3, 9, 3]
    plaintext

    Sorting them results in:

    Left List: [1, 2, 3, 3, 3, 4] Right List: [3, 3, 3, 4, 5, 9]

    Pairing smallest to smallest and calculating the distance:

    |1 - 3| + |2 - 3| + |3 - 3| + |3 - 4| + |3 - 5| + |4 - 9| = 11
    plaintext

    Solution in code

    Here’s my humble solution for Part 1:

    fn part_one(vec_a: &Vec<i64>, vec_b: &Vec<i64>) -> u64 { let mut sum: u64 = 0; for (a, b) in vec_a.iter().zip(vec_b.iter()) { sum += a.abs_diff(*b); } return sum; }
    rust

    Explanation

    • The function takes two sorted lists (vec_a and vec_b).
    • Using zip, we pair the elements in order and compute the absolute difference for each pair.
    • The differences are summed up to get the total distance.

    Part 2: Calculating Similarity Score

    Now, we calculate a “similarity score” by multiplying each number in the left list by the number of times it appears in the right list.

    Example: Using the same lists:

    Left List: [3, 4, 2, 1, 3, 3] Right List: [4, 3, 5, 3, 9, 3]
    plaintext
    • 3 appears 3 times in the right list → 3 * 3 = 9
    • 4 appears 1 time in the right list → 4 * 1 = 4
    • 2 appears 0 times → 2 * 0 = 0
    • And so on…

    The total similarity score is 31.

    Solution in code

    Here’s my solution for Part 2:

    fn part_two(vec_a: &Vec<i64>, vec_b: &Vec<i64>) -> i64 { let mut similarity: i64 = 0; let mut freqs: HashMap<i64, i64> = HashMap::new(); for b in vec_b.iter() { let count = freqs.entry(*b).or_insert(0); *count += 1; } for a in vec_a.iter() { let b_freq = freqs.get(&a).unwrap_or(&0); similarity += a * b_freq; } return similarity; }
    rust

    Explanation

    • The function takes lists (vec_a and vec_b).
    • We create a HashMap to store the frequency of each number in the right list.
    • We iterate over the right list and increment the frequency count for each number.
    • We iterate over the left list and calculate the similarity score by multiplying each number by its frequency in the right list.

    Putting It All Together

    The following run function ties everything together:

    pub fn run() { println!("Day 01:"); let lines = lines_from_file("./src/day01/input"); let mut vec_a: Vec<i64> = Vec::new(); let mut vec_b: Vec<i64> = Vec::new(); for line in lines.iter() { vec_a.push(line[0]); vec_b.push(line[1]); } vec_a.sort(); vec_b.sort(); let sum = part_one(&vec_a, &vec_b); let similarity = part_two(&vec_a, &vec_b); println!("Part 1: {}", sum); println!("Part 2: {}", similarity); }
    rust

    Im using this kinda pattern for each day of the challenge.

    What it does

    1. Reads input from a file, parsing each line into two lists: vec_a and vec_b.
    2. Sorts both lists to prepare them for pairing in Part 1.
    3. Calculates the total distance and similarity score using the two helper functions.
    4. Prints the results for both parts.

    Day 2: Red-Nosed Reports

    This challenge was about analyzing unusual data reports to determine their safety under specific conditions. Let's break down the problem and my solution in Rust.

    Part 1: Analyzing Safe Reports

    Each report is a list of levels (numbers). A report is considered safe if:

    1. The levels are either all increasing or all decreasing.
    2. The difference between any two adjacent levels is at least 1 and at most 3.

    Example Data:

    7 6 4 2 1: Safe (all decreasing by 1-2). 1 2 7 8 9: Unsafe (difference of 5 between 2 and 7). 9 7 6 2 1: Unsafe (difference of 4 between 6 and 2). 1 3 2 4 5: Unsafe (switches between increasing and decreasing). 8 6 4 4 1: Unsafe (4 and 4 have no change). 1 3 6 7 9: Safe (all increasing by 1-3).
    plaintext

    Task: Count how many reports are safe according to these rules.

    Solution in code

    The is_safe function determines if a report meets the safety criteria.

    fn is_safe(line: &Vec<i64>) -> bool { let mut is_safe = true; let mut dir = 0; for report_pair in line.windows(2) { let diff = report_pair[0] - report_pair[1]; let new_dir = if diff > 0 { 1 } else if diff < 0 { -1 } else { 0 }; if diff == 0 || (dir != 0 && dir != new_dir) || diff.abs() > 3 { is_safe = false; break; } dir = new_dir; } is_safe }
    rust

    Explanation

    • Loops through pairs of adjacent levels using windows(2).
    • Tracks the “direction” (increasing, decreasing, or no change).
    • Ensures adjacent levels differ by at least 1 and at most 3.

    Heres the code for part one:

    fn part_one(lines: &Vec<Vec<i64>>) -> u64 { let mut safe_count = 0; for line in lines.iter() { if is_safe(&line) { safe_count += 1; } } safe_count }
    rust

    Part 2: Using the Problem Dampener

    The “Problem Dampener” allows the removal of one level from an otherwise unsafe report, potentially making it safe.

    Example Data:

    7 6 4 2 1: Safe without changes. 1 2 7 8 9: Unsafe, even after removing any one level. 9 7 6 2 1: Unsafe, even after removing any one level. 1 3 2 4 5: Safe after removing the second level (3). 8 6 4 4 1: Safe after removing the third level (4). 1 3 6 7 9: Safe without changes.
    plaintext

    Task: Count how many reports are safe with the Problem Dampener.

    Solution in code

    This part adds the flexibility to remove a single level and recheck the safety of the report.

    fn part_two(lines: &Vec<Vec<i64>>) -> u64 { let mut safe_count = 0; for reports in lines.iter() { for i in 0..reports.len() { let tolerated_levels: Vec<_> = reports[..i] .iter() .chain(&reports[i + 1..]) .cloned() .collect(); if is_safe(&tolerated_levels) { safe_count += 1; break; } } } safe_count }
    rust

    Explanation

    • Iterates over each report and each level in the report.
    • Creates a new list without the current level.
    • Checks if the new list is safe.
    • Increments the safe count if the report is safe after removing a level.

    The run function ties everything together:

    pub fn run() { println!("Day 02:"); let lines = lines_from_file("./src/day02/input"); // Part A let safe_count_1 = part_one(&lines); let safe_count_2 = part_two(&lines); println!("Part 1: {}", safe_count_1); println!("Part 2: {}", safe_count_2); }
    rust

    Reflection

    The Advent of Code challenges are a great way to practice problem-solving and learn new programming languages. Rust is a powerful language with a strong emphasis on safety and performance. I enjoyed solving these puzzles in Rust and and am eager for the upcoming challenges.