14
votes
Day 4: Ceres Search
Today's problem description: https://adventofcode.com/2024/day/4
Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python
with any of the "short names" listed in this page of supported languages):
<details>
<summary>Part 1</summary>
```python
Your code here.
```
</details>
Advent of Code has a way of making you feel stupid, and, well, today I feel really, really stupid. For how simple the puzzle was, it took an embarrassingly long time to solve (almost 90 minutes).
Musings
I have a
Grid2D
helper class and tried to be fancy with it -- instead of just looping through the grid in eight different directions, I tried to get away with rotating and mirroring the grid, because I have helper methods for that sort of thing. That would have been faster if it had worked, but it didn't (because I had an off-by-one error in the actual counting), so I wasted 15 minutes on that before using the naive approach.Then I got stuck for a very long time on Part 2, because apparently bending the
MAS
around the corner isn't allowed, i.e.is invalid, and the puzzle description did not make that clear. Unfortunately it didn't appear in the test input either (which might have been deliberate), so I was left scratching my head for a good long while.
Part 1 (Rust)
Part 2 (Rust)
Python
Ported my handy
Point2D
class from Rust over to Python this year.Part 1
Point2D
Brute force, but it worked well enough. I found complex numbers useful here for storing and shifting coordinates. My Python solutions:
Part 1
Part 2
Grid problems! I pulled out the helper class I've used for a few years now, makes it a little easier to deal with various approaches to dealing with 2D grids; things like a method for doing something at each index of the grid, or handling out of bounds with a default value, that kind of thing.
Part 1 Ruby
Part 2 Ruby
Helper "Grid" class
I'm very much enjoying having a Point class with simple syntax, but I can't say it's making up for the rest of Pharo's syntax being kind of a pain. It's tough to go from:
for finding a diagonal string, to:
Maybe I'm just not finding the cleanest approaches, but it's frustrating.
I have, however, been enjoying the pressure to heavily decompose tasks and easily add tests. I feel like I'm leaving each day in a state that would be easily refactored or improved.
Pharo Smalltalk Solution
This was one problem where rushing it worked surprisingly to my favor. AWK solutions:
Part 1
My approach here was to locate X's and look for occurrences of "MAS" in all directions. I generally would have used loops for this, but I was in a rush so I just hard-coded everything. Quick and dirty.
Part 2
I was expecting to have to switch to a smarter approach, but this was even easier than part 1 for me. I locate A's and look around diagonally for M/S pairs.
Today's solution felt gross, but it's probably because I felt like I was hacking this one instead of coming up with a good solution. I still feel like I'm abusing Python instead of letting it work for me.
Part 1 | Part 2
I got tired of copy/pasting code and I've been publishing my solutions on GitHub anyways so shrug
I had some fun with this one and ended up writing solvers to find arbitrary strings in the word search, not just
XMAS
.Spoilers
I used cardinal and ordinal directions to describe traversal through the puzzle, that was a fun way to frame it.
My solution for Part 1 loops through the rows and columns, looking for the first letter (
X
). When it finds one, it does some quick boundary checks to see which directions it should bother searching in, from that starting position. Once the searchable directions are determined, then walk each direction one step at a time, validating the letter each step.Part 2 was pretty wild, especially since I'd already committed to searching for strings of unknown length. Though we have to assume an odd number of chars so we can orbit around the one in the center. First my solution has to figure out the midpoint of the search string. Then like in Part 1 it loops through the puzzle, looking for that letter — though it only looks through an interior portion of the puzzle where the letter positions are valid for the middle of an X shape.
Then it starts traversing the diagonal axes, first NW-SE, then NE-SW. For each axis it walks from the middle of the word out toward the edges. At each step it's detecting whether the current letter is a valid next letter for the word, in either possible direction (i.e., whether the word is spelled backward or forward along this axis). If both directions have been ruled out, the process short-circuits and skips to looking for a new middle letter. This was a good use case for JavaScript's labeled statements which I almost never get to use! If a full word is found — twice, once for each axis — that's considered a found X shape and it's counted.
Parts 1 and 2 (TypeScript)
This one was kind of uninteresting to me. I knew how I was going to solve it nearly instantly upon reading the problem text. Maybe it's not a clever solution, but then I don't know that this puzzle really deserves a clever solution. It's just a word search. Part 2 was even easier - I solved it in only a couple minutes.
Solution (Jai)
My initial solution isn't as nice as I'd like it to be, but it works.
I might go back over it tomorrow and add some functions to my utils module to make working with grids easier.
Rhai Solution
Edit: I added grid and point types and redid my solution using them.
Rhai Solution
Python: Step-by-step explanation | full code
My grid parsing and
neighbors
function came in clutch today. For part 1, I could find everyX
, see if it has a neighboringM
, and calculate the direction we stepped, and step 2 more times to check forA
andS
.For part 2, I tweaked
neighbors
to be able to do diagonals only. It was the same basic approach though: start withA
, find a diagonalM
, and then go the opposite way from theA
to check forS
. If there are exactly 2 of those, we're good to go!Mostly today's writeup covered how my grid system works. I end up using it a ton, so it's nice to have a cogent description of it somewhere.
I do wonder if I should make an actual
NamedTuple
class for myGridPoint
s instead of just a type alias. Is mostly compatible, but will also let me add and subtract via method overrides instead of needing dedicated aadd_points(a: GridPoint, b: GridPoint) -> GridPoint
function. Ah well; that sounds like work for the off season!Part 1 I could've made nicer and I'm sure it could be better, but I think my part 2 is fairly nice
Racket
Elixir
Pattern matching was useful for this one. I've also been iterating on a
Grid
module/data structure over several years of AoC puzzles, so I had an easy time parsing the input and moving around the grid.My approach for both parts was to find all the "starting points" of the desired patterns—
X
's for part 1 andA
's for part 2. Then, I checked locally around each one for matching patterns. EachX
could form up to 8XMAS
's, eachA
formed either 0 or 1 "X-MAS".Both parts
Just the solution code—here is my Grid module, if you're interested.
A fun thing: I updated
Grid.lines_of_values
to be optionally lazy-evaluating for this puzzle. Using the lazy version makes my part 1 solution run 10x faster! (It avoids unnecessary extra cell lookups.)