12
votes
Day 8: Haunted Wasteland
Today's problem description: https://adventofcode.com/2023/day/8
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++
I really disliked Part 2.
Part 2 Gripes
So the ultimate solution here is pretty simple, since it's just taking all the loop lengths and getting their LCM.
However, there are so many edge cases that are possible here that you couldn't logically arrive at a quick solution from the problem description and must analyze the input to see what's going on.
For example, from what I can tell the following input is entirely valid:
You can check to see that in this case you never escape!
There are more complex cases than that, though:
where the solution requires the Chinese Remainder Theorem to solve programmatically.
And there are cases even worse than that...
To make matters worse, they used the same LCM idea in 2019 and had the problem worded in such a way that you could arrive at the LCM solution logically and prove that it worked, rather than guessing at it from the specific input you're given. They got it right already! Was this really the best way to bring out this idea again?
I get that sometimes hacking the input is its own type of cleverness, but this was a significant break from the convention of the other problems and there was no indication this would the right way to go.
Part 1 Solution
The problem here is straightforward, which is to just follow the path from AAA until it reaches ZZZ. Of course, that leaves the full implementation to you.Implementation notes:
Aside from the almost comedically bad string parsing (even after cleaning up),
there are a couple of dumb implementation details that I love,
like converting a string to an int by just sticking the whole thing in a map or by dynamically resizing the array whenever we need extra elements.
After parsing all the strings, it's relatively easy to get the answer out.
If anyone has a better way to parse strings (that doesn't involve a library) I would be happy to hear it.
Somehow I suspect this might not exist, because C++ tends to be the sort of language that makes you do that yourself.
Part 1 Code
Part 2 Code
In Go and Python, I get a lot of mileage out of
string.split
for parsing. In my Day 2 solution, I break the line down by:
,;
,,
,I haven't written C++ in a while, so when I was looking to see what the string split capability was, I found that the Man Himself commented on token parsing at #5 in this article that it's only becomes neatly available in c++20.
Especially if you are splitting on single tokens, it's pretty easy to implement the split yourself, which as you noted, is where C++ often leaves you with strings. I think this is a product of the era that produced C++ - much more concern about minimizing resource utilization and more focus on numeric computation than on UIs (which generally means more parsing and generating strings for human interactions).
I'll also note that today's solution is fixed width, so you can just
substr
the values directly out the string without all the token parsing.Also, on your gripes about Part 2, I think AoC suffers a little bit from the fact that we know it's constructed to have a (relatively) simple solution. If there was something that would make it so that every solution was super-complicated or time-consuming to solve, they would have designed that out. This is the same thing I run into doing puzzles on lichess. I'm pretty good at them, but I'm not that good at regular chess because in the puzzle, I know there's a clever move I'm supposed to look for but in a game (as in real life coding) there is no such guarantee.
For part 1,
I'll admit now that part of my hideous string parsing is what happens when some part of my brain goes "but you must think of the general case" but only for things like input string length, leading to parsing ideas like "remove the last element which we know to be ")" but only reference the possibly non-fixed (even though we know it's fixed) string size when doing so".
I was already doing some fiddling with stringstream but I didn't know about split_view, so I'll look into that. At first glance it looks like it only accepts one delimiter, but that might still be useful in the future if I have to deal with commas or some related non-whitespace thing.
Part 2 discussion, with spoilers
You're right that Advent of Code often simplifies the problem implicitly in order to allow for simpler solutions.
To be more specific, my problem with Part 2 is that the simplified input is artificial to the point of absurdity.
For contrast, in Day 5 the maps given in the input were bijective (I think) even though there was no indication that this would be true in the problem (and indeed it was easy to create a non-bijective map). However, this idea that those maps will be one-to-one is very intuitive and is clearly shown by the example input, so it felt like a reasonable assumption to make.
In addition, there is a nice general solution that covers non-bijective maps. It's not as simple as the specialized solution, but it's still reasonable and thinking about it makes the problem more interesting.
However, the simplest type of graphs I can think of where the intended solution works is...
"Graphs in which the first node is not in a cycle, but immediately enters a cycle where, looking at the state graph, only the last element in the cycle (before looping) has Z as a suffix"
Or (closely related but not quite the same):
"Graphs in which the first step from the first node enters a cycle whose length is a multiple of the instruction length and only the last element in the cycle has Z as a suffix"
(There are other graphs for which this works but I think they're even more complicated to describe)
This is plainly ridiculous. To steal your chess analogy, it's like seeing a move and thinking "oh, this tactic would work if only they took with the queen, but obviously they won't because they can just block the threat with another piece". Except the tactic maker does intend have them take with the queen, because only then could the tactic feature a Windmillwe're really stretching the analogy here you get the point.
I was going to say that upon writing this I realized that the example input follows that description, but it doesn't! The 22A cycle has period 3 (the instructions are LR, length 2), clearly only doing so because all the L/R choices are the same.
Note on the example input
The example input doesn't present a simpler class of graphs, because the state after 1 move is 22B to move Right but the state after 4 moves is 22B to move Left. It's entirely possible for you to arrive at the same node but take a different path because you're at a different point in the instruction set. It only happens to be true here because L/R are the same everywhere, and we would need to add even more conditions to the graph if we wanted to quantify that.
In addition, there are several very similar classes of graphs with different solutions.
For example, removing the requirement of "the node with Z is at the end of the loop" adds in the need for the Chinese Remainder Theorem. Removing "only one node with Z in the cycle" adds a ton of complexity. Even changing the janky "the first node is not in a cycle but its first move enters the cycle" to "the first node is the first node of the cycle" will at least require the solution to subtract one, but they didn't even do that.
Advent of code does often simplify its problem sets, and most of the time it does so pretty well. But this one feels like it used "simplify the input" to paper over bad problem design.
I agree with your gripes. But if you've done the previous years of Advent of Code, you'll see there is (IMO) at least 1 problem every year, where making certain assumptions about the input allow you to solve the problem in a trivial amount of time. That wouldn't work for a general arbitrary input.
I dislike it as well but it's not unexpected for Advent of Code.
Easily my favorite day so far.
spoiler part 2
I'm still using Python 3.8 which doesn't have `math.lcm` built in, so I tried using numpy, but that gave me the wrong answer. So...I printed the numbers and fed it into Wolfram Alpha and that was right. Gonna update to 3.11 so it works out better after this.Solutions:
Part 1
Part 2
Didn't realize that was missing from the stdlib for so long, but thankfully the version of PyPy I'm using has it.
Edit: Are the
2000
and2000000
multipliers you use just arbitrary large numbers? If you look at my solutions there's a neat Python-specific trick for this.yep just large numbers. that's a neat built-in, thanks!
You can also get around that particular omission with a neat identity!
spoilers
Today's problem reminded me of 2022's Day 17, which was a good one! Here are my Python solutions:
Part 1
Python's
itertools.cycle
is really useful here.Let my CPU cook for a few minutes before turning on my brain and thinking. I got to break out my main for loop into a generator function which is always nice.
Part 2
Really fun problem, and felt just about the right level of difficulty for this far into the month. The solution was clever, but not too hard to figure out - I like solutions like that, since they make me feel smart ;)
I solved the problem a while before posting this, but took some time to assemble a nice program that solves both parts at once and to write actual input parsing (I initially just copied the input into my program and used a regex to convert it to a Python dictionary). I got top 400 on part 1, but unfortunately wasn't quite so fast doing part 2.
(Also, yes, I know the input parsing is horrible. It's just Advent of Code though, it's fine I promise)
Solution
I tried to learn Ruby over half a decade ago, but I found that it mostly overlap Python in use cases so I didn't get to use Ruby much and forgot almost everything about it. Looking at the problem it seems doable in Ruby even though I don't know how to write anything in Ruby now. Surprisingly, I think this is one of the fastest day I've completed so far.
Part 1
Part 2
I don't like math problem posing as programming problem...
Language: Python
Part 1
First part was very straight-forward: read the input in and simulate the navigation. Taking advantage of itertools.cycle made cycling through the instructions very easy :]
Part 2
The second part was also straight-forward: locate the ghosts, and then navigate from each ghost until you hit an element that ends with a 'Z'. The trick to avoid brute-forcing is to realize that the ghosts will cycle (ie. repeat the same paths) if they all don't land on an element that ends with a 'Z' at the same time. To solve the problem, you just ened to calculate the steps for each ghost to reach an endpoint and then compute the lowest common multiple of those counts. Fortunately, Python has
math.lcm
, so not much code was needed!GitHub Repo
Re: Part 2
Question
Oh wow, that's so much simpler than what I did.
I'm not so good at math, so I'm having trouble understanding why you can stop at the first node that ends in 'Z', which you're doing if I'm not reading the Python code wrongly.
What if there are multiple Z-nodes in the cycle, and you don't happen to need to be stopped at the first one, but, say, at the second one, when all other paths terminate? I reckon there's probably a mathematical reason for this case being impossible, but right now it seems weird to me. Can you enlighten me?
Response
Honestly, I'm not sure if there is a mathematical reason (at least I didn't have one in mind). I sort have just assumed that once it hits a Z-node that it cycles through the same sequence. If this assumption didn't hold, then you are right... my solution would not always work.
So perhaps better lucky than good :]
First a joke:
What's a Go programmer's favorite fruit?
main.go
(in my defense, it is late and I am tired).
Golang solution
Very pleasing escalation from p1 to p2. I spent some spare time today building a general tree node structure in prep for tree problems. So I was bummed about it being a graph problem. However, the graph for this was pretty simple to implement. I still think there will be at least one DFS problem so I'm ready with my shiny new hammer.
Spoilers part 2
I should not have wasted time on the naïve solution (doing the walk of the graph with all of the starts simultaneously, instead of doing them one by one and finding the LCM). I should know by now to look for the shortcut first.
I feel like I got a bit lucky that my first guess at part 2 worked.
Spoilers
Not looking at the input, I was worried there could have been cycles with multiple `XXZ` patterns at different periods, or offsets from starting in an initial state not included in the cycle. I know in at least one year a challenge required bringing out the Chinese remainder theorem.This day also got me to finally start looking at different data structures in Haskell. I found
Data.Map
, but was surprised to find it has lookup of O(log(n)). More than enough for today, but interesting nonetheless.Haskell Solution
I'm sure I'm still doing plenty of non-idiomatic goofiness, but that's what trying new things is for!
(Rust)
Thoughts
Well, that was fun. I tried to brute-force Part 2 at first, with puzzles being brute-forceable being a theme of the year so far, but nope.
I hate it when Part 2 asks you to do something in parallel, like the "release steam from vents together with an Elephant"-puzzle in one of the last years, because depending on how you structured part 1, that can be really difficult to rework. Thankfully that wasn't really the case for me here.
Part 1
(nom-Parser omitted for brevity)
Part 2
The algorithm is simple once you figured it out: find all cycles, compute the indices on which a given path arrives at a Z-node, and then find the least common multiple between all paths, because that is the first step at which all paths will land on a Z-node.
I finally got around to day 8 tonight. I'm only posting part 2 since it's really similar to part 1 and I'm really happy that I figured out...
Spoilers
that it was an LCM problem. I'm usually very bad at figuring these types of things out. And on the plus side I can add the `gcd` and `lcm` code to my utils :DParsing was also pretty easy with some simple regex today, which is very welcome. Okay, here's my code
Part 2
Because I didn't brute force the problem I was able to get the solution in less than a second which was like an early Christmas for me! Reading through some of the comments here it seems like today's part 2 is maybe controversial. Personally I thought it was fun, but I also lucked out a little bit with some assumptions and figuring out the secret earlier than usual.
I tried to do part 2 naively at first and when that worked on the example but not the input I tried to do the next simplest thing which then happened to work, but upon reading other comments I realized it wasn't "supposed" to work except the input was specially crafted for it to work.
Kotlin
Rust
Day 8
im not super happy with part 2, as to solve it I had to make some assumptions that were true in my input (and apparently everyone elses input) but not stated in the problem.
https://git.venberg.xyz/Gabe/advent_of_code_2023/src/branch/main/days/day08
Elixir
As many others have pointed out, part 2 wasn't very fun because I initially considered that each "ghost" might not always start on step 0 of its respective loop, thus making it far more difficult to calculate where they match up. It wasn't so bad once I figured out that they all start on step 0 and don't have any "sub-loops" / pass through multiple
__Z
labels, but those properties weren't made clear by the puzzle description.New lil' Math module
There have been a handful of puzzles that involve calculating GCD/LCM, and I always forget how, so I figured I'd create a reusable module for those and other slightly-more-than-basic math functions.
Parts 1 and 2
I've been stubbornly avoiding regexes for parsing inputs this year, because binary pattern matching is neat and weird. As long as the values you're trying to pull out of the string have a constant character length (variable length allowed if it's at the end of the string), binary pattern matching is sufficient.