Today’s Advent of Code Puzzle had us “redistributing” data inside an array. We needed to perform a series of redistributions and stop when we detected that the array has returned to a state we’d seen before (indicating an infinite loop in the redistribution algorithm).

Part 1 – Using Sets

I needed a couple of helper methods to solve this. First, findMaxIndex to find the index of the maximum item in an array:

Next, the redistribute function which takes an array and returns a “redistributed” array. You’ll notice that this isn’t a pure function – I simply mutate the input array and return it (we’ll come back to that later).

And finally we need to keep calling redistribute until we get a duplicate. But how can we compare arrays? The ES6 Set class sadly doesn’t see two identical arrays as being the same object. We can test this with the handy node REPL and see we can add the “same” array of integers twice.

But there’s a quick and easy hack we can use to get round this. Just call toString on the array and put the string representation in the Set.

Here’s a quick and dirty implementation for counting redistributions using a Set of strings:

Part 2 – Upgrading to Maps

For part 2, we needed to remember not just previous states, but how many redistributions there were to get into that state. We can do this by upgrading from Set to Map keyed on the string representation of the array, and with the redistribution count as the value. The updated countRedistributions now returns not only the number of redistributions, but also the size of the “loop”:

Optimizations

There are (at least) two ways my solution could be improved. First, the redistribute method is inefficient if a large number needs to be redistributed. We should only need to modify each entry in the array once, rather than looping round incrementing by one.

Here’s a faster redistribute function that updates each slot a maximum of once:

There are of course other ways of doing this, but now we’re returning a new array and leaving the one we were passed in unchanged.

What’s the cost of doing this? Well, I added a timing function to my puzzle solver framework, using process.hrtime which is the recommended way to time functions in node. Here’s my timing helper that returns the function result and the duration in milliseconds:

This revealed that my first optimization didn’t make a huge difference on my input. And if we look at the numbers I had we can see why:

11 11 13 7 0 15 5 5 4 4 1 1 7 1 15 11

We rarely had to loop round all banks more than once as part of redistribution. Had these numbers been in the thousands, the optimization would have made a big difference.

And what about making redistribute pure by returning a new array? Well, that roughly doubled the time this function took to run (although the times were very small in the first place). Is that a price worth paying? Well for a simple puzzle like this it hardly matters either way. Immutability can bring a small performance penalty as in this example, but provides many benefits in terms of maintainability, testability and thread safety. And so even though I didn’t use it today or yesterday, I remain a big proponent of making your data structures immutable and your functions pure wherever possible.

About Mark Heath

I'm a Microsoft MVP and software developer based in Southampton, England, currently working as a Software Architect for NICE Systems. I create courses for Pluralsight and am the author of several open source libraries. I currently specialize in architecting Azure based systems and audio programming. You can find me on: