diff --git a/src/year2021/day18/Day18.kt b/src/year2021/day18/Day18.kt new file mode 100644 index 0000000..5d852c5 --- /dev/null +++ b/src/year2021/day18/Day18.kt @@ -0,0 +1,212 @@ +package year2021.day18 + +import readInput + +enum class SnailfishNumberType { + Regular, + Pair +} + +abstract class SpecialNumber : Cloneable { + public abstract override fun clone(): SpecialNumber + abstract fun explode(path: MutableList): Pair + abstract fun split(path: MutableList): Pair + + abstract val type: SnailfishNumberType + abstract val magnitude: Long + + operator fun plus(other: SpecialNumber): SpecialNumber = reduce(PairNumber(this.clone(), other.clone())) +} + +data class RegularNumber(var number: Long) : SpecialNumber() { + override fun clone(): SpecialNumber = RegularNumber(number) + override fun explode(path: MutableList): Pair = false to this + override fun split(path: MutableList): Pair = + if (number < 10) + (false to this) + else + (true to PairNumber( + RegularNumber(number / 2), RegularNumber(number - number / 2) + )) + + override val type: SnailfishNumberType + get() = SnailfishNumberType.Regular + + override val magnitude: Long + get() = number + + override fun toString(): String = "$number" +} + +data class PairNumber(var left: SpecialNumber, var right: SpecialNumber) : SpecialNumber() { + override fun clone(): SpecialNumber = PairNumber(left.clone(), right.clone()) + + private fun tryReduce( + operation: (SpecialNumber, MutableList) -> Pair, + path: MutableList + ): Pair { + path.add(this) + + val (reducedLeft, left) = operation(left, path) + this.left = left + if (reducedLeft) { + path.removeLast() + return true to this + } + + val (reducedRight, right) = operation(right, path) + this.right = right + + path.removeLast() + return reducedRight to this + } + + override fun explode(path: MutableList): Pair { + path.add(this) + + if (path.size > 4 && left.type == SnailfishNumberType.Regular && right.type == SnailfishNumberType.Regular) { + val previous = predecessor(path) + if (previous != null) { + previous.number += left.magnitude + } + + val next = successor(path) + if (next != null) { + next.number += right.magnitude + } + + path.removeLast() + return true to RegularNumber(0) + } + + path.removeLast() + return tryReduce(SpecialNumber::explode, path) + } + + override fun split(path: MutableList): Pair = + tryReduce(SpecialNumber::split, path) + + override val type: SnailfishNumberType + get() = SnailfishNumberType.Pair + + override val magnitude: Long + get() = 3 * left.magnitude + 2 * right.magnitude + + override fun toString(): String = "[$left,$right]" +} + +fun String.toSnailfishNumber(): SpecialNumber { + fun parseNumber(fromIndex: Int): Pair { + if (this[fromIndex] != '[') { + val toIndex = + (fromIndex until this.length).firstOrNull { this[it] == ',' || this[it] == ']' } ?: this.length + + return toIndex to RegularNumber(this.slice(fromIndex until toIndex).toLong()) + } + + check(this[fromIndex] == '[') + + val (comma, left) = parseNumber(fromIndex + 1) + check(this[comma] == ',') + + val (toIndex, right) = parseNumber(comma + 1) + check(this[toIndex] == ']') + + return toIndex + 1 to PairNumber(left, right) + } + val (index, number) = parseNumber(0) + check(index == this.length) + return number +} + +fun predecessor(path: List): RegularNumber? { + var i = path.size - 1 + + while (i > 0) { + if (path[i] === (path[i - 1] as PairNumber).right) { + var node = (path[i - 1] as PairNumber).left + + while (node !is RegularNumber) { + node = (node as PairNumber).right + } + return node + } + i-- + } + + return null +} + +fun successor(path: List): RegularNumber? { + var i = path.size - 1 + + while (i > 0) { + if (path[i] === (path[i - 1] as PairNumber).left) { + var node = (path[i - 1] as PairNumber).right + + while (node !is RegularNumber) { + node = (node as PairNumber).left + } + return node + } + i-- + } + + return null +} + +fun reduce(number: SpecialNumber): SpecialNumber { + var newNumber = number + val stack = mutableListOf() + + while (true) { + val (hasExploded, afterExplode) = newNumber.explode(stack) + check(stack.isEmpty()) + if (hasExploded) { + newNumber = afterExplode + continue + } + + val (hasSplit, afterSplit) = newNumber.split(stack) + check(stack.isEmpty()) + if (hasSplit) { + newNumber = afterSplit + continue + } + + break + } + + return newNumber +} + +fun asSFNumber(number: Long): SpecialNumber = RegularNumber(number) +fun asSFNumber(left: Long, right: Long): SpecialNumber = PairNumber(RegularNumber(left), RegularNumber(right)) +fun asSFNumber(left: SpecialNumber, right: Long): SpecialNumber = PairNumber(left, RegularNumber(right)) +fun asSFNumber(left: Long, right: SpecialNumber): SpecialNumber = PairNumber(RegularNumber(left), right) +fun asSFNumber(left: SpecialNumber, right: SpecialNumber): SpecialNumber = PairNumber(left, right) + +fun part1(input: List): Long = + input.map { it.toSnailfishNumber() } + .reduce(SpecialNumber::plus) + .magnitude + +fun part2(input: List): Long = + input.map(String::toSnailfishNumber).let { numbers -> + numbers.indices + .flatMap { i -> numbers.indices.map { j -> i to j } } + .filter { (i, j) -> i != j }.maxOf { (i, j) -> + (numbers[i] + numbers[j]).magnitude + } + } + +fun main() { + val sample = readInput(18, "sample") + val input = readInput(18, "input") + + check(part1(sample) == 4140L) + println(part1(input)) + + check(part2(sample) == 3993L) + println(part2(input)) +} diff --git a/src/year2021/day18/SnailfishNumberTest.kt b/src/year2021/day18/SnailfishNumberTest.kt new file mode 100644 index 0000000..f9e1f93 --- /dev/null +++ b/src/year2021/day18/SnailfishNumberTest.kt @@ -0,0 +1,192 @@ +package year2021.day18 + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class SnailfishNumberTest { + @Test + fun toSnailfishNumber() { + assertEquals(asSFNumber(1, 2), "[1,2]".toSnailfishNumber()) + assertEquals( + asSFNumber(asSFNumber(1, 2), 3), "[[1,2],3]".toSnailfishNumber() + ) + assertEquals(asSFNumber(9, asSFNumber(8, 7)), "[9,[8,7]]".toSnailfishNumber()) + assertEquals(asSFNumber(asSFNumber(1, 9), asSFNumber(8, 5)), "[[1,9],[8,5]]".toSnailfishNumber()) + assertEquals( + asSFNumber( + asSFNumber( + asSFNumber(asSFNumber(1, 2), asSFNumber(3, 4)), asSFNumber(asSFNumber(5, 6), asSFNumber(7, 8)) + ), 9 + ), "[[[[1,2],[3,4]],[[5,6],[7,8]]],9]".toSnailfishNumber() + ) + assertEquals( + asSFNumber( + asSFNumber( + asSFNumber(9, asSFNumber(3, 8)), asSFNumber(asSFNumber(0, 9), 6) + ), asSFNumber( + asSFNumber(asSFNumber(3, 7), asSFNumber(4, 9)), 3 + ) + ), "[[[9,[3,8]],[[0,9],6]],[[[3,7],[4,9]],3]]".toSnailfishNumber() + ) + assertEquals( + asSFNumber( + asSFNumber( + asSFNumber(asSFNumber(1, 3), asSFNumber(5, 3)), asSFNumber(asSFNumber(1, 3), asSFNumber(8, 7)) + ), asSFNumber( + asSFNumber(asSFNumber(4, 9), asSFNumber(6, 9)), asSFNumber(asSFNumber(8, 2), asSFNumber(7, 3)) + ) + ), "[[[[1,3],[5,3]],[[1,3],[8,7]]],[[[4,9],[6,9]],[[8,2],[7,3]]]]".toSnailfishNumber() + ) + } + + @Test + fun reduce() { + assertEquals( + "[[[[0,7],4],[[7,8],[6,0]]],[8,1]]".toSnailfishNumber(), + reduce("[[[[[4,3],4],4],[7,[[8,4],9]]],[1,1]]".toSnailfishNumber()) + ) + + assertEquals( + "[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]".toSnailfishNumber(), + reduce("[[[[[7,7],[7,7]],[[8,7],[8,7]]],[[[7,0],[7,7]],9]],[[[[4,2],2],6],[8,7]]]".toSnailfishNumber()) + ) + } + + @Test + fun explode() { + assertEquals( + "[[[[0,9],2],3],4]".toSnailfishNumber(), + "[[[[[9,8],1],2],3],4]".toSnailfishNumber().explode(mutableListOf()).second + ) + assertEquals( + "[7,[6,[5,[7,0]]]]".toSnailfishNumber(), + "[7,[6,[5,[4,[3,2]]]]]".toSnailfishNumber().explode(mutableListOf()).second + ) + assertEquals( + "[[6,[5,[7,0]]],3]".toSnailfishNumber(), + "[[6,[5,[4,[3,2]]]],1]".toSnailfishNumber().explode(mutableListOf()).second + ) + assertEquals( + "[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]".toSnailfishNumber(), + "[[3,[2,[1,[7,3]]]],[6,[5,[4,[3,2]]]]]".toSnailfishNumber().explode(mutableListOf()).second + ) + assertEquals( + "[[3,[2,[8,0]]],[9,[5,[7,0]]]]".toSnailfishNumber(), + "[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]".toSnailfishNumber().explode(mutableListOf()).second + ) + } + + @Test + fun split() { + assertEquals(asSFNumber(5, 5), asSFNumber(10).split(mutableListOf()).second) + assertEquals(asSFNumber(5, 6), asSFNumber(11).split(mutableListOf()).second) + assertEquals(asSFNumber(6, 6), asSFNumber(12).split(mutableListOf()).second) + } + + @Test + fun getType() { + assertEquals(SnailfishNumberType.Regular, "13".toSnailfishNumber().type) + assertEquals(SnailfishNumberType.Pair, "[1,2]".toSnailfishNumber().type) + } + + @Test + fun getMagnitude() { + assertEquals(143, "[[1,2],[[3,4],5]]".toSnailfishNumber().magnitude) + assertEquals(1384, "[[[[0,7],4],[[7,8],[6,0]]],[8,1]]".toSnailfishNumber().magnitude) + assertEquals(445, "[[[[1,1],[2,2]],[3,3]],[4,4]]".toSnailfishNumber().magnitude) + assertEquals(791, "[[[[3,0],[5,3]],[4,4]],[5,5]]".toSnailfishNumber().magnitude) + assertEquals(1137, "[[[[5,0],[7,4]],[5,5]],[6,6]]".toSnailfishNumber().magnitude) + assertEquals(3488, "[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]".toSnailfishNumber().magnitude) + } + + @Test + fun plus() { + assertEquals( + "[[[[0,7],4],[[7,8],[6,0]]],[8,1]]".toSnailfishNumber(), + "[[[[4,3],4],4],[7,[[8,4],9]]]".toSnailfishNumber() + "[1,1]".toSnailfishNumber() + ) + assertEquals( + "[[[[4,0],[5,4]],[[7,7],[6,0]]],[[8,[7,7]],[[7,9],[5,0]]]]".toSnailfishNumber(), + "[[[0,[4,5]],[0,0]],[[[4,5],[2,6]],[9,5]]]".toSnailfishNumber() + "[7,[[[3,7],[4,3]],[[6,3],[8,8]]]]".toSnailfishNumber() + ) + } + + @Test + fun plusFold() { + assertEquals( + "[[[[1,1],[2,2]],[3,3]],[4,4]]".toSnailfishNumber(), listOf( + "[1,1]", "[2,2]", "[3,3]", "[4,4]" + ).map(String::toSnailfishNumber).reduce(SpecialNumber::plus) + ) + + assertEquals( + "[[[[3,0],[5,3]],[4,4]],[5,5]]".toSnailfishNumber(), listOf( + "[1,1]", "[2,2]", "[3,3]", "[4,4]", "[5,5]", + ).map(String::toSnailfishNumber).reduce(SpecialNumber::plus) + ) + + assertEquals( + "[[[[5,0],[7,4]],[5,5]],[6,6]]".toSnailfishNumber(), listOf( + "[1,1]", "[2,2]", "[3,3]", "[4,4]", "[5,5]", "[6,6]", + ).map(String::toSnailfishNumber).reduce(SpecialNumber::plus) + ) + } + + @Test + fun plusFoldLarger() { + val numbers = listOf( + "[[[0,[4,5]],[0,0]],[[[4,5],[2,6]],[9,5]]]", + "[7,[[[3,7],[4,3]],[[6,3],[8,8]]]]", + "[[2,[[0,8],[3,4]]],[[[6,7],1],[7,[1,6]]]]", + "[[[[2,4],7],[6,[0,5]]],[[[6,8],[2,8]],[[2,1],[4,5]]]]", + "[7,[5,[[3,8],[1,4]]]]", + "[[2,[2,2]],[8,[8,1]]]", + "[2,9]", + "[1,[[[9,3],9],[[9,0],[0,7]]]]", + "[[[5,[7,4]],7],1]", + "[[[[4,2],2],6],[8,7]]", + ).map(String::toSnailfishNumber) + + val expected = listOf( + "[[[[4,0],[5,4]],[[7,7],[6,0]]],[[8,[7,7]],[[7,9],[5,0]]]]", + "[[[[6,7],[6,7]],[[7,7],[0,7]]],[[[8,7],[7,7]],[[8,8],[8,0]]]]", + "[[[[7,0],[7,7]],[[7,7],[7,8]]],[[[7,7],[8,8]],[[7,7],[8,7]]]]", + "[[[[7,7],[7,8]],[[9,5],[8,7]]],[[[6,8],[0,8]],[[9,9],[9,0]]]]", + "[[[[6,6],[6,6]],[[6,0],[6,7]]],[[[7,7],[8,9]],[8,[8,1]]]]", + "[[[[6,6],[7,7]],[[0,7],[7,7]]],[[[5,5],[5,6]],9]]", + "[[[[7,8],[6,7]],[[6,8],[0,8]]],[[[7,7],[5,0]],[[5,5],[5,6]]]]", + "[[[[7,7],[7,7]],[[8,7],[8,7]]],[[[7,0],[7,7]],9]]", + "[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]" + ).map(String::toSnailfishNumber) + + assertEquals( + expected.last(), numbers.reduce(SpecialNumber::plus) + ) + } + + @Test + fun plusFoldSample() { + val numbers = listOf( + "[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]]", + "[[[5,[2,8]],4],[5,[[9,9],0]]]", + "[6,[[[6,2],[5,6]],[[7,6],[4,7]]]]", + "[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]]", + "[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]]", + "[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]]", + "[[[[5,4],[7,7]],8],[[8,3],8]]", + "[[9,3],[[9,9],[6,[4,9]]]]", + "[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]]", + "[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]]" + ).map(String::toSnailfishNumber) + + assertEquals( + "[[[[6,6],[7,6]],[[7,7],[7,0]]],[[[7,7],[7,7]],[[7,8],[9,9]]]]".toSnailfishNumber(), + numbers.reduce(SpecialNumber::plus) + ) + } + + @Test + fun deepExplode() { + println(reduce("[[[[9,[3,[2,[3,0]]]],[[7,9],5]],8],9]".toSnailfishNumber())) + } +} \ No newline at end of file diff --git a/src/year2021/day18/sample.txt b/src/year2021/day18/sample.txt new file mode 100644 index 0000000..2efedbf --- /dev/null +++ b/src/year2021/day18/sample.txt @@ -0,0 +1,10 @@ +[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]] +[[[5,[2,8]],4],[5,[[9,9],0]]] +[6,[[[6,2],[5,6]],[[7,6],[4,7]]]] +[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]] +[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]] +[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]] +[[[[5,4],[7,7]],8],[[8,3],8]] +[[9,3],[[9,9],[6,[4,9]]]] +[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]] +[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]] \ No newline at end of file