The Advent of Code 2023 day 8 input has very special properties that are not mentioned in the problem description, yet they greatly simplify solving part 2. I first tried solving the more general problem, but I gave up when I had to write the code for practical and aesthetic reasons. Instead, I solved the specific problem, which was far more trivial.
After my solution descriptions and discussing why I gave up on the general problem, I give an extended discussion about how all the nasty business I endured result from issues of the problem itself (and not because of me).
After struggling with the general solution, I ended up writing a solution for the problem using the unmentioned special properties of the input, and moving on with my life. However, rather than accepting a complete failure, I document here my algorithm to count this endeavor as a partial success.
The special properties are described in the rest of this paragraph — containing spoilers for the problem, naturally. The connected components of the given graph (of the specified state transition machine) are double-thick circles of circumference divisible by the length of the instruction list. The double-thickness merely records whether the last instruction was left or right. For each circle, there is a starting node that points to the nodes directly after the ending node. This means that we merely need to take the least common multiple of the circumferences of the circles, measured by how many steps it takes to reach an ending node from a starting node.
It is hard to emphasize how much these properties trivialize the problem.
The day8_alt*.scm
files are partial attempts to solve the more general problem in a somewhat efficient manner, which is as follows. Feel free to skim this section.
Let the state space
For each starting point A
beginning at instruction Z
), remembering their distances from the starting point. By marking points as visited, we terminate the search after revisiting a point, separating the data into preperiodic and periodic parts
Finally, we unify our data. If there are no starting points, we clearly need reduce
on the above data. Given partial solutions
First, we define some helpful notation and data. We use the common notation | l\in L}$. For any partial solution
Let
We compute | (r,t)\in U \text{ and } r \equiv t \mod g} + m_y$, where the final addition is performed in the integers. (The extended Euclidean algorithm can compute extra data to simplify this calculation.) The formula on the left side gives solutions modulo
Once we reduce our list of partial solutions to a triple
If the graph is small enough, it can always be preprocessed. On the other hand, if the number of starting points is small and the trajectories sparse, this is not very efficient.
The final step where we unify partial solutions is quite egregrious in its complexity, needing at worst
Even though attempts to code this solution were nearly complete, I gave up because I was struggling against Guile's ergonomics to both achieve good performance and keep my code elegant and readable, which was not fun for me at all. For example, I always had to be aware of exactly what sort of container I was working with and all the quirks of their APIs, which had me constantly searching countless pages of the Guile manual. Perhaps I have been spoiled by other languages like C++ and Rust, which have excellent collections libraries (especially the latter, whose iterator module std::iter
is without equal), and Haskell, whose typeclasses and other features make committing my algorithm to code extremely easy. (I found myself greatly missing Functor
and Maybe
, both which also have serviceable analogues in Rust). Besides, I had already solved it on paper, so this was merely a (rather painful) formality.
In any domain, when clearly defining a problem (as opposed to discovering a problem), a solution to that problem should tautologically be for the problem. For purely algorithmic programming problems, one also wants desirable properties of the solution, such as good best/average/worst case time/space complexity.
Implicit in these desired properties is the input space the cases are drawn from, and that good solutions should work well on most or all of the possible inputs. Advent of Code problems seem to cleanly fall under the framework of algorithmic programming problems: a problem description, an expectation that you write a solution to the problem, and a large input that is used to verify you (probably) have a solution. (The alternatives, formal verification or running the program on hidden inputs, are respectively absurd or likely too costly for the authors.) However, in Advent of Code 2023 day 8 part 2, the solution drastically simplifies if one takes into account properties of the particular input in the problem statement. This violates the principle that the solution should work well on most/all inputs, not just the one input.
One could argue that most problems in practice (i.e. ones engineers encounter) are initially poorly defined and often require research and investigation to better define them. Under this view, engineering problems require sufficient investigation to deliver better and more cost-efficient solutions. From this point of view, Advent of Code problems do not ask you to write an algorithm to solve the described problem, but instead ask you to produce the correct output for the one given input. Indeed, this is exactly what is asked of you.
In my experience, however, this is not how most Advent of Code problems typically work. They are not the sort of engineering problems that software engineers typically spend their time solving. They are problems that require one to devise an algorithm — more of a mathematics or computer science problem than an engineering problem. I'm perfectly fine with solving engineering problems, but the appearance and history of Advent of Code leads one to believe that the offered problems are of the algorithmic type, not of the engineering type.
Furthermore, if one were to solve an Advent of Code problem with a quick-and-dirty script in a browser console, this would quite arguably not be in the spirit of the puzzle. Yes, this method literally solves the problem — getting the output for the singular input — but it corrodes the purity of the exercise. One is no longer writing clever solutions to precisely defined problems, but is instead merely chasing the goal of completion, of golden stars. At the risk of sounding silly, you can't sit back and know that you've written a solution that physically embodies the universal structures of algorithms and computation.
And that knowledge, that feeling of understanding a timeless problem and proving it by committing it to code, is pretty fun.