14
votes
Day 10: Adapter Array
Today's problem description: https://adventofcode.com/2020/day/10
Join the Tildes private leaderboard! You can do that on this page, by entering join code 730956-de85ce0c
.
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>
I completely missed the start because I got distracted by Cyberpunk 2077. Whoo! But I took a nice break from shooting baddies to do some math and coding.
Day 10 Part A – Python
For part A I figured I would do something different and use a dataframe and take advantage of some of the built in functions. It worked pretty well.
Day 10 Part B – Python
After bashing my head against it for like half an hour, I ended up scrapping my dataframe so that I could do the computations I needed. Basically I look for the length of each 'streak' of consecutive adapters. These are ones that could possibly have one or more of them removed. I find the length of the different streaks and use it to to some multiplication at the end. I discovered that a streak of length '2' would give 2 options. A streak of length 3 would have 2^2 = 4 options, and a streak of length 4 would give 2^3 - 1 = 7 options. The minus 1 represents a jump of > 3, which our adapters don't allow, so it gets dropped. Thankfully we didn't have to deal with any streaks of length > 3.
Tips and Commentary
See if however you're storing your data has a built in way to sort it
For the love of god, do not try to brute force part B. It will take a LONG time. My answer was on the order of 24 trillion and yours might be higher if you're unlucky
Prime factorization was what helped me figure out part B. Wolfram Alpha can do prime factorization for you
2 = 2^1, 4 = 2^2, 8 = 2^3
8 - 1 = 7. Yes, this is actually a hint. I'm serious.
I finally got my recursive solution running, after realizing I can optimize away returning the array of all the combinations (because that ran through memory like you wouldn't believe) in favor of returning a total of the combinations. I kicked it off not long ago, and it's just passing 57 million combinations.
I cheated and peeked at your solution instead of doing the math myself, and I think my penance will be to leave this running on my crappy single-core 1.6gHz dev machine instead of refactoring. I hope it will be done before the competition is over...
edit: It won't. After running approximately 48 hours, I stopped it around sequence 24,062,200,000 -- at that rate, there's no way I'm brute-forcing that answer by the 25th. Plan B for penance: Work out the maths myself, instead of implementing someone else's answer. :)
There was a problem in AoC 2018 that required
spoilers
linked listsI actually ended up writing a proper solution before it finished running after researching how to do it, but still. Array solution worked eventually.
All that said, 57 million is a tiny fraction of a trillion. Hope it finishes 😬
That's both terrifying and impressive. I'm curious to know how long it will take
Using functools.lru_cache almost feels like cheating (both parts finish in 300 ms).
Part 2
I got the first sample for part 2 to work, but haven't finished working out the math to make my solution work correctly for the second sample and my real input.
discussion
By looping through the list of adapters three at a time, it's possible to find and remove all the unnecessary adapters (any time you see `a, b, c` where the difference between `a` and `c` is less/equal to 3, you can remove `b` from the set.This gets you the minimal valid solution, and a list/count of removable items. I believe it should be possible to use this information to directly compute the number of permutations very quickly, but I'm still trying to remember/derive the math for that.
It's also possible that I'm chasing a dead end and am completely off base.
Ruby
This one took me a long while, first part was easy enough, but I ended up getting stuck on the second part.
Part 1
Part 2
I started off trying to find the set of removable adapters and figure the permutations directly from there, but that wasn't working and I banged my head on that wall for a while, before stepping back and looking at each *group* of removable items. Any run of 2 or more single-steps brings a number of choices that grows in a predictable sequence.Put generally we have: 0 -> 1 (n/a no action); 1 -> 1 (not removable); 2 -> 2 (you can remove either); 3 -> 4 (remove any or all); 4 -> 7 (remove any, all, or two sets of three). This was enough to build out the rest of the sequence, with each new number being the sum of the last three. I ended up pre-computing the first 10 numbers in the sequence, but I'm pretty sure these inputs only actually need the values for 1-4.
Inspired by a streamer I like to watch VODs of after I finish each puzzle, I added a little test suite that checks my implementation against the provided sample cases before running it against the full input. It also adds benchmarking, but so far everything's been fast enough for that not to matter much. You can see it in the full implementation here.
Mit scheme.
Part 1 & 2
For part two I started out with a recursive memoized approach but then after messing with it I realized only that last three values could matter, so I keep a list of three values corresponding to the adapters (-3, -2, -1) relative of the current adapter. I shift it at the beginning of each iteration to correct it if the difference between the current and previous adapter is greater than one, and at the end by one.Python
Repo Link
Part 1
This was relatively straightforward once I understood it. Simply sort the adapters (including the 0 port and the max(adapters) + 3 device port) and then count the differences between each adapter.
Part 2
This took me a bit longer because I didn't want to simply brute-force it. I knew there was a dynamic programming solution, but I had to draw it out on a piece of paper before I saw the appropriate relationship.
The idea is that for each adapter, you simply need to look backwards and consider how many ways you could have arrived at that joltage. So the recurrence relationship is:
counts(adapter) = sum(counts(adapter - d) where d is 1, 2, or 3)
.Elixir
Always fun when you can solve the whole thing without a single branching expression (
if
,cond
,case
). Thanks, useful standard library functions!This one also runs pretty fast—65 μs for part 1 and 80 μs for part 2.
Elixir, parts 1 and 2
The first part seemed too easy, the second part I will admit I didn't have a good way to get the runtime down until I let myself follow in the footsteps of @weemadarthur and implemented memoization. I did try to parallelize first but I definitely did it wrong and spawned so many processes my computer locked up!
Part 1
Part 2 diff
I don't think I'll keep up with the performance comparisons, as I don't think they do much interesting. This one does chug a little bit though, I get the sense that my penchant for loops in recursive functions does a number on the Julia compiler. Either way, they are easy to come up with and perform well enough here.
This problem was really confusing for me to understand at first, though what it was actually asking wasn't complicated. Shows how much I need to improve my reading comprehension.
Repo link
Part 1
Standard, manual countingPart 2
Solution using memoization.Welp, this one is very spreadsheet friendly.
Sheets!
Part 1
Part 2
=IF(G4-G3<=3,H3,0)+
IF(G4-G2<=3,H2,0)+
IF(G4-G1<=3,H1,0)
Oh hey this is a good excuse to start contributing. Hi to Tildes!
C#
Part 1
Pretty boilerplate - not even super necessary to actually build the diffs array, but it means I can just throw LINQ at it and type faster.
Part 2
I believe this was my first use of dynamic programming since my algorithms class. This was a satisfying one - I'm pretty sure it was 10 minutes of thinking/whiteboard-sketching, then 5 minutes of typing and watching it work first time.
Today's was a good example of a puzzle that's a fun challenge without being tedious. Took me a while to figure out part 2 but thankfully, it spared me the hardcore combinatorics once I figured out the joke.
Part 1 seemed suspiciously easy, you could just sort the numbers and measure the gaps (the length of the explanation, suggesting something horribly complicated, was a bit mean, though). I'm pretty sure it was supposed to be a "hint" for part 2 and a rather good one. The moment I read "trillions" I knew the name of the game: looking for ways to cut the problem into smaller parts.
I noticed that all 3-gaps had to be traversed since you couldn't fill them with alternate routes. Further, there seemed to be no gaps of 2 and no sequences of 1-gaps longer than 4. Also a single 1-gap between two 3-gaps (or the start or the end of your list) didn't allow any alternative routes, either.
So this came down to counting the number of possible routes within those 2, 3 and 4-sequences of 1-gaps, which just happened to be (I counted manually) 2, 4 and 7. A sequence of two 1-gaps followed by a sequence four 1-gaps would allow 2 * 7 different routes and so on. All I had to do was count all those sequences and multiply by the amount of different routes each of them allows.
Part 1+2 (Python)
Rust
Yikes. I went with a recursive solution for this but needed to implement caching to get the runtime down. I wanted to use the
cached
crate but struggled massively with Rust's borrow checker to get that working. Finally figured out I could uselazy_static
to getcached
to work. Considering how atrocious my solution attempts originally looked, it cleaned up nicely.Solution
This was, by far, the least amount of fun I've had on one of these problems so far. Apart from first grappling with Javascript betraying my assumption of pass-by-value-by-default which led to recursive debugging hell, the combinatorics involved in actually divining the answer went completely over my head. After having burned literally 8 entire hours of brainpower at this point on this problem, I finally gave up and implemented the solution that @PapaNachos and @nothis outlined. Thanks for posting your tips and code, both of you. (And everyone else, of course!)
For the curious, my recursive solution ran for 48 hours on a single-core 1.6 gHz machine at 90%+ cpu usage and got to roughly 24 billion combinations. My answer using combinatorics math was just north of 124 trillion. It would have taken my computer 28.3 years (assuming constant speed and none of the parts burning out) to calculate that result.
I left both of my recursive implementations in my part 2 code, for the curious, or as a learning tool for new programmers. The first one I did (function named findSequences) was meant to return an array of all the combinations themselves, of which I could take .length to get the answer. This ran out of memory (4GB on my machine) after roughly 2.8 million combinations. The second one (function named findSequences2) instead foregoes storing the combinations in favor of just totaling them. If you'd like, you can see how fast it runs for you!
Part 1
Part 2