Introduction
In Day 18 comes up with the problem the is described in a crazy way.
There is no tree structure mentioned in the description, however we can notice that this seems to be the best
structure to represent given data. Let’s see how to do it in Kotlin.
Solution
We represent the data specifically as binary tree, that can be TreeParent
node with two children or
TreeLeaf
that holds some number value. Because of the modifications that are going to happen on that
trees, we represent them as mutable data, not to copy too much of them if not needed.
Then we can see that concatenating trees is really simple - it requires only creating new node, making
it a parent for old parents and assigning all its children. Basically, it can be implemented as simply
as in operator fun plus
for TreeNode
. However, implementation of this function includes reduction of
the result, as described in problem description.
During the reduction we deal with two different types of events:
- For explodes we need to find the first node that is at depth 4 or more and has two number children.
This can be done with tree scanning with helper function
findToExpldde
. Its role is to go at least at
depth 4 in tree and then return the left most parent that has both number children. Then, to implement the
explosion functionality, we need to find the left and right siblings of these nodes in tree. We can find e.g.
the right sibling by going up as long as we go only from right child, then take the right child of the node to which
we came from left, and go left to find the right sibling of the starting node. We implemented this
functionality with some pretty functions, that allows to select the directions of traversal like1
2
3
4
| explode
.goUpFrom { left }
?.left
?.updateOnMost({ right }) { it + leftValue }
|
so don’t require any code repetitions for symmetrical cases.
The final exchange of node to zero is easy, as we have a reference to parent node, so we can change
the child to TreeLeaf
with value 0. - Also, the split functionality is pretty straightforward when we have a pointer to the parent node.
So our task here is to find the first node with big enough value and exchange it with a node holding two
values - e.i. the functionalities of
findToSplit
and split
functions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
| object Day18 : AdventDay() {
override fun solve() {
val data = reads<String>() ?: return
val snailFish = data.map { it.toSnailFish() }
snailFish.reduce { l, r -> l + r }.magnitude().printIt()
sequence {
for (l in snailFish) for (r in snailFish)
if (l != r) yield(l + r)
}.maxOf { it.magnitude() }.printIt()
}
}
private sealed class TreeNode(var parent: TreeParent?) {
abstract fun copy(with: TreeParent? = null): TreeNode
}
private class TreeLeaf(var value: Int, parent: TreeParent?) : TreeNode(parent) {
override fun copy(with: TreeParent?) = TreeLeaf(value, with)
}
private class TreeParent(parent: TreeParent? = null) : TreeNode(parent) {
lateinit var left: TreeNode
lateinit var right: TreeNode
override fun copy(with: TreeParent?) = TreeParent(with).also {
it.left = left.copy(it)
it.right = right.copy(it)
}
}
private fun String.toSnailFish(): TreeNode = asSequence().run {
fun Sequence<Char>.parse(parent: TreeParent? = null): Pair<TreeNode, Sequence<Char>> = when (first()) {
'[' -> {
val fish = TreeParent(parent)
val (left, fstRest) = drop("[".length).parse(fish)
val (right, sndRest) = fstRest.drop(",".length).parse(fish)
Pair(fish.also { it.left = left; it.right = right }, sndRest.drop("]".length))
}
else -> Pair(TreeLeaf(first().digitToInt(), parent), dropWhile { it.isDigit() })
}
parse().let { (snailFish, _) -> snailFish }
}
private fun TreeNode.magnitude(): Long = when (this) {
is TreeLeaf -> value.toLong()
is TreeParent -> 3 * left.magnitude() + 2 * right.magnitude()
}
private operator fun TreeNode.plus(other: TreeNode) = TreeParent().also { parent ->
parent.left = this.copy(parent)
parent.right = other.copy(parent)
}.apply { reduce() }
private tailrec fun TreeNode.updateOnMost(
select: TreeParent.() -> TreeNode,
update: (Int) -> Int
): Unit = when (this) {
is TreeLeaf -> value = update(value)
is TreeParent -> select().updateOnMost(select, update)
}
private tailrec fun TreeParent.goUpFrom(select: TreeParent.() -> TreeNode): TreeParent? {
val currParent = parent
return if (currParent == null) currParent
else if (currParent.select() == this) currParent.goUpFrom(select)
else currParent
}
private fun TreeNode.leftFinalParent(): TreeParent? = when {
this is TreeParent && left is TreeLeaf && right is TreeLeaf -> this
this is TreeParent -> left.leftFinalParent() ?: right.leftFinalParent()
else -> null
}
private fun TreeNode.changeTo(createNode: (TreeParent?) -> TreeNode) = when {
parent?.right == this -> parent?.right = createNode(parent)
parent?.left == this -> parent?.left = createNode(parent)
else -> Unit
}
private fun TreeLeaf.split() = changeTo { parent ->
TreeParent(parent).apply {
left = TreeLeaf(value / 2, this)
right = TreeLeaf(value / 2 + value % 2, this)
}
}
private val TreeParent.leftValue: Int get() = (left as? TreeLeaf)?.value ?: 0
private val TreeParent.rightValue: Int get() = (right as? TreeLeaf)?.value ?: 0
private fun TreeNode.reduce() {
fun TreeNode.findToExplode(level: Int): TreeParent? = when {
level == 0 -> leftFinalParent()
level > 0 && this is TreeParent ->
left.findToExplode(level - 1) ?: right.findToExplode(level - 1)
else -> null
}
fun TreeNode.findToSplit(): TreeLeaf? = when (this) {
is TreeLeaf -> if (value > 9) this else null
is TreeParent -> left.findToSplit() ?: right.findToSplit()
}
while (true) {
val explode = findToExplode(level = 4)
if (explode == null) findToSplit()?.split() ?: break
else explode.run {
goUpFrom { left }?.left?.updateOnMost({ right }) { it + leftValue }
goUpFrom { right }?.right?.updateOnMost({ left }) { it + rightValue }
changeTo { parent -> TreeLeaf(0, parent) }
}
}
}
|
Firstly, we need to notice that operator fun plus
for TreeNode
copies the added nodes. That’s because
during the addition process they are reduced so they content may change. However, we need them unchanged in
the second part of the task, so coping in this single place was needed.
It’s wort noticing how we implemented a few helper functions, that deal with tree structure. Let’s see that
a few of them are defined as tail-recursive functions, that will be optimized by compile to while
loops.
The last thing to notice is the definition of TreeParent
that uses the lateinit var
to store its children.
That’s because be need to create this node first, before creating its children, to give the parent node to
the children and then, after creating them, assign to parent node. This guarantees that the values will be
not null after that process, so we can use the as not nullable values after initialization.