9
votes
Day 9: Mirage Maintenance
Today's problem description: https://adventofcode.com/2023/day/9
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>
C++
A surprisingly conventional problem here, but I'm always down for a bit of math.
Part 1 Solution
This one is a pretty straightforward problem, and one that you might have seen before in math somewhere.In this case, rather than stopping once we see all zeros it's easier to just go all the way to when it's just a single digit (which will certainly be zero if a previous row is zero, and if not then the next row will contain no digits and be "all zeros" anyway).
After that, we can just take the last element of each array to build the next column in the consecutive differences.
Part 2 Solution
Funnily enough, the simplest solution (code-wise) for this is just to reverse the input array. Proving that this solution works is pretty interesting in my opinion, but the most obvious proof probably involves a lot of algebra which doesn't seem all that fun.Even if you don't go through the effort of proving that, if you kept all those consecutive difference arrays you can always just go back and extract the first values to go backwards one step.
Part 1/2 Code
Bonus Observations
The most obvious way to achieve what the problem is asking is just to reimplement the algorithm being described, and it's most likely the simplest. But what if I told you it's not asymptotically optimal?We might hope that this algorithm terminates quickly for all inputs, but in fact you can find inputs that force it to run in O(n^2). Examples include
1,2,4,8,16,...,2^n
or0,1,0,-1,0,...,sin(pi*n/2)
.The question is then:
Given a list of N elements, can you find the next value with this consecutive-differences algorithm in faster than quadratic time?
I already gave away that the answer is yes, but people smarter than me have shown connections to much cooler math in order to get there.
The big leap here is to realize that this problem is equivalent to Polynomial Interpolation which might feel crazy if you haven't seen it before.
The basic idea is this: Look at the consecutive differences for polynomials:
n:
n^2:
n^3:
And you'll notice that n takes 1 step to reach fixed consecutive differences, n^2 takes two, and n^3 takes 3. We might wonder whether this pattern continues, and it does! Starting from first principles takes forever, but just as a next step you should consider how consecutive differences looks when expressed with polynomials.
From this fact with polynomals we can work backwards, i.e. looking at the consecutive differences from bottom to top and re-adding polynomials to get the original list of numbers.
That would look something like
And working from the bottom means that this polynomial will have minimal degree (i.e. you can't magic a simpler polynomial using some other method).
Then, going back to our original problem, this is essentially transforming our list of numbers
a_0,a_1,a_2,a_3,...,a_n
into a set of coordinates(0,a_0),...,(n,a_n)
and finding the least degree polynomialP
whereP(x) = a_x
for0 <= x <= n
and this gives us our solutions for Part 1 and Part 2: The part 1 answer isP(n+1)
and the Part 2 answer isP(-1)
.Okay this all loops back around to that faster than quadratic solution because we have a much faster way to calculate a polynomial from its points: Lagrange Interpolation which is certified math by its math person name. Unfortunately the documentation on this is a little sketchy, but thanks to the ever cursed Fast Fourier Transform you can actually do this in O(n log^2(n)). The best source I found in my 1 minute of searching is here. I know that this algorithm works because I've used it before, but it's just really bad to describe without drowning in math.
As a bonus bonus, this also gives us a radically different way to prove that reversing the list works for Part 2:
Reinterpreting our list again as coordinates, we already know that there is some least degree polynomial
P
that will match every coordinate on the list, and that the next and previous values areP(n+1)
andP(-1)
, respectively.If we reverse our list, this list must also have a least degree polynomial (which we could call
Q
) that matches every coordinate on that list. But wait,Q(x) = P(n-x)
works, doesn't it?P
andQ
should have the same degree soP(n-x)
should work just as well to fit the description of such aQ
. Furthermore,Q(n+1) = P(n-(n+1)) = P(-1)
so the next element for the reversed array is indeed the previous element of the regular array.There's even more bonus by the way: something missing from this is whether the least degree polynomial was unique. We secretly assumed this fact during the reversal proof, but you can get sucked into even more different math (in this case matrices) because there's always more math.
Once again, Mathematica feels like cheating:
Part 1
Part 2
Yesterday I felt torn about looking up the LCM algorithm vs figuring it out myself. Nevertheless, the simplicity and compactness of the made my soul happy.
This was a fun one! I tried to apply my shortcut from part 1 to part 2, but since I got a little lucky with my part 1, I had to actually try to understand math for part 2.
Part 1
Part 2
I'm very happy with today's problem. With how some of the first few days went I was afraid this year would be really hard.
Rust
Day 9
Enjoyed this one also quite a bit
Part 1
Part 2
just added `line.reverse()`Well, this one was... weirdly easy. And part 2 was almost exactly the same difficulty as part 1. It took me about a minute. I feel this problem would be much better suited for around day 4.
Solution
I had the same feeling. Part 2 actually took me four minutes because I fat-fingered a 1 instead of a 0 when modifying the algorithm, and I had to debug that.
Now that I think about it, part of what made this problem feel so easy was that the entire solution was basically described in the problem text itself. I feel like that wasn't really necessary...
Golang solution
Weirdly easy, and not very satisfying part 2. My memory of past years was "you can't do part 2 the naive way, you have to find the algorithm trick that will make it scalable". So far we've only had one of those, and that was (only slightly) complicated because I didn't have an LCM library algo for Go. I wanted to go back and see what my submission times were like in previous years, but I changed by github ID, and apparently that wiped out my history on previous years. Bah.
Wow, this is easy. I kinda like this one.
Today's language is Java, and I hate the multiple ways to store things in a sequential order with different API surfaces. Two days ago I wrote C++23 and it didn't feel this confusing to use vector, array, span in the same code.
Nonetheless, good thing we now have stream and it felt modern. I was writing some Kotlin code right before the start, and it felt much, much more pleasant to use.
Surprisingly easy, the most trouble I had was failing to parse negative numbers. While trying to diagnose why my answer was wrong, I added some debug statements... that corrupted my data structure because it was mutable and I doubly modified them. After fixing that (by copying everything), I finally got to look at the stack and noticed I was missing all the negative numbers.
Kotlin
Nice. I was expecting to spend a couple of hours on this, since weekends are supposedly harder, but this is the first time I solved the entire puzzle in under 30 minutes. Of course that's still slow by leaderboard standards, but I'll take it.
(Rust)
Code
Was it funny to anyone else that we had to construct a pyramid of numbers in the desert?
For part 2 I just reversed the input array and handed that to
extrapolate()
.Language: Python
Part 1
Pretty straightforward. Took advantage of
itertools.pairwise
.Part 2
Only thing that changed from the first part was that I used
functools.reduce
to take the differences of the first elements of the generated sequences (rather than the sum of the last elements for Part 1).GitHub Repo
I guess they wanted me to be able to enjoy my Saturday? Once again, definitely could have been done simpler but I generally don't plan out my code in advance and just write as I read the problem.
Code
This was easier than I expected for a saturday puzzle, but pleasant to implement.
Part 1 - Ruby
Part 2
Simple day 9, so I decided to do a little recursion (with memoization) since most solutions here are iterative. My Python:
Part 1
Part 2
Same as part one but reverse the sequence first.
Elixir
Not much to say about this one, pretty straightforward. My only mistake was accidentally rerunning my part 1 solution and submitting that as my part 2 answer after seeing my tests pass.
Parts 1 and 2
I did a tiny bit of optimization by reversing the lists of numbers depending on which end I was interested in, so that I could do
hd(list)
(constant time) rather thanList.last(list)
(linear time). Probably could have avoided a bunch of near-duplicate code, or the non-tail-call recursion for part 2, but meh this works.Took me a while to figure out exactly how I wanted the recursion to work, but was pretty straightforward once I had that figured out. For part 2, I just reversed the input and fed it into part 1.
https://git.venberg.xyz/Gabe/advent_of_code_2023/src/branch/main/days/day09