9
votes
Day 7: Bridge Repair
Today's problem description: https://adventofcode.com/2024/day/7
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>
First day actually feeling good about my solution! I'm sure I'm still doing plenty of unidiomatic things, but most of the solution came together nicely. Two things I'm happy with:
The whole setup of Pharo with images is weird, but seems to really encourage doing things like messing with built-in classes. It's very fun to just decide "I guess Integers do that now!"
Smalltalk Solution
I'm actually pretty happy about my solutions today!
I got stuck for a while on part 2 and kept getting a value that was too big, but eventually it wasn't? I'm not even totally sure what I fixed. I guess I probably just had my
||
procedure wrong but then eventually it worked.Running the combined parts even after compiling to bytecode takes ~2.3 seconds so there are probably some optimizations I could do, but overall I like my solution.
Racket
Weird your solution is about 3 times faster than my part 2 but we're essentially doing the same work. Have to look closer to see if I'm doing something silly†.
You asked before about idiomatic use of Racket and there's one very common thing most developers do.
Instead of declaring your tests so that they run with your code, create a testing specific submodule e.g.
The convention is to use
test
for testing (and you can scatter several suchmodule+ test...)
declarations throughout your code - to keep the tests close to the procedures. You can run only the tests from the command line withraco test <path>
which runs the tests modules and racket <path> will run the main submodule (if declared) or the top level procedures.† Turns out I was. I used
(format "~a~a" x y)
for concatenation instead of the more obvious and specificstring-append
&string->number
combination. This one weird trick reduced my runtime by ~4s!Thanks!!
Tonight's was really easy compared to last night's (at least compared to my attempt at a non-brute for solution to that using graphs).
Most of the time it took me to do part one was just searching for the right iterator combination in itertools and most of the time to do part two was runtime (this is the second night Rhai's low performance showed up, 2:08 runtime).
Rhai Solution
This one was surprisingly simple. Part 2 especially required very, very minimal changes for me, at least as far as AWK goes.
Part 1
Simple recursive solution. I actually accidentally misinterpreted it initially and thought I was supposed to find the number of possibilities for each equation, but it runs quick enough that doing so doesn't really have any downside.
Part 2
This one was very simple, I just copied another function call into the chain on line 6 and deleted the operator (which automatically leads to concatenation in AWK). I finally felt the consequences of recursively finding all possible solutions here, but it still completes within a few seconds so I just let it be.
Python: Step-by-step explanation | full code
I stuck with my theme of "just do it literally", which largely keeps working. I solved part 1 recursively with a list of numbers and
op
s, adding the result to the front of the list until I had only a single number left.I did the same thing for part 2 and got ~22s of runtime. Rather than refactor, I used
multiprocessing
to parallelize the whole thing, getting me down to ~4s for both parts. Still slower than I'd like, but within reasonable bounds for minimal effort.I always love reading these write-ups when they show up in my RSS reader. :)
You misspelt "unacceptable" as "unnacaptible" in this one (unless that was deliberate to be whimsical); and while I'm at it, I've always been bothered that Day 19 of 2022 (which is my favorite AoC puzzle) has the algorithm name wrong. What you've described is called Beam search -- branch and bound generally does not limit the number of solutions considered unless a to-be-discarded solution can be proven inferior to the current best.
Thank you for saying so! I'm glad you're enjoying them.
Nope, I'm just a bad speller and was writing both late and in VSCode 😅 I'll get that fixed!
TIL! I'll update that as well. Appreciate the feedback.
Those changes will all go live when the next post is published.
I was so proud of myself for this one — I quickly came up with a solution, coded it, and ran it with the sample data and it worked the first time (no bugs)! Then I plugged in the actual puzzle input, and it gave the wrong answer.
That’s the most frustrating situation in AoC, because it means there’s some edge case in the full input that isn’t in the sample, and I can’t identify the problem. I spent a bunch of time yesterday digging through my results one line at a time, looking for anomalies, but it was fruitless as I don’t really know what I’m looking for. So now I’m just stuck, on Part 1.
I’ll bang on it some more today, maybe I’ll be able to spot something new after having slept on it.
I'M SO MAD.
I spent hours poring over the internal state of my code, line by line. Couldn't find a flaw anywhere. The solutions it found were consistently right, and the ones it flagged as invalid were also accurate. But the output was always wrong. Finally, in a last desperate gasp, I went back to the problem requirements and realized something...
Two operators only.
+
and*
.I can't believe I missed that... my implementation included
-
and/
. I feel like an idiot. I commented out the extra ones and it worked immediately. UGH. Okay, moving on to Part 2 now and (assuming I don't get stuck again) I'll add a top-level comment with my solutions.Update: Done.
Not at all happy with my solution today; it's really slow and just the first thing I thought of to do. These kinds of "place the operators" puzzles have always stumped me for some reason. I had to write a permutation finding procedure manually, since nothing like that comes with the compiler, but that didn't take too long.
Solution (Jai)
Thoughts
This one took about an hour to solve, twice as long as yesterday's, but bone-headed mistakes are gonna happen. I just have to accept that.
After the Hot Springs puzzle last year, I kind of assumed you had to memoize the recursive equation solve in part 2, but adding memoization added so much overhead that the runtime was 10x what it is without it. Now part 2 runs in about 0.8 seconds.
Part 1 (Rust)
Part 2 (Rust)
I started out on this thinking I could do a little better than brute force by doing the following:
+
's, so that each attempt would produce a generally increasing result*Buuuuut sorting the list would cause the operations to be applied in the wrong order. That was it for my optimization ideas, so brute force it is!
Both parts (Elixir)
Somewhat out of laziness, I generated the op permutations from an incrementing integer.
E.g. On a line with n numbers, I would need n-1 operators.
For part 1, this would require testing up to 2(n-1) op combos, so I used an integer starting at 0 and incrementing to that number. When it was say, 6, that would translate to binary
0 1 1
, and then I'd translate each digit to an operator:+ * *
Part 2 was almost the same, except the combos were 3(n-1) and I converted the integer to ternary: 6 ->
0 2 0
->+ || +
.Benchmarks
Is there any way to optimize this? (Besides the obvious parallelization by line)
It almost seems like a cryptography problem.
Running the whole thing in one process:
After switching to a
Task.async_stream
call to check each line concurrently:* (tbh I think this logic was flawed as well. For example, 5 + 1 = 6 and 5 * 1 = 5.)
TypeScript
Well this was embarrassing. I skimmed over the instructions too quickly and ended up missing an important detail. My logic was fine but that mistake prevented me from completing Part 1 for a rather long time.
Spoilers
On the bright side, my intuition that this was a job for a recursive function was spot-on. I almost never use those (apart from coding challenges like AoC) and they tend to break my brain, so I expected to wrestle with the implementation for a while. I'm actually quite pleased that I nailed it right away! I must be getting the hang of these.
My function evaluates the first two operands in the list, and loops through the array of available operations to create permutations. (Originally this array included
-
and/
but I had to sheepishly remove them when I realized they were out of scope.) For each permutation, the function calculates the result of that operation and calls itself with[result, ...rest]
as the new list of operands. When the function sees that only one element is in that array, it accepts that as the final result for the current recursed permutation, and it compares that against the expected number.Part 2 was the same logic, I just added a new concatenation operation. Thankfully my approach of representing the available operations as functions in an array was well suited for this, and I was able to complete it within a couple minutes.
Parts 1 and 2 (TypeScript)