top of page

Advent of code in SPL - 2025 day 7

  • Writer: Gabriel Vasseur
    Gabriel Vasseur
  • Feb 27
  • 6 min read

Day 7 is here.


Part 1 - visual solution


One thing I've learned about myself with these challenges, is that I always want to make the solution as splunky as possible. For me that means starting with a stream of events, i.e. treating the challenge data as one event per line, and pretending they are indexed events in splunk. That's also why I wanted to use timed lookups in day 5. Of course, that kind of approach is not always ideal for this type of programming challenges, and it pays to think of the problem in a different (though less splunky) way. So with today's challenge we will go through a real journey.


To start with, we'll solve part 1 in a visual way, partly because I wanted to follow the description of the challenge closely (let's face it, it's kind of cool and unusual), partly because I didn't spend much time thinking about efficient ways to go about a solution (but we'll get there!).


We'll first use the demo data:


Then we can work on our beam propagator search. Let's first bring in the previous line:


Let's split them character by character, and mvexpand so we have one event per position. Note how we keep track of on which line and column we are with streamstats:


The idea is to look at each position, and alter it depending on what's above it or to the left or right accordingly. So let's first grab the information:


Note: we could get rid of the current and previous manifold, but we keep them for debugging purposes. We mvjoin them to make the output vertically denser.


Now we can evaluate the new value. There are probably better ways to do this, but this is what I ended up with:


Now we can put it together with stats list():


Starting to take shape!


All we need to do it write it back to the temporary lookup:


We can run this repeatedly and watch the beam split and propagate, just as in the example! It's clunky, but I find it pretty cool.


Now, what we need to keep track of is the number of splits. So we need to add a bit of logic:


Note how the appendpipe allows us to output the lookup without the split_count data but we can still see it in the results.


Finally we can push this to a single search with map:


Amazing. One improvement we can make is ignore lines with no splitters. They look cool but add nothing to the problem. Let's filter them out with regex and tackle the full data:


Once again we hit the annoying 100 limit for stats' list() command. If you remember in day 4 I showed a clever workaround. However, I am very grateful to Chris Kaye (a.k.a ITWhisperer) who taught me that stats has a limit option (which as far as I can tell is not documented anywhere). That's a much easier solution:


Hooray!


Part 1 revisited


With part 1 out of the way, my hopes were dashed when I read part 2 of the challenge. My initial instinct was that it required recursion. But later I learned that ITWhisperer had solved the challenge fully in SPL, which meant it was possible and I was on the wrong tracks.


So let's first try to rethink part 1 in a less visual and more practical way. Unfortunately, we need to move away from a stream of events (one event per line of challenge data) and rephrase the problem in one event.


My first thought was to have a multivalue listing each row, start with an initial state, and have nested loops:

  • an outer loop to go through each row in turn

  • and an inner loop to go through each splitter in the row and work out how it affects the state.

So before the loops it would start like this:


We now have a starting point (source) and a list of rows, each with splitters. It's a much more compact version of the challenge data.


The problem is you can't have nested loops in SPL. Worse, foreach in multivalue mode only allows the use of a single eval command in the loop.


BUT! In this case, we can actually process each splitter in turn without caring on which line it is, as long as we do them in order.


So we just need to get rid of the mvjoin command to "flatten" the splitter field into a simple multivalue:


So now we just need to figure out the loop code. To do this, I found it very helpful to build a play search on the side. After a few iterations I had this:


Let's plug it in our search:


That seems promising. All we have to do is keep track of the number of splits:


21 splits! That works!


We could note that state is a bit messy, with many duplicates. We could tidy it up with a mvdedup but that's not strictly necessary:


Let's try the full data:


Amazing!


It's a bit sad to abandon the streaming nature of SPL, but there's no denying that this is a much better solution. Now we should be in a better place to think of part 2.


Part 2


In part 1, we went through each splitter, and we kept track of all the positions for the beam in state:

  • State is a list of positions where there is a beam.

  • Each splitter is potentially modifying state.

  • We keep track of the number of splits.


In other words: in part 1 we have one universe but the beam can be multiple places. In part 2, we have multiple universes, but the beam can only be in one place. This means we still only need a one-dimensional list to keep track of state!


Ok, that's encouraging, let's figure out the logic with our little example:


Wait a minute, that's the same as before...! In part 1 the key information was that we had one split, in part 2 the key information is that we now have 2 states (or 2 universes).


So maybe all we need to do is count the number of values in state...! (without the mvdedup, and we no longer split_count)


But that's only 18, not the 40 the example data should give.


Time to think a bit with paper and a pencil. If the source is at position 7, and we have splitters on:

  • 7

  • 6 and 8

  • 7

Something like this:

.......^.......
...............
......^.^......
...............
.......^.......

This is the minimum to get the interesting behaviour: there's only 4 splits, and in part 1 we would end up in 4 beams, but because there are 2 ways to reach the last splitter, there's actually 6 universes. How do we convey that? Let's keep playing:


So far, so good!


Now we see what the problem is: for the last splitter on 7, there were two 7s in the before state, and they should all be split. Currently our logic works as if there's only one. That's the bit we need to change. In other words we need to propagate the duplicates.


Oooh, nice. Let's standardise that:


Oh no, that broke somehow.


Again, mvappend is weird. This doesn't work:


But if we use intermediate fields, it works:


Why?! Anyway:

Amazing. Now we feel like we understand the problem and the solution better. Let's plug that in our solution:


Hurrah!


Now for the full data:


Unfortunately we run out of memory!!!


I guess it's not a good idea to have all these duplicates in state. Instead we could just have a bunch of counters. The index in state would represent the column (x) and the value at this index (i.e. "mvindex( state, x)") would be the number of instances/duplicates. This would prevent any memory usage explosion.


Back to the drawing board with this new representation:


Let's plug that back in our solution (note how we tweaked how the first state is populated):


Hmm, that's wrong.


The way I debugged this was by adding "| head 1" after the mvexpand command and slowly increasing 1 whilst comparing state to what I worked out on paper that it should be. That's how I noticed that it works perfectly well until the last iteration: The edges are the problem. Let's pad it out:


We've done it:


!!!! Yay !!!


Wow, nearly 41 trillion universes! Blimey, no wonder our previous approach was running out of memory...!

Comments


©2021 by Gabriel Vasseur. Proudly created with Wix.com

bottom of page