Introduction
The Day 22 problem is the next example of problem that is strictly divided in two parts, that seems to be identical but requires much different solutions. In the first part we can start with naive implementation which is good as the considered space is limited but in the second part we have to come up with some smarter approach. Let’s see the idea behind and the cool implementation in Kotlin.
Solution
For the first part we prepare straightforward solution which keeps all the cubes in space separately, as
their number is limited by task description (i.e. it can be at most $101^2$). So for every Step
we
take care only about the Cube
s from limited range and add them or remove from current collection.
However, in the second part this approach is too naive. That’s because the sizes of the added and removed
cubes are really huge, so adding individual Cube
s in space would take too much time and memory.
After some time of thinking about the solution, we can come up with the approach of inserting 3D ranges to reactor, so instead of keeping information about individual cubes, we keep the groups of them.
The hardest part of the solution is to implement the difference of Range3D
that we represented as
a triple of IntRange
. To do that, we provided a few helper infix function that makes checking relative
position of ranges easier. In my opinion, the hardest part was the proper implementation of
operator fun IntRange.minus(r: IntRange)
that is later used in operator fun Range3D.minus(r: Range3D)
.
The main idea behind this approach is to divide the considered Range3D
s into 8 (or less) smaller pieces
and check which of them are in the result Range3D
.
Day22.kt
|
|
Extra notes
The whole solution takes advantage of defining many infix and operator functions for ranges. Most of them are defined in order to get a simple way of calculating difference of many ranges.
When performing most of the operations on sets of ranges, we use the sequences to produce the values.
That’s because there are many transformations done on these iterables so approach with sequences is
preferred. Building the sequences is in Kotlin as easy as building collections with sequence { }
builder or sequenceOf()
function, so we definitely should consider using them in our code more
frequently.
We haven’t mentioned yet in our discussions the getters’ implementation in Kotlin. While usually we define the field values with immediate initialisation like
|
|
it might be not a good approach in multiple situations because the calculatedSomeFieldValue
function
is called just on object initialisation.
One of the approaches here is to provide the getter implementation of the field, so it’s values will be calculated every time when the property is accessed. We can with simple expression definition like
|
|
which can be also written as multiple statements, if some extra instructions are needed to calculate result like
|
|
In both of these cases, the function calculating the field value is called every time when the field is accessed. That’s may take a lot of resources so sometimes the lazy approach is definitely preferred. It can be used when the returned field value is known to be always the same, so it can be cached in delegated property. It’s enough to define such field as
|
|
Then, only at the first access of someField
the calculatedSomeFieldValue
is called. It’s pretty
and short approach to get a really cool effect, so we should remember about it when defining
the fields in our classes (especially when they depend on some objects’ state) 🤞.