Advent of Code 2025

Here it comes that time of the year again: Advent of Code is out!

This year, as Eric pointed out, the total number of puzzles will be only 12. He has a life, and after 10 years of doing this, he wants to dedicate less time to it; totally understandable.

I actually appreciated the new duration even more. It’s usually hard to keep up with the pace of the challenges and having less puzzles made it a more relaxing experience.

By the end, I earned 22 stars out of 24. You can find the code for all solved puzzles (with example inputs) in this repository: https://github.com/boh717/advent-of-code-2025/.

So now let’s dive into the puzzles!

Day 1

Puzzle | Code

In the first part of this initial day, we need to perform arithmetic operations within a circular range of numbers; this screams modulo operations!

Depending on the direction, we have the following:

let new_starting_point = if direction == "R" {
    (starting_point + num_offset).rem_euclid(100)
} else {
    (starting_point - num_offset).rem_euclid(100)
};

The second part would be easily solvable with a brute force approach, but I prefer to tackle it mathematically. Admittedly, it took much more than expected as I got lost in the edge cases for the L direction 🤦

This function did the trick:

fn count_all_zero_clicks(starting_point: i32, num_offset: i32, direction: &str) -> i32 {
    let distance_to_zero = match (starting_point, direction) {
        (0, _) => 100,
        (_, "R") => 100 - starting_point,
        _ => starting_point,
    };

    if num_offset >= distance_to_zero {
        1 + (num_offset - distance_to_zero) / 100
    } else {
        0
    }
}

Day 2

Puzzle | Code

Today, we need to find invalid IDs.

The first part is straightforward because an invalid ID is made only of some sequence of digits repeated twice. That means that all invalid IDs must have an even number of figures.

We can split the IDs into two equal parts based on their length and then check them. Boom! Solved!

The second part is trickier:

Now, an ID is invalid if it is made only of some sequence of digits repeated at least twice.

The brute force approach could work, but we can do better. We need to split the sequences only into parts that are equal in length. For example, 1212 can only be split into [1212], [12, 12], and [1, 2, 1, 2]. Any other split wouldn’t make sense for our use case.

That means we can use the ID length divisors and use them to chunk the sequence. Evaluating each chunk will reveal the invalid sequences we’re looking for!

What’s important is to exclude the divisor equal to the length:

.filter(|&&d| d < length)

Not excluding it would generate a single chunk (i.e., [1212]), which would result in an invalid, but it is indeed a false positive.

A final note: this blog post provided me with several ideas on how to check if all elements of a vector are equal, without relying on the itertools crate or reinventing the wheel. Well done!

Day 3

Puzzle | Code

The first part of the day is easy. It’s an O(n^2) algorithm by the book. I could think of something to be more proud of, but why? Let’s be greedy, also because, by any chance, the second part will be much harder than this. Or maybe I’m lucky. Who knows.

This function worked like a charm, and the first star is mine!

fn get_highest_voltage_per_bank(battery_bank: &str) -> usize {
    battery_bank
        .char_indices()
        .map(|(idx, ch)| {
            let sub_str: String = battery_bank.chars().skip(idx + 1).collect();
            sub_str
                .chars()
                .map(|sub_char| format!("{}{}", ch, sub_char).parse().unwrap())
                .max()
                .unwrap_or(0)
        })
        .max()
        .unwrap_or(0)
}

Now, we have to do the same thing, but instead of 2-size voltage, we need 12! Certainly, the previous approach can’t work; the sun would probably explode before it reaches termination.

Did you spot the greedy easter egg before? Well, that’s what we’re going to do!

The intuition here lies in the fact that the left-most digits are much more important, so we can decide locally which digits to pick without evaluating all other options. When we encounter a larger digit, we can revert to previous smaller choices as long as we have enough digits left to complete the sequence. A monotonic stack will help us achieve the second star.

In the end, I also rewrote the first part using this approach; the only difference is the sequence length we’re looking for, and it’s much more performant (from previous O(n^2) to O(n)).

Day 4

Puzzle | Code

The grid season has begun, but there are definitely days worse than this!

The first part requires going through the grid and evaluating how many items (i.e., rolls of paper @) satisfy a specific condition. It’s great that I have the neighbors_with_diagonals function from last year, so I don’t have to go crazy with the indexes to get them right!

Part 1 is faster done than said:

fn part1(grid: &HashMap<Coordinate, char>) -> usize {
    grid.iter()
        .filter(|(k, v)| **v == '@' && is_roll_paper_reachable(grid, &k.neighbors_with_diagonals()))
        .count()
}

fn is_roll_paper_reachable(grid: &HashMap<Coordinate, char>, neighbors: &[Coordinate]) -> bool {
    neighbors
        .iter()
        .filter(|n| grid.get(n).is_some_and(|c| *c == '@'))
        .count()
        < 4
}

The second part implies taking the above logic and iterating it until there are no more changes in the grid. I opted for an imperative solution, which was easy to implement and fast to reason about. It’s not the best algorithm because the time complexity is O(n*iterations), which could lead to O(n^2) in the worst case.

A better approach would have been to consider that the rolls that become potentially eligible for removal in the next iteration are the neighbors of the current iteration’s removals. That would bring down the time complexity to O(n), since each cell only needs to be checked once.

Day 5

Puzzle | Code

Today is tricky, but fun!

We have numeric ranges (known as fresh ingredients), and we need to verify how many numbers (known as available ingredients from another list) fall into at least one of these ranges. In other words, how many of the available ingredients are fresh?

Given that ranges can overlap, my first thought (don’t ask me why) was: “Cool, I’ll use a set, store every number, and then check the number’s existence there”.

This worked like a charm on the example input, but when run on the real input, it turned out to be uberly slow because the provided ranges are huge! Here’s the offending code:

fn part1(fresh: &[String], available: &[String]) -> usize {
    let mut available_fresh_ingredients: HashSet<usize> = HashSet::new();
    for range in fresh {
        let (start, end) = parse_range(range);
        (start..=end).for_each(|n| {
            available_fresh_ingredients.insert(n);
        });
    }

    available
        .iter()
        .filter(|&n| available_fresh_ingredients.contains(&n.parse().unwrap()))
        .count()
}

I took another direction then: for every number to check, I go through the ranges, and stop if I find a containing range. Yes, it’s the usual O(n^2), but it was fast enough.

The second part wanted us to count the number of fresh ingredients. Well, there are two clear issues: the ranges are huge and overlapping! We can’t go through them one by one. We wouldn’t finish in a reasonable amount of time, and the result would be wrong.

The idea was to merge the overlapping ranges. Running only this idea already caused issues with the example input. After a while, I peeked at Reddit, and this post clicked: my idea was correct, but I needed to sort them first!

The version that brought me the second star is the following:

fn part2(fresh_ingredients: &[String]) -> usize {
    let mut fresh_ranges: Vec<RangeInclusive<usize>> = get_fresh_ranges(fresh_ingredients);
    fresh_ranges.sort_by_key(|range| *range.start());
    let mut final_ranges: HashSet<RangeInclusive<usize>> = HashSet::new();

    for current_range in fresh_ranges {
        if let Some(stored_range) = final_ranges
            .iter()
            .find(|&other| ranges_overlap(&current_range, other))
        {
            let new_range = merge_ranges(&current_range, stored_range);
            final_ranges.remove(&stored_range.clone());
            final_ranges.insert(new_range);
        } else {
            final_ranges.insert(current_range);
        }
    }

    final_ranges.iter().map(|r| r.end() - r.start() + 1).sum()
}

It’s not over yet! Can you spot the issues? There are two:

  • It’s still O(n^2) (the merge step dominates, because sort is O(n * log(n)))
  • The HashSet is unnecessary. Since the input is sorted, any overlapping range must be the last one we added, so we can use a Vec.

Taking advantage of that, the algorithm becomes O(n * log(n)) (dominated by the sorting step this time; merge step is O(n) instead) 🚀

    let final_ranges = fresh_ranges
        .into_iter()
        .fold(Vec::new(), |mut acc, current| {
            match acc.last() {
                Some(last) if ranges_overlap(&current, last) => {
                    let new_range = merge_ranges(&current, &acc.pop().unwrap());
                    acc.push(new_range);
                }
                _ => acc.push(current),
            }
            acc
        });

A final note: when I read about overlapping ranges, I remembered a fantastic blog post that explains an easy way to check if intervals overlap or not.

Day 6

Puzzle | Code

Weird input ahead, and we have to read it vertically. Given the lines, for part 1, we need to process all first elements, all second elements, and so on.

Given every line is a vector, we can zip them all, and the solution is served! I relied on itertools and its izip! to simplify the syntax.

Time for part 2, and it gets even trickier.

Now, we need to read the lines as before, but treating each number as vertical, so it goes one layer deeper. The only hint we have on how to split numbers correctly is “Problems are still separated with a column consisting only of spaces”. So that’s what I used for the extract_spaced_numbers function.

One final note: using izip! implies that input is fixed, so the pushed code will work only with the real puzzle input and not the example.

Day 7

Puzzle | Code

The first thing that came to my mind was: “Well, I just need to count the number of splitters, how easy is that?!” Then I realized that life is harder and that some splitters are not reached, and so I need to simulate the full beam journey (of course, it’s day 7, not 1!). The beam starts at S and flows downward. When it hits a splitter (^), it splits left and right. Splitters that the beam never reaches don’t count. By propagating the beam (|) downwards, when I encounter a splitter (^) that has a beam above, I note it down. The count of used splitters is the solution to part 1.

fn part1(input: &[String], grid: &mut HashMap<Coordinate, char>) -> usize {
    let mut used_splitters: HashSet<Coordinate> = HashSet::new();
    for (line_idx, line) in input.iter().enumerate() {
        for (col_idx, ch) in line.char_indices() {
            let current_coord = Coordinate::new(line_idx as i32, col_idx as i32);
            match ch {
                'S' => {
                    let new_beam = current_coord.down();
                    grid.entry(new_beam).and_modify(|c| *c = '|');
                }
                '^' => {
                    if let Some(el) = grid.get(&current_coord.up())
                        && *el == '|'
                    {
                        for neighbor in [
                            current_coord.left(),
                            current_coord.right(),
                        ] {
                            grid.entry(neighbor).and_modify(|c| *c = '|');
                        }
                        used_splitters.insert(current_coord);
                    }
                }
                _ => {
                    if let Some(el) = grid.get(&current_coord.up())
                        && *el == '|'
                    {
                        grid.entry(current_coord).and_modify(|c| *c = '|');
                    }
                }
            }
        }
    }

    used_splitters.len()
}

Part 2 calls for dynamic programming. A naive approach, enumerating all paths, would explode combinatorially. Instead of tracking individual paths, we track how many timelines reach each cell. When two paths merge at the same position, we add their counts. This way, we read the input only once!

This is the idea:

.......S.......
.......1.......
......1^1......
......1.1......
.....1^2^1.....
.....1.2.1.....
....1^3^3^1....
...............

The key change from Part 1 is to use += to accumulate counts when timelines merge.

counts
    .entry(neighbor)
    .and_modify(|n| *n += count_above)
    .or_insert(count_above);

By summing the numbers in the last row, I get the number of timelines. The second star is mine!

Day 8

Puzzle | Code

We start the day by connecting 3D junction boxes using the minimum cable. First, I need a way to order pairs based on their distance. I’ll use a min_heap, so that I just need to pop it, and I’ll always get the smallest pair.

After that, I got a bit stuck, and I had to do some research to understand the best way to represent the circuit. I stumbled upon the Union-Find data structure, which perfectly suits my case!

Together with the min_heap, this is essentially Kruskal’s algorithm for Minimum Spanning Tree.

There are only examples in Python or other languages, so I had to reimplement it in Rust (although a crate was available, I decided to do it myself, a good learning experience).

Star 1 achieved!

For Part 2, I need to find the final connection that merges everything into a single circuit. It turns out to be an extension of the existing part, so I iteratively track the last pair processed, and I use it when we reach exactly one component.

Day 9

Puzzle | Code

Today’s first part was unexpectedly very easy. We have a set of coordinates, and based on them, we need to find the largest rectangle that can be formed (by measuring its area). Less than 10 lines of code, and part 1 is complete.

Quite astonished, I go ahead, and I understand why part 1 was easy! We still need to find the largest rectangle, but for it to be valid, it must stay within certain boundaries. These boundaries are defined by the input coordinates, which describe a rectilinear polygon.

So, in a nutshell, a rectangle is valid if it’s all contained in the polygon. Wow :D

My approach was to build a list of both horizontal and vertical edges with their coordinates: basically, store all the segments that make up the polygon perimeter.

After doing that, it’s easy to determine if a rectangle is valid or not: we can check the coordinates and see if any edge crosses our rectangle or not. It’s important to verify both Xs and Ys to ensure it does.

I asked the AI to visualize what I did:

        x_min           edge.x          x_max
          │               │               │
          ▼               ▼               ▼
     ┌────────────────────│───────────────┐  ← y_min
     │                    │               │
     │    RECTANGLE       │ ← EDGE        │
     │                    │   cuts        │
     │                    │   through!    │
     └────────────────────│───────────────┘  ← y_max
                          │

Second star achieved.

Final note: I found this Reddit comment suggesting plotting the polygon as SVG, and it was fun! The polygon turned out to be a pixelated circle with a horizontal bar through the middle, like the Greek letter Θ!

Here’s the SVG if you want to visualize it:

<svg viewBox="0 0 100000 100000" xmlns="http://www.w3.org/2000/svg"><path d="M 98437,50292 L 98437,51500 L 97797,51500 L 97797,52727 L 98037,52727 L 98037,53928 L 97771,53928 L 97771,55105 L 97366,55105 L 97366,56334 L 97448,56334 L 97448,57559 L 97428,57559 L 97428,58674 L 96769,58674 L 96769,59844 L 96478,59844 L 96478,61244 L 97162,61244 L 97162,62215 L 96031,62215 L 96031,63421 L 95855,63421 L 95855,64506 L 95276,64506 L 95276,65935 L 95716,65935 L 95716,67069 L 95248,67069 L 95248,68136 L 94619,68136 L 94619,69250 L 94121,69250 L 94121,70148 L 93161,70148 L 93161,71453 L 93075,71453 L 93075,72705 L 92834,72705 L 92834,73405 L 91574,73405 L 91574,74837 L 91615,74837 L 91615,75887 L 90978,75887 L 90978,76473 L 89646,76473 L 89646,77521 L 89036,77521 L 89036,78516 L 88347,78516 L 88347,79846 L 88080,79846 L 88080,80564 L 87025,80564 L 87025,81364 L 86091,81364 L 86091,82231 L 85245,82231 L 85245,82958 L 84255,82958 L 84255,84102 L 83693,84102 L 83693,84915 L 82790,84915 L 82790,85808 L 81961,85808 L 81961,86984 L 81364,86984 L 81364,87745 L 80399,87745 L 80399,87830 L 78919,87830 L 78919,89209 L 78428,89209 L 78428,89411 L 77079,89411 L 77079,90156 L 76118,90156 L 76118,91046 L 75241,91046 L 75241,91092 L 73856,91092 L 73856,92194 L 73087,92194 L 73087,92254 L 71748,92254 L 71748,93198 L 70866,93198 L 70866,93964 L 69879,93964 L 69879,93871 L 68513,93871 L 68513,94876 L 67612,94876 L 67612,95156 L 66414,95156 L 66414,95214 L 65150,95214 L 65150,96207 L 64191,96207 L 64191,95819 L 62811,95819 L 62811,96678 L 61785,96678 L 61785,96356 L 60462,96356 L 60462,97549 L 59472,97549 L 59472,96821 L 58100,96821 L 58100,97397 L 56968,97397 L 56968,97464 L 55753,97464 L 55753,98165 L 54604,98165 L 54604,97641 L 53338,97641 L 53338,98079 L 52147,98079 L 52147,98021 L 50926,98021 L 50926,97675 L 49712,97675 L 49712,97578 L 48506,97578 L 48506,97622 L 47296,97622 L 47296,98304 L 46027,98304 L 46027,97973 L 44828,97973 L 44828,97753 L 43624,97753 L 43624,97413 L 42443,97413 L 42443,97575 L 41176,97575 L 41176,96788 L 40089,96788 L 40089,96483 L 38916,96483 L 38916,96743 L 37595,96743 L 37595,96010 L 36533,96010 L 36533,95487 L 35425,95487 L 35425,95070 L 34289,95070 L 34289,95234 L 32935,95234 L 32935,94560 L 31887,94560 L 31887,93582 L 30984,93582 L 30984,93546 L 29672,93546 L 29672,93272 L 28448,93272 L 28448,92041 L 27715,92041 L 27715,92162 L 26263,92162 L 26263,90977 L 25543,90977 L 25543,90912 L 24154,90912 L 24154,89816 L 23412,89816 L 23412,88998 L 22505,88998 L 22505,88911 L 21063,88911 L 21063,87550 L 20569,87550 L 20569,87275 L 19229,87275 L 19229,86106 L 18623,86106 L 18623,85672 L 17377,85672 L 17377,84344 L 16955,84344 L 16955,83704 L 15885,83704 L 15885,82873 L 14996,82873 L 14996,82114 L 14019,82114 L 14019,81256 L 13142,81256 L 13142,80160 L 12551,80160 L 12551,79384 L 11561,79384 L 11561,78070 L 11285,78070 L 11285,76927 L 10809,76927 L 10809,76070 L 9918,76070 L 9918,75397 L 8700,75397 L 8700,74245 L 8237,74245 L 8237,72887 L 8170,72887 L 8170,72131 L 6999,72131 L 6999,70851 L 6834,70851 L 6834,69638 L 6570,69638 L 6570,68679 L 5735,68679 L 5735,67593 L 5170,67593 L 5170,66229 L 5353,66229 L 5353,65258 L 4464,65258 L 4464,64209 L 3733,64209 L 3733,62801 L 4217,62801 L 4217,61760 L 3420,61760 L 3420,60547 L 3265,60547 L 3265,59451 L 2557,59451 L 2557,58176 L 2738,58176 L 2738,57000 L 2381,57000 L 2381,55749 L 2570,55749 L 2570,54582 L 2063,54582 L 2063,53347 L 2233,53347 L 2233,52155 L 1746,52155 L 1746,50922 L 2182,50922 L 2182,50269 L 94634,50269 L 94634,48484 L 1719,48484 L 1719,47296 L 2373,47296 L 2373,46090 L 2458,46090 L 2458,44878 L 2492,44878 L 2492,43686 L 2711,43686 L 2711,42478 L 2809,42478 L 2809,41286 L 3021,41286 L 3021,40000 L 2788,40000 L 2788,38887 L 3392,38887 L 3392,37615 L 3331,37615 L 3331,36519 L 3941,36519 L 3941,35370 L 4338,35370 L 4338,34262 L 4850,34262 L 4850,32946 L 4795,32946 L 4795,32097 L 5956,32097 L 5956,30998 L 6448,30998 L 6448,29851 L 6837,29851 L 6837,28512 L 6855,28512 L 6855,27567 L 7680,27567 L 7680,26604 L 8442,26604 L 8442,25509 L 8964,25509 L 8964,24108 L 9013,24108 L 9013,23257 L 9951,23257 L 9951,22611 L 11152,22611 L 11152,21536 L 11724,21536 L 11724,20568 L 12447,20568 L 12447,19491 L 13042,19491 L 13042,18395 L 13632,18395 L 13632,17693 L 14672,17693 L 14672,16825 L 15519,16825 L 15519,16012 L 16420,16012 L 16420,15069 L 17195,15069 L 17195,14227 L 18070,14227 L 18070,13699 L 19215,13699 L 19215,12364 L 19688,12364 L 19688,11739 L 20752,11739 L 20752,11233 L 21891,11233 L 21891,10261 L 22695,10261 L 22695,9429 L 23612,9429 L 23612,8880 L 24713,8880 L 24713,8740 L 26046,8740 L 26046,7934 L 26982,7934 L 26982,7745 L 28251,7745 L 28251,7019 L 29237,7019 L 29237,6237 L 30211,6237 L 30211,5612 L 31268,5612 L 31268,5046 L 32357,5046 L 32357,5239 L 33729,5239 L 33729,4170 L 34643,4170 L 34643,4162 L 35922,4162 L 35922,3441 L 36981,3441 L 36981,3683 L 38305,3683 L 38305,2779 L 39342,2779 L 39342,2488 L 40534,2488 L 40534,2525 L 41786,2525 L 41786,2326 L 42991,2326 L 42991,2810 L 44279,2810 L 44279,2520 L 45461,2520 L 45461,2582 L 46676,2582 L 46676,2211 L 47865,2211 L 47865,1675 L 49067,1675 L 49067,1590 L 50292,1590 L 50292,2462 L 51491,2462 L 51491,1830 L 52734,1830 L 52734,2237 L 53927,2237 L 53927,2419 L 55128,2419 L 55128,2124 L 56391,2124 L 56391,2754 L 57530,2754 L 57530,3081 L 58702,3081 L 58702,2859 L 59984,2859 L 59984,3790 L 61017,3790 L 61017,4021 L 62201,4021 L 62201,4358 L 63358,4358 L 63358,4579 L 64552,4579 L 64552,4574 L 65833,4574 L 65833,4780 L 67058,4780 L 67058,5901 L 67924,5901 L 67924,6249 L 69088,6249 L 69088,6798 L 70166,6798 L 70166,7033 L 71398,7033 L 71398,7890 L 72320,7890 L 72320,7779 L 73768,7779 L 73768,9015 L 74460,9015 L 74460,9576 L 75536,9576 L 75536,9983 L 76720,9983 L 76720,10633 L 77754,10633 L 77754,11729 L 78459,11729 L 78459,12394 L 79473,12394 L 79473,12882 L 80640,12882 L 80640,13967 L 81312,13967 L 81312,14780 L 82208,14780 L 82208,15616 L 83081,15616 L 83081,16249 L 84160,16249 L 84160,17375 L 84738,17375 L 84738,18041 L 85805,18041 L 85805,19061 L 86482,19061 L 86482,19997 L 87251,19997 L 87251,20978 L 87963,20978 L 87963,21968 L 88660,21968 L 88660,22714 L 89711,22714 L 89711,23693 L 90444,23693 L 90444,24597 L 91308,24597 L 91308,25736 L 91793,25736 L 91793,26817 L 92368,26817 L 92368,27941 L 92857,27941 L 92857,29146 L 93169,29146 L 93169,30280 L 93609,30280 L 93609,31500 L 93836,31500 L 93836,32536 L 94497,32536 L 94497,33560 L 95225,33560 L 95225,34896 L 95074,34896 L 95074,36011 L 95544,36011 L 95544,37018 L 96426,37018 L 96426,38318 L 96267,38318 L 96267,39474 L 96633,39474 L 96633,40536 L 97505,40536 L 97505,41862 L 97033,41862 L 97033,43038 L 97349,43038 L 97349,44240 L 97510,44240 L 97510,45415 L 97961,45415 L 97961,46631 L 98067,46631 L 98067,47852 L 98084,47852 L 98084,49081 L 97602,49081 L 97602,50292 Z" fill="none" stroke="black" stroke-width="200"/></svg>

Day 10

Puzzle | Code

I was almost worried that this year, graph searching algorithms were not present. Luckily, I was proved wrong!

The first part today is very easy. We’re given a set of lights (some on, some off) and several buttons. Each button toggles a specific subset of lights, and pressing it flips the state of those lights. The goal is to find the minimum number of button presses to reach a target light configuration.

After some regular expression duties to parse the input into a usable form, I converted the light input strings into boolean vectors so I could invert them easily.

By applying BFS, I earned the first star!

Energized by that, I went to the second part, and I was so happy that my approach would have worked the same; I just needed to change how to calculate the successors for the BFS algorithm. Amazing, a free star!

Done the work, I execute the program and… wait… wait… wait… wait… It’s incredibly slow, oh no! After digging a bit and peeking at Reddit, I saw that many people used a solver; someone mentioned Gaussian Elimination and other math techniques. BFS can’t scale because the state space grows exponentially.

Rather than spending hours learning a new mathematical domain, I’m choosing to move on and perhaps revisit this puzzle later when I have more time to dive into the theory. Sometimes knowing when to skip is part of the experience!

Day 11

Puzzle | Code

We are on the second-to-last day, and I’m very surprised because both parts turned out to be straightforward once you spot the right approach!

The first part requires us to count the number of paths in a directed acyclic graph (DAG) from a starting node to an end node. The count_paths from the pathfinding crate handles this perfectly! Game over!

The second part looks much harder at first: we need to count paths from A to B that pass through two specific nodes (in any order). The trick is that in a DAG, the number of paths through intermediate nodes can be computed by breaking them into segments and multiplying the subpath counts. For example, paths A→X→Y→B equals paths(A→X) * paths(X→Y) * paths(Y→B). Since the two nodes can be visited in either order, we compute both orderings and sum the results.

Second star achieved!

Implementation note: I manually added the out node to the map with empty neighbors. This way, the goal check (n.name == "out") is easier and we avoid a bug in which the algorithm wouldn’t find any paths because the end node is not in the list (yeah, that happened today).

Day 12

Puzzle | Code

The last day has come, and the description clearly shows it’s crazy!

We’ve been given irregularly shaped gifts and several regions underneath Christmas trees. Each region has rules on how to fill it in; for example, use 3 gifts of the first type and two gifts of the third type. Gifts can be rotated and flipped. As an outcome, we should count the number of regions that fit the number of gifts specified in the rules.

Yes, as I said, a mind-blowing problem.

I stared at the screen for some time, but my mind stayed blank. Time to have a look at Reddit.

It seems that, intentionally or not, the gift shapes are trolling us; we should solve the puzzle mathematically, and it will work. Really?

Well, I wouldn’t spend days trying to figure out how to arrange gifts, so it’s worth giving it a try. We can assume each gift is regular and covers a 3x3 area, so we multiply 9 by the number of gifts and compare it to the available region area. We count the region if it’s bigger than the one occupied by the gifts:

    input
        .iter()
        .filter(|s| {
            let (size, gifts) = s.split_once(": ").unwrap();
            let gifts_size: usize = gifts
                .split_whitespace()
                .map(|n| n.parse::<usize>().unwrap())
                .sum();
            let (n1, n2) = size.split_once('x').unwrap();
            let grid_size = n1.parse::<usize>().unwrap() * n2.parse::<usize>().unwrap();
            grid_size >= gifts_size * 9
        })
        .count()

And that actually made me earn the first star of the day! There’s something weird about this challenge, because the same code doesn’t work for the example input 🤷

Like last year, unfortunately, I cannot go for the second star. I need all previous stars, but day 10 blocks me. My Advent of Code journey ends, but it’s been a fun ride!