corneil
Corneil du Plessis
Github: corneil
Twitter: @corneil
Mastodon: @corneil@hachyderm.io
corneil |
I am a Staff Software Engineer at VMware working on the Spring Cloud Data Flow team. I enjoyed AoC 2019 and decided to give it another go this year.
I love coding in Kotlin and I try write the code solving the puzzles in a way that someone else can understand the solution.
This is a simple Hello World program in Kotlin, if you want to experiment with Kotlin use the Kotlin Playground
My aim with Advent of Code is to write code solving the puzzles in a way that is understandable to a wide audience. Feel free to reach out with comments at the links above.
In order to follow these solutions you will need to login to Advent of Code and complete each day.
The day was lines representing the calories of food items carried by elves and separated by blank lines. Kotlin File has a readLines function that makes it easy to parse the input, calculate the calories for each elf and add to a list.
fun findCalories(input: List<String>): List<Int> {
val elves = mutableListOf<Int>()
var calories = 0
for (line in input) {
if (line.isBlank()) {
elves.add(calories)
calories = 0
} else {
calories += line.toInt()
}
}
elves.add(calories)
return elves
}
For part one the max value it the answer.
fun findMaxCalories(input: List<String>): Int {
val calories = findCalories(input)
return calories.max()
}
For part two the result is the sum of the first three when sorted in descending order. Kotlin has the take
method on collections which is very useful in this case.
fun topThree(input: List<String>): Int {
val calories = findCalories(input)
return calories.sortedDescending().take(3).sum()
}
Part 1 meant we needed to parse input representing an opponent hand when playing RPS, and we had to assume the 2nd input was an answer we needed to provide while "cheating".
Part 2 came with a curveball in that the 2nd input should be treated as an outcome. This means we needed to find the correct hand for the outcome. The score calculation was a function of the outcome and the hand we selected.
Below is the final code and there are loaders for creating the rounds for part 1 and part 2.
enum class Rps(val identifiers: String, val shapeScore: Int) {
ROCK("AX", 1),
PAPER("BY", 2),
SCISSORS("CZ", 3)
}
enum class Outcome(val identifier: String, val score: Int) {
LOSE("X", 0),
DRAW("Y", 3),
WIN("Z", 6)
}
data class Hand(val them: Rps, val me: Rps)
object Rules {
data class Rule(val loser: Rps, val winner: Rps)
private val beats = mapOf(
Rps.ROCK to Rule(Rps.SCISSORS, Rps.PAPER),
Rps.PAPER to Rule(Rps.ROCK, Rps.SCISSORS),
Rps.SCISSORS to Rule(Rps.PAPER, Rps.ROCK)
)
fun winner(shape: Rps): Rps {
val winner = beats[shape] ?: error("Cannot find $shape")
return winner.winner
}
fun loser(shape: Rps): Rps {
val loser = beats[shape] ?: error("Cannot find $shape")
return loser.loser
}
fun didIWin(round: Hand): Boolean {
val shape = loser(round.me)
return shape == round.them
}
}
fun main() {
fun toRps(hint: String): Rps {
return Rps.values().find { it.identifiers.contains(hint) }
?: error("Invalid RPS $hint")
}
fun toOutcome(outcome: String): Outcome {
return Outcome.values().find { it.identifier == outcome }
?: error("Invalid Outcome:$outcome")
}
fun readRounds(input: List<String>): List<Hand> {
return input.map {
val hints = it.split(" ")
Hand(toRps(hints[0]), toRps(hints[1]))
}
}
// choose a shape to match the outcome
fun chooseShape(shape: Rps, outcome: Outcome): Rps {
return when (outcome) {
Outcome.DRAW -> shape
Outcome.WIN -> Rules.winner(shape)
Outcome.LOSE -> Rules.loser(shape)
}
}
// convert 2nd to outcome and then find a shape to match outcome given 1st
fun readRounds2(input: List<String>): List<Hand> {
return input.map {
val hints = it.split(" ")
Pair(toRps(hints[0]), toOutcome(hints[1]))
}.map {
Hand(it.first, chooseShape(it.first, it.second))
}
}
fun calcRound(round: Hand): Int {
return when {
round.them == round.me -> Outcome.DRAW.score
Rules.didIWin(round) -> Outcome.WIN.score
else -> Outcome.LOSE.score
}
}
fun calcScore(round: Hand): Int {
return round.me.shapeScore + calcRound(round)
}
fun calcTotal(rounds: List<Hand>): Int {
return rounds.sumOf { calcScore(it) }
}
fun part1() {
val testScore = calcTotal(readRounds(readInput("day02_test")))
println("Test Total = $testScore")
check(testScore == 15)
val total = calcTotal(readRounds(readInput("day02")))
println("Total = $total")
}
fun part2() {
val testScore2 = calcTotal(readRounds2(readInput("day02_test")))
println("Test Total 2 = $testScore2")
check(testScore2 == 12)
val total2 = calcTotal(readRounds2(readInput("day02")))
println("Total 2 = $total2")
}
part1()
part2()
}
Today puzzle was about manipulating strings and calculating the values of characters according rules.
fun calcPriority(value: Char): Int = when (value) {
in 'a'..'z' -> value - 'a' + 1
in 'A'..'Z' -> value - 'A' + 27
else -> error("Invalid input $value")
}
fun calcRucksacks(input: List<String>): Int =
input.map {
Pair(it.substring(0 until it.length / 2), it.substring(it.length / 2)) (1)
}
.map {
it.first.toSet().intersect(it.second.toSet()).first() (2)
}
.sumOf { calcPriority(it) } (3)
1 | Split string into equal parts to represent compartments |
2 | Determine common character by converting to set of characters and finding union |
3 | Calculate the sum of the priorities |
fun calcBadges(input: List<String>): Int =
input.mapIndexed { index, s -> index / 3 to s }
.groupBy({ it.first }, { it.second }) (1)
.map { e ->
e.value
.map { it.toSet() }
.reduce { a, b -> a.intersect(b) }
.first() (2)
}
.sumOf { calcPriority(it) } (3)
1 | Group strings by 3. |
2 | Determine common character from all 3 strings. |
3 | Calculate the sum of the priorities |
String.toSet
gives to a Set
of Char
reduce
is a great way to determine the intersection on multiple sets.
groupBy
can take two parameters and with the second parameter you can determine the type of the value.
fun main() {
fun calcPriority(value: Char): Int = when (value) {
in 'a'..'z' -> value - 'a' + 1
in 'A'..'Z' -> value - 'A' + 27
else -> error("Invalid input $value")
}
fun calcRucksacks(input: List<String>): Int =
input.map {
Pair(it.substring(0 until it.length / 2), it.substring(it.length / 2))
}
.map { it.first.toSet().intersect(it.second.toSet()).first() }
.sumOf { calcPriority(it) }
fun calcBadges(input: List<String>): Int =
input.mapIndexed { index, s -> index / 3 to s }
.groupBy({ it.first }, { it.second })
.map { e -> e.value.map { it.toSet() }
.reduce { a, b -> a.intersect(b) }
.first()
}
.sumOf { calcPriority(it) }
fun part1() {
val testPriorities = calcRucksacks(readInput("day03_test"))
println("Test Priorities = $testPriorities")
check(testPriorities == 157)
val priorities = calcRucksacks(readInput("day03"))
println("Priorities = $priorities")
check(priorities == 8123)
}
fun part2() {
val testPriorities = calcBadges(readInput("day03_test"))
println("Test Priorities = $testPriorities")
check(testPriorities == 70)
val priorities = calcBadges(readInput("day03"))
println("Priorities = $priorities")
check(priorities == 2620)
}
part1()
part2()
}
I wasted some time today because I misunderstood the fully contains applying to a pair of assignments. I was trying to check across assignments. Re-reading when I got the wrong answer meant some facepalms.
We needed to convert the input depicting 2 ranges into sets of sections representing by numbers. So that meant converting 2-4,6-8
to a Pair
of IntRange
and then to a Pair
of Set<Int>
which it as simple as:
fun convertRange(input: String): IntRange {
val values = input.split("-")
return values[0].toInt()..values[1].toInt()
}
fun convertRanges(input: List<String>): List<Pair<Set<Int>, Set<Int>>> =
input.map { it.split(",") }
.map { Pair(convertRange(it[0]).toSet(), convertRange(it[1]).toSet()) }
Determining fully contains for part 1 was as simple as:
fun calcContains(ranges: List<Pair<Set<Int>, Set<Int>>>): Int {
return ranges.count {
it.first.containsAll(it.second) || it.second.containsAll(it.first)
}
}
And determining an overlap for part1 was as simple as:
fun calcOverlap(ranges: List<Pair<Set<Int>, Set<Int>>>): Int {
return ranges.count { it.first.intersect(it.second).isNotEmpty() }
}
Re-read the problem because a lot of effort was made to explain with a complete example.
If you first answer is incorrect you may have misunderstood something.
fun main() {
fun convertRange(input: String): IntRange {
val values = input.split("-")
return values[0].toInt()..values[1].toInt()
}
fun convertRanges(input: List<String>): List<Pair<Set<Int>, Set<Int>>> =
input.map { it.split(",") }
.map { Pair(convertRange(it[0]).toSet(), convertRange(it[1]).toSet()) }
fun calcContains(ranges: List<Pair<Set<Int>, Set<Int>>>): Int {
return ranges.count {
it.first.containsAll(it.second) || it.second.containsAll(it.first)
}
}
fun calcOverlap(ranges: List<Pair<Set<Int>, Set<Int>>>): Int {
return ranges.count { it.first.intersect(it.second).isNotEmpty() }
}
fun part1() {
val testInput = readInput("day04_test")
val testCount = calcContains(convertRanges(testInput))
println("Part 1 Test Count = $testCount")
check(testCount == 2)
val input = readInput("day04")
val count = calcContains(convertRanges(input))
println("Part 1 Count = $count")
check(count == 524)
}
fun part2() {
val testInput = readInput("day04_test")
val testCount = calcOverlap(convertRanges(testInput))
println("Part 2 Test Count = $testCount")
check(testCount == 4)
val input = readInput("day04")
val count = calcOverlap(convertRanges(input))
println("Part 2 Count = $count")
check(count == 798)
}
part1()
part2()
}
Today we had to implement a parser to read the state of stacks of containers and instructions for moving the containers. The result was a list of the top container labels. I decided to create a data structure for the instructions and a simple Stack.
data class Instruction(val count: Int, val from: Int, val to: Int)
class Stack<T>(private val elements: MutableList<T> = mutableListOf<T>()) {
fun empty(): Boolean = elements.isEmpty()
fun peek(): T = elements[elements.lastIndex]
fun pop(): T = elements.removeAt(elements.lastIndex)
fun push(value: T) {
elements.add(value)
}
fun items() = elements.toList()
fun size() = elements.size
}
fun readStack(lines: List<String>, stack: Int): Stack<Char> {
val result = mutableListOf<Char>()
val offset = stack * 4 + 1
for (line in lines) {
if (line.length > offset) {
val crate = line[offset]
if (crate in 'A'..'Z') {
result.add(crate)
}
}
}
result.reverse()
println("Stack[${stack + 1}]=${result.joinToString("")}")
return Stack(result)
}
fun loadStacks(input: List<String>): List<Stack<Char>> {
val lines = input.toMutableList()
val numbers = lines.last()
.split(" ")
.filter { it.isNotEmpty() }
.map { it.toInt() }
lines.removeAt(lines.lastIndex)
val stacks = mutableListOf<Stack<Char>>()
for (stack in numbers.indices) {
stacks.add(readStack(lines, stack))
}
return stacks.toList()
}
fun loadInstructions(input: List<String>): List<Instruction> {
val regex = """move (\d+) from (\d+) to (\d+)""".toRegex()
return input.mapNotNull { line ->
regex.find(line)?.let {
val (count, from, to) = it.destructured
Instruction(count.toInt(), from.toInt(), to.toInt())
}
}
}
Perform all the instructions and provide the list of top level crates:
fun perform(stacks: List<Stack<Char>>, instruction: Instruction) {
for (i in 1..instruction.count) {
val crate = stacks[instruction.from - 1].pop()
stacks[instruction.to - 1].push(crate)
}
}
fun performAndFindTops(
stacks: List<Stack<Char>>,
instructions: List<Instruction>
): String {
instructions.forEach {
perform(stacks, it)
}
return stacks.joinToString("") { it.peek().toString() }
}
The crane can now move multiple containers at the same time retaining their order. I decided to use a local stack to pop all the container and then push them again which will retain the order. The alternative would have been to adjust the Stack to remove multiple items at once.
fun perform9001(stacks: List<Stack<Char>>, instruction: Instruction) {
println(instruction)
val tmp = Stack<Char>()
for (i in 1..instruction.count) {
val crate = stacks[instruction.from - 1].pop()
tmp.push(crate)
}
while (!tmp.empty()) {
stacks[instruction.to - 1].push(tmp.pop())
}
}
fun performAndFindTops9001(
stacks: List<Stack<Char>>,
instructions: List<Instruction>
): String {
instructions.forEach {
perform9001(stacks, it)
}
return stacks.joinToString("") { it.peek().toString() }
}
Sometimes a requirement change can impact your data structures in huge ways. Using a stack to represent the piles of crates was fine until we needed to move multiple crates at a time. In a real system the performance differences of moving multiple crates at the same time will be huge if it is a core part of the system.
data class Instruction(val count: Int, val from: Int, val to: Int)
class Stack<T>(private val elements: MutableList<T> = mutableListOf<T>()) {
fun empty(): Boolean = elements.isEmpty()
fun peek(): T = elements[elements.lastIndex]
fun pop(): T = elements.removeAt(elements.lastIndex)
fun push(value: T) {
elements.add(value)
}
fun items() = elements.toList()
fun size() = elements.size
}
fun main() {
fun readStack(lines: List<String>, stack: Int): Stack<Char> {
val result = mutableListOf<Char>()
val offset = stack * 4 + 1
for (line in lines) {
if (line.length > offset) {
val crate = line[offset]
if (crate in 'A'..'Z') {
result.add(crate)
}
}
}
result.reverse()
println("Stack[${stack + 1}]=${result.joinToString("")}")
return Stack(result)
}
fun loadStacks(input: List<String>): List<Stack<Char>> {
val lines = input.toMutableList()
val numbers = lines.last().split(" ").mapNotNull { if(it.isNotBlank()) it.toInt() else null }
lines.removeAt(lines.lastIndex)
val stacks = mutableListOf<Stack<Char>>()
for (stack in numbers.indices) {
stacks.add(readStack(lines, stack))
}
return stacks.toList()
}
fun loadInstructions(input: List<String>): List<Instruction> {
val regex = """move (\d+) from (\d+) to (\d+)""".toRegex()
return input.mapNotNull { line ->
regex.find(line)?.let {
val (count, from, to) = it.destructured
Instruction(count.toInt(), from.toInt(), to.toInt())
}
}
}
fun perform(stacks: List<Stack<Char>>, instruction: Instruction) {
for (i in 1..instruction.count) {
val crate = stacks[instruction.from - 1].pop()
stacks[instruction.to - 1].push(crate)
}
}
fun perform9001(stacks: List<Stack<Char>>, instruction: Instruction) {
val tmp = Stack<Char>()
for (i in 1..instruction.count) {
val crate = stacks[instruction.from - 1].pop()
tmp.push(crate)
}
while (!tmp.empty()) {
stacks[instruction.to - 1].push(tmp.pop())
}
}
fun performAndFindTops(stacks: List<Stack<Char>>, instructions: List<Instruction>): String {
instructions.forEach {
perform(stacks, it)
}
stacks.forEach { stack -> println("[${stack.items().joinToString("")}]") }
return stacks.joinToString("") { it.peek().toString() }
}
fun performAndFindTops9001(stacks: List<Stack<Char>>, instructions: List<Instruction>): String {
instructions.forEach {
perform9001(stacks, it)
}
stacks.forEach { stack -> println("[${stack.items().joinToString("")}]") }
return stacks.joinToString("") { it.peek().toString() }
}
val test = """ [D]
[N] [C]
[Z] [M] [P]
1 2 3
move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2
"""
val testInputs = test.split("\n\n").map { readText(it) }
val input = readFileGroup("day05")
fun part1() {
val testTops = performAndFindTops(loadStacks(testInputs[0]), loadInstructions(testInputs[1]))
println("Part 1 Test Tops = $testTops")
check(testTops == "CMZ")
val tops = performAndFindTops(loadStacks(input[0]), loadInstructions(input[1]))
println("Part 1 Tops = $tops")
check(tops == "PTWLTDSJV")
}
fun part2() {
val testTops = performAndFindTops9001(loadStacks(testInputs[0]), loadInstructions(testInputs[1]))
println("Part 2 Test Tops = $testTops")
check(testTops == "MCD")
val tops = performAndFindTops9001(loadStacks(input[0]), loadInstructions(input[1]))
println("Part 2 Tops = $tops")
check(tops == "WZMFVGGZP")
}
part1()
part2()
}
Today was probably the easiest one so far from my perspective. We had to write a simple parser to find the first unique sequence of characters with a given length and report the number of characters read.
The Kotlin collections method windowed
provides a way to create a window over an Iterable
and provides a simple way to solve this. Checking that the content of the window as a unique set is the same size as the window.
fun findUniquePacketEnd(input: String, packetSize: Int): Int {
return input.toList()
.windowed(packetSize)
.indexOfFirst { it.toSet().size == packetSize } + packetSize
}
fun main() {
fun findUniquePacketEnd(input: String, packetSize: Int): Int {
return input.toList()
.windowed(packetSize)
.indexOfFirst { it.toSet().size == packetSize } + packetSize
}
val input = readFileToString("day06")
fun part1() {
val testStart = findUniquePacketEnd("nznrnfrfntjfmvfwmzdfjlvtqnbhcprsg", 4)
println("Part1 Test Start = $testStart")
check(testStart == 10)
val start = findUniquePacketEnd(input, 4)
println("Part1 Start = $start")
check(start == 1965)
}
fun part2() {
val testStart = findUniquePacketEnd("nznrnfrfntjfmvfwmzdfjlvtqnbhcprsg", 14)
println("Part 2 Test Message = $testStart")
check(testStart == 29)
val start = findUniquePacketEnd(input, 14)
println("Part 2 Start = $start")
check(start == 2773)
}
part1()
part2()
}
We had to parse a series of commands and output representing changing to and listing directories as in the example below.
$ cd /
$ ls
dir a
14848514 b.txt
8504156 c.dat
dir d
$ cd a
$ ls
dir e
29116 f
2557 g
62596 h.lst
$ cd e
$ ls
584 i
$ cd ..
$ cd ..
$ cd d
$ ls
4060174 j
8033020 d.log
5626152 d.ext
7214296 k
The aim was to determine directory sizes. In the first case the challenge was to find directories with a total size including children of below a threshold and the second problem was to find the smallest directory above a certain size.
data class FileInfo(val name: String, val size: Int)
class Directory(val name: String) {
private val files = mutableMapOf<String, FileInfo>()
private val directories = mutableMapOf<String, Directory>()
fun listDirectories() = directories.values.toList()
fun hasDir(dirname: String): Boolean = directories.containsKey(dirname)
fun getDir(dirname: String) = directories[dirname]
fun addDir(dirname: String): Directory = directories.getOrPut(dirname) { Directory(dirname) }
fun addFile(name: String, size: Int): FileInfo {
val file = FileInfo(name, size)
files[name] = file
return file
}
fun listFiles(): List<FileInfo> = files.values.toList()
fun fileSizes(): Int = files.values.sumOf { it.size }
fun totalSize(): Int = fileSizes() + directories.values.sumOf { it.totalSize() }
override fun toString(): String {
return "Directory(name='$name'," +
"directories=${directories.values.map { it.name }.toList()}," +
"files=${files.values.map { "${it.name}:${it.size}" }}" +
")"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Directory
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
}
The parsing meant keeping track of the directory tree and the current directory.
I used the Stack
from Day 05 to maintain the parent directories.
fun parseCommandsAndOutput(lines: List<String>): Directory {
val root = Directory("/")
val dirs = Stack<Directory>()
dirs.push(root)
var current = root
for(line in lines) {
val data = line.split(" ")
when {
data[0] == "$" && data[1] == "cd" && data[2] == "/" -> { dirs.clear(); dirs.push(root) }
data[0] == "$" && data[1] == "cd" && data[2] == ".." -> { dirs.pop(); current = dirs.peek()}
data[0] == "$" && data[1] == "cd" -> { dirs.push(current.addDir(data[2])); current = dirs.peek() }
data[0] == "$" && data[1] == "ls" -> println("listing ${dirs.items().joinToString("/") { it.name }}")
data[0] == "dir" -> current.addDir(data[1])
else -> {
val file = current.addFile(data[1], data[0].toInt())
println("file=dir:${current.name} + ${file.name}:${file.size}")
}
}
}
return root
}
Both solutions need to obtain a list of all directories.
fun listAllDirectories(directories: Directory): List<Directory> {
val result = mutableListOf<Directory>()
directories.listDirectories().forEach {
result.add(it)
result.addAll(listAllDirectories(it))
}
return result
}
Find the directory sizes at most 100000 and calculate the total.
fun calcDirectoriesBelow(input: List<String>, maxValue: Int): Int {
val root = parseCommandsAndOutput(input)
val directories = listAllDirectories(root)
return directories.map { it.totalSize() }.filter { it <= maxValue }.sum()
}
Find the small of the directories large enough to delete in order to free up enough space so that at least 30000000 of 70000000 is unused.
fun calcDirectoriesToFree(input: List<String>, diskSize: Int, freeSpace: Int): Int {
val root = parseCommandsAndOutput(input)
val directories = listAllDirectories(root)
val unused = diskSize - root.totalSize()
val requiredDelete = freeSpace - unused
return directories.map { it.totalSize() }.filter { it > requiredDelete }.min()
}
fun main() {
val test = readFile("day07_test")
val input = readFile("day07")
data class FileInfo(val name: String, val size: Int)
class Directory(val name: String) {
private val files = mutableMapOf<String, FileInfo>()
private val directories = mutableMapOf<String, Directory>()
fun listDirectories() = directories.values.toList()
fun hasDir(dirname: String): Boolean =
directories.containsKey(dirname)
fun getDir(dirname: String) = directories[dirname]
fun addDir(dirname: String): Directory =
directories.getOrPut(dirname) { Directory(dirname) }
fun addFile(name: String, size: Int): FileInfo {
val file = FileInfo(name, size)
files[name] = file
return file
}
fun listFiles(): List<FileInfo> = files.values.toList()
fun fileSizes(): Int = files.values.sumOf { it.size }
fun totalSize(): Int =
fileSizes() + directories.values.sumOf { it.totalSize() }
override fun toString(): String {
return "Directory(name='$name',directories=${
directories.values.map { it.name }.toList()
},files=${files.values.map { "${it.name}:${it.size}" }})"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Directory
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
private fun printTree(dir: Directory, depth: Int) {
println("- ${dir.name} (dir)")
val prefix = 0 until depth
dir.listDirectories().forEach {
prefix.forEach { _ -> print(' ') }
printTree(it, depth + 2)
}
dir.listFiles().forEach {
prefix.forEach { _ -> print(' ') }
println("- ${it.name} (file, size = ${it.size})")
}
}
fun printTree() = printTree(this, 0)
}
fun listAllDirectories(directories: Directory): List<Directory> {
val result = mutableListOf<Directory>()
directories.listDirectories().forEach {
result.add(it)
result.addAll(listAllDirectories(it))
}
return result
}
fun parseCommandsAndOutput(input: List<String>): Directory {
val root = Directory("/")
val dirs = Stack<Directory>()
dirs.push(root)
var current = root
input.forEach { line ->
val data = line.split(" ")
when {
data[0] == "$" && data[1] == "cd" && data[2] == "/" -> { dirs.clear(); dirs.push(root) }
data[0] == "$" && data[1] == "cd" && data[2] == ".." -> { dirs.pop(); current = dirs.peek()}
data[0] == "$" && data[1] == "cd" -> { dirs.push(current.addDir(data[2])); current = dirs.peek() }
data[0] == "$" && data[1] == "ls" -> println("listing ${dirs.items().joinToString("/") { it.name }}")
data[0] == "dir" -> current.addDir(data[1])
else -> {
val file = current.addFile(data[1], data[0].toInt())
println("file=dir:${current.name} + ${file.name}:${file.size}")
}
}
}
return root
}
fun calcDirectoriesBelow(input: List<String>, maxValue: Int): Int {
val root = parseCommandsAndOutput(input)
root.printTree()
val directories = listAllDirectories(root)
return directories.map { it.totalSize() }.filter { it <= maxValue }.sum()
}
fun calcDirectoriesToFree(input: List<String>, diskSize: Int, freeSpace: Int): Int {
val root = parseCommandsAndOutput(input)
val directories = listAllDirectories(root)
val unused = diskSize - root.totalSize()
val requiredDelete = freeSpace - unused
return directories.map { it.totalSize() }.filter { it > requiredDelete }.min()
}
fun part1() {
val testResult = calcDirectoriesBelow(test, 100000)
println("Part 1 Answer = $testResult")
check(testResult == 95437)
separator()
val result = calcDirectoriesBelow(input, 100000)
check(result == 1792222)
println("Part 1 Answer = $result")
separator()
}
fun part2() {
val testResult = calcDirectoriesToFree(test, 70000000, 30000000)
println("Part 2 Answer = $testResult")
check(testResult == 24933642)
separator()
val result = calcDirectoriesToFree(input, 70000000, 30000000)
println("Part 2 Answer = $result")
check(result == 1112963)
separator()
}
println("Day - 07")
part1()
part2()
}
Today’s puzzle was all about creating a grid and determining the state of coordinates given certain conditions.
I decided to create a class to represent the grid as an array of int array.
class Grid(val lines: Array<IntArray>) {
fun width() = lines[0].size
fun height() = lines.size
fun get(x: Int, y: Int) = lines[x][y]
}
The data was presents as lines of digits, each digits represented a tree and the value was the height of the tree.
30373
25512
65332
33549
35390
Parsing meant converting each line to an array of int by converting each character to it’s integer representation.
fun parseGrid(input: List<String>): Grid {
return Grid(input.map { line ->
line.toList()
.map { it - '0' }
.toIntArray()
}.toTypedArray())
}
We had to determine how many trees are visible considering it is invisible it there is a tree of similar or greater height above or below, to the left or right.
I added a visible method to Grid
fun visible(x: Int, y: Int): Boolean {
val treeHeight = get(x, y)
var bottom = true
var top = true
var left = true
var right = true
for (i in 0 until x) {
val tree = get(i, y)
if (tree >= treeHeight) {
top = false
break
}
}
for (i in x + 1 until height()) {
if (get(i, y) >= treeHeight) {
bottom = false
break
}
}
for (i in y + 1 until width()) {
if (get(x, i) >= treeHeight) {
right = false
break
}
}
for (i in 0 until y) {
if (get(x, i) >= treeHeight) {
left = false
break
}
}
return top || bottom || left || right
}
fun calcVisible(grid: Grid): Int {
val height = grid.height()
val width = grid.width()
var result = (height * 2) + (width * 2) - 4
for (x in 1 until height - 1) {
for (y in 1 until width - 1) {
if (grid.visible(x, y)) {
result += 1
}
}
}
return result
}
We had to calculate a scenic value for each tree and determine the answer was maximum scenic. The scenic value was determined by how many trees were visible from a given tree.
I added a scenic method to Grid.
fun scenic(x: Int, y: Int): Int {
val treeHeight = get(x, y)
var bottom = 0
var top = 0
var left = 0
var right = 0
for (i in x + 1 until height()) {
bottom += 1
if (get(i, y) >= treeHeight) {
break
}
}
for (i in x - 1 downTo 0 ) {
top += 1
if (get(i, y) >= treeHeight) {
break
}
}
for (i in y + 1 until width()) {
right += 1
if (get(x, i) >= treeHeight) {
break
}
}
for (i in y - 1 downTo 0 ) {
left += 1
if (get(x, i) >= treeHeight) {
break
}
}
return bottom * left * right * top
}
fun calcMaxScenic(grid: Grid): Int {
val height = grid.height()
val width = grid.width()
var result = 0
for (x in 1 until height - 1) {
for (y in 1 until width - 1) {
result = max(result, grid.scenic(x, y))
}
}
return result
}
import kotlin.math.max
fun main() {
val test = readLines(
"""30373
25512
65332
33549
35390"""
)
val input = readFile("day08")
class Grid(val lines: Array<IntArray>) {
fun width() = lines[0].size
fun height() = lines.size
fun get(x: Int, y: Int) = lines[x][y]
fun visible(x: Int, y: Int): Boolean {
val treeHeight = get(x, y)
var bottom = true
var top = true
var left = true
var right = true
for (i in 0 until x) {
val tree = get(i, y)
if (tree >= treeHeight) {
top = false
break
}
}
for (i in x + 1 until height()) {
if (get(i, y) >= treeHeight) {
bottom = false
break
}
}
for (i in y + 1 until width()) {
if (get(x, i) >= treeHeight) {
right = false
break
}
}
for (i in 0 until y) {
if (get(x, i) >= treeHeight) {
left = false
break
}
}
return top || bottom || left || right
}
fun scenic(x: Int, y: Int): Int {
val treeHeight = get(x, y)
var bottom = 0
var top = 0
var left = 0
var right = 0
for (i in x + 1 until height()) {
bottom += 1
if (get(i, y) >= treeHeight) {
break
}
}
for (i in x - 1 downTo 0 ) {
top += 1
if (get(i, y) >= treeHeight) {
break
}
}
for (i in y + 1 until width()) {
right += 1
if (get(x, i) >= treeHeight) {
break
}
}
for (i in y - 1 downTo 0 ) {
left += 1
if (get(x, i) >= treeHeight) {
break
}
}
return bottom * left * right * top
}
}
fun parseGrid(input: List<String>): Grid {
return Grid(input.map { line ->
line.toList()
.map { it - '0' }
.toIntArray()
}.toTypedArray()
)
}
fun calcVisible(grid: Grid): Int {
val height = grid.height()
val width = grid.width()
var result = (height * 2) + (width * 2) - 4
for (x in 1 until height - 1) {
for (y in 1 until width - 1) {
if (grid.visible(x, y)) {
result += 1
}
}
}
return result
}
fun calcMaxScenic(grid: Grid): Int {
val height = grid.height()
val width = grid.width()
var result = 0
for (x in 1 until height - 1) {
for (y in 1 until width - 1) {
result = max(result, grid.scenic(x, y))
}
}
return result
}
fun part1() {
val testResult = calcVisible(parseGrid(test))
println("Part 1 Answer = $testResult")
check(testResult == 21)
val result = calcVisible(parseGrid(input))
println("Part 1 Answer = $result")
check(result == 1851)
}
fun part2() {
val testResult = calcMaxScenic(parseGrid(test))
println("Part 2 Answer = $testResult")
check(testResult == 8)
val result = calcMaxScenic(parseGrid(input))
println("Part 1 Answer = $result")
check(result == 574080)
}
println("Day - 08")
separator()
part1()
separator()
part2()
}
Today’s puzzle was about simulating the movement of a rope or chain. The knots / links follow each other when the head is pulled in a direction. The simulation had to implement rules for the movement and record the movement of the tail and reporting the number of unique positions visited by the tail.
In part 1 the rope has 2 knots and in part 2 the rope had 10 knots.
I decided to model the Rope
as an array of Position
data class Position(val row: Int, val col: Int) {
fun left() = Position(row, col - 1)
fun right() = Position(row, col + 1)
fun above() = Position(row - 1, col)
fun below() = Position(row + 1, col)
fun distance(pos: Position): Int = max(abs(pos.row - row), abs(pos.col - col))
operator fun minus(pos: Position) = Position(row - pos.row, col - pos.col)
operator fun plus(pos: Position) = Position(row + pos.row, col + pos.col)
fun sign(): Position = Position(row.sign,col.sign)
fun move(direction: Char): Position {
return when (direction) {
'U' -> above()
'D' -> below()
'L' -> left()
'R' -> right()
else -> error("Expected one of U,D,L,R not ${direction}")
}
}
override fun toString(): String {
return "(col=$col, row=$row)"
}
}
class Rope(val knots: Array<Position>) {
val head: Position
get() = knots[0]
val tail: Position
get() = knots[knots.lastIndex]
operator fun set(index: Int, pos: Position) { knots[index] = pos }
operator fun get(index: Int) = knots[index]
fun indexOf(pos: Position) = knots.indexOf(pos)
override fun toString() = knots.contentToString()
}
The input was a list of steps with the direction as one of U
,D
,L
,R
and a number for the distance moved.
data class Step(val direction: Char, val distance: Int)
fun parseSteps(input: List<String>): List<Step> {
return input.map {
val words = it.split(" ")
Step(words[0][0], words[1].toInt())
}
}
After when I got to part 2 I had a situation where my answer to the provided data was correct but AOC report my answer with the input as low. I tried to find a case where I wasn’t handling the tail correctly. All this back and forth lead me to simplifying the calculation to the point where I calculated the difference between the knots and the movement was then a step in the direction of the first knot and it could be diagonally if the difference had values in both axis.
This worked and was a lot simpler. When I looked at some other submissions in the Kotlin community I discovered Int.sign()
which returns 0
,1
or -1
depending in the value. I added operators to Position
for -
and +
and a sign
method. This reduced the final expression to a very simple one.
for (index in 1 until knots) {
val prev = rope[index - 1] (1)
val next = rope[index] (2)
if (prev != next && prev.distance(next) > 1) { (3)
val diff = prev - next (4)
rope[index] = next + diff.sign() (5)
}
}
1 | The previous knot |
2 | The knot we want to move |
3 | If they aren’t in same location and the Chebyshev distance is more than 1. |
4 | Determine the difference between the positions |
5 | Add the sign of the difference to the knot we want to move |
fun calcVisits(steps: List<Step>, knots: Int, printStep: Boolean): Int {
val rope = Rope(Array(knots) { Position(0, 0) })
val start = rope.head
val visited = mutableSetOf<Position>()
visited.add(rope.tail)
steps.forEach { step ->
for (i in 0 until step.distance) {
rope[0] = rope.head.move(step.direction)
for (index in 1 until knots) {
val prev = rope[index - 1]
val next = rope[index]
if (prev != next && prev.distance(next) > 1) {
val diff = prev - next
rope[index] = next + diff.sign()
}
}
visited.add(rope.tail)
if (printStep) {
println()
print(steps, visited, start, rope, knots, false)
}
}
}
println()
print(steps, visited, start, rope, knots, true)
return visited.size
}
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sign
fun main() {
val test = """R 4
U 4
L 3
D 1
R 4
D 1
L 5
R 2"""
val test2 = """R 5
U 8
L 8
D 3
R 17
D 10
L 25
U 20"""
val input = readFile("day09")
data class Step(val direction: Char, val distance: Int)
data class Position(val row: Int, val col: Int) {
fun left() = Position(row, col - 1)
fun right() = Position(row, col + 1)
fun above() = Position(row - 1, col)
fun below() = Position(row + 1, col)
fun distance(pos: Position): Int = max(abs(pos.row - row), abs(pos.col - col))
operator fun minus(pos: Position) = Position(row - pos.row, col - pos.col)
operator fun plus(pos: Position) = Position(row + pos.row, col + pos.col)
fun sign(): Position = Position(row.sign,col.sign)
fun move(direction: Char): Position {
return when (direction) {
'U' -> above()
'D' -> below()
'L' -> left()
'R' -> right()
else -> error("Expected one of U,D,L,R not ${direction}")
}
}
override fun toString(): String {
return "(col=$col, row=$row)"
}
}
fun parseSteps(input: List<String>): List<Step> {
return input.map {
val words = it.split(" ")
Step(words[0][0], words[1].toInt())
}
}
class Rope(val knots: Array<Position>) {
val head: Position
get() = knots[0]
val tail: Position
get() = knots[knots.lastIndex]
operator fun set(index: Int, pos: Position) { knots[index] = pos }
operator fun get(index: Int) = knots[index]
fun indexOf(pos: Position) = knots.indexOf(pos)
override fun toString() = knots.contentToString()
}
fun print(
steps: List<Step>,
visited: Set<Position>,
start: Position,
rope: Rope,
knots: Int,
visitsOnly: Boolean = false
) {
var maxRow = 0
var minRow = 0
var maxCol = 0
var minCol = 0
var dummy = Position(0, 0)
steps.forEach { step ->
repeat(step.distance) {
dummy = dummy.move(step.direction)
maxRow = max(maxRow, dummy.row)
maxCol = max(maxCol, dummy.col)
minRow = min(minRow, dummy.row)
minCol = min(minCol, dummy.col)
}
}
for (row in minRow..maxRow) {
val covered = mutableSetOf<Int>()
for (col in minCol..maxCol) {
val pos = Position(row, col)
if (visitsOnly) {
if (pos == start) {
print('s')
} else {
print(if (visited.contains(pos)) '#' else '.')
}
} else {
val index = rope.indexOf(pos)
when {
index == 0 -> print('H')
index > 0 -> print(if (knots == 2) 'T' else index.toString())
pos == start -> print('s')
else -> print(if (visited.contains(pos)) '#' else '.')
}
if(index >= 0 && covered.isEmpty()) {
if(pos == start) {
covered.add(rope.knots.size) // indicate 's'
}
covered.add(index)
for (i in index..rope.knots.lastIndex) {
if (pos == rope[i]) {
covered.add(i)
}
}
if(covered.size == 1) {
covered.clear()
}
}
}
}
if(covered.size > 1) {
print(" (")
val coveredKnots = covered.sorted()
coveredKnots.forEachIndexed { index, knot ->
if(index == 1) {
print(" covers ")
} else if(index > 1) {
print(" ,")
}
when(knot) {
0 -> print('H')
rope.knots.lastIndex -> print('T')
rope.knots.size -> print('s')
else -> print("$knot")
}
}
println(')')
}
println()
}
}
fun calcVisits(steps: List<Step>, knots: Int, printStep: Boolean): Int {
val rope = Rope(Array(knots) { Position(0, 0) })
val start = rope.head
val visited = mutableSetOf<Position>()
visited.add(rope.tail)
steps.forEach { step ->
for (i in 0 until step.distance) {
rope[0] = rope.head.move(step.direction)
for (index in 1 until knots) {
val prev = rope[index - 1]
val next = rope[index]
if (prev != next && prev.distance(next) > 1) {
val diff = prev - next
rope[index] = next + diff.sign()
}
}
visited.add(rope.tail)
if (printStep) {
println()
print(steps, visited, start, rope, knots, false)
}
}
}
println()
print(steps, visited, start, rope, knots, true)
return visited.size
}
fun part1() {
val testResult = calcVisits(parseSteps(readLines(test)), 2, true)
println("Part 1 Answer = $testResult")
check(testResult == 13)
val result = calcVisits(parseSteps(input), 2, false)
println("Part 1 Answer = $result")
check(result == 6503)
}
fun part2() {
check(calcVisits(parseSteps(readLines(test)), 10, true) == 1)
val testResult = calcVisits(parseSteps(readLines(test2)), 10, true)
println("Part 2 Answer = $testResult")
check(testResult == 36)
val result = calcVisits(parseSteps(input), 10, false)
println("Part 2 Answer = $result")
check(result == 2724)
}
println("Day - 09")
separator()
part1()
separator()
part2()
}
Today’s puzzle was about simulating a computer and the cathode ray tube.
So far we have only 2 instructions to simulate: noop
and addx
. It very important to carefully read the instructions describing the clock cycles. In this case noop
has one cycle and addx
has two cycle.
I decided to model a instructions as a sealed class and the processor as a class to maintain the clock and register value with a lambda to be called on each tick.
sealed class OpCode(val prefix: String) {
object NOOP: OpCode("noop")
data class ADDX(val value: Int): OpCode("addx")
}
Parsing will create the instances of the various inner classes of the sealed class.
fun parseInstructions(lines: List<String>): List<OpCode> {
return lines.map { ins ->
val words = ins.split(" ")
when (words[0]) {
"noop" -> OpCode.NOOP
"addx" -> OpCode.ADDX(words[1].toInt())
else -> error("Unknown instruction: $ins")
}
}
}
class Processor(val processing: Processor.() -> Unit) {
var clock = 0
private set
var regX: Int = 1
private set
fun tick() {
clock += 1
processing(this) (1)
}
fun execute(instructions: List<OpCode>) {
instructions.forEach { ins ->
when (ins) {
is OpCode.NOOP -> tick()
is OpCode.ADDX -> addX(ins)
}
}
}
private fun addX(ins: OpCode.ADDX) {
tick()
val value = ins.value
tick()
regX += value
}
}
1 | processing lambda is called after each clock tick. |
The first part had to calculate signal string based on the value of regX
and on the 20th tick and each 40th tick thereafter.
fun calcSignalStrength(instructions: List<OpCode>): Int {
var signalValue = 0
val cpu = Processor { (1)
if (clock % 40 == 20) {
signalValue += regX * clock (2)
}
}
cpu.execute(instructions)
return signalValue
}
1 | The lambda declaration after the Processor will be invoked on each tick |
2 | The lambda cannot modify clock or regX |
The second part had to render the CRT of 40 characters wide. The CRT prints from left to right on each clock tick and will print a #
if the 3 character sprite overlaps with the cursor
position, otherwise it prints .
.
I had to print spaces instead of dots before I could read the character spelled out by part 2.
fun renderCrt(instructions: List<OpCode>): List<String> {
val pixels = mutableListOf<Char>()
val crt = mutableListOf<String>()
val cpu = Processor {
val sprite = regX - 1
val cursor = (clock - 1) % 40
if (cursor >= sprite && cursor <= sprite + 2) {
pixels.add('#')
} else {
pixels.add('.')
}
if (cursor == 39) {
crt.add(pixels.joinToString(""))
pixels.clear()
}
}
cpu.execute(instructions)
return crt.toList()
}
import utils.*
sealed class OpCode(val prefix: String) {
object NOOP: OpCode("noop")
data class ADDX(val value: Int): OpCode("addx")
}
fun main() {
val test = readFile("day10_test")
val input = readFile("day10")
fun parseInstructions(lines: List<String>): List<OpCode> {
return lines.map { ins ->
val words = ins.split(" ")
when (words[0]) {
"noop" -> OpCode.NOOP
"addx" -> OpCode.ADDX(words[1].toInt())
else -> error("Unknown instruction: $ins")
}
}
}
class Processor(val processing: Processor.() -> Unit) {
var clock = 0
private set
fun tick() {
clock += 1
processing(this)
}
var regX: Int = 1
private set
fun execute(instructions: List<OpCode>) {
instructions.forEach { ins ->
when (ins) {
is OpCode.NOOP -> tick()
is OpCode.ADDX -> addX(ins)
}
}
}
private fun addX(ins: OpCode.ADDX) {
tick()
val value = ins.value
tick()
regX += value
}
}
fun calcSignalStrength(instructions: List<OpCode>): Int {
var signalValue = 0
val cpu = Processor {
if (clock % 40 == 20) {
signalValue += regX * clock
}
}
cpu.execute(instructions)
return signalValue
}
fun renderCrt(instructions: List<OpCode>): List<String> {
val pixels = mutableListOf<Char>()
val crt = mutableListOf<String>()
val cpu = Processor {
val sprite = regX - 1
val cursor = (clock - 1) % 40
if (cursor >= sprite && cursor <= sprite + 2) {
pixels.add('#')
} else {
pixels.add('.')
}
if (cursor == 39) {
crt.add(pixels.joinToString(""))
pixels.clear()
}
}
cpu.execute(instructions)
return crt.toList()
}
fun part1() {
val testResult = calcSignalStrength(parseInstructions(test))
println("Part 1 Answer = $testResult")
check(testResult == 13140)
val result = calcSignalStrength(parseInstructions(input))
println("Part 1 Answer = $result")
}
val expectedTestCrt = readLines(
"""##..##..##..##..##..##..##..##..##..##..
###...###...###...###...###...###...###.
####....####....####....####....####....
#####.....#####.....#####.....#####.....
######......######......######......####
#######.......#######.......#######....."""
)
val expectedCrt = readLines(
"""####..##....##..##..###....##.###..####.
#....#..#....#.#..#.#..#....#.#..#.#....
###..#.......#.#..#.#..#....#.#..#.###..
#....#.......#.####.###.....#.###..#....
#....#..#.#..#.#..#.#....#..#.#.#..#....
#.....##...##..#..#.#.....##..#..#.####."""
)
fun part2() {
val testCrt = renderCrt(parseInstructions(test))
println("Part 2 Test")
testCrt.forEach { println(it) }
check(testCrt == expectedTestCrt)
val crt = renderCrt(parseInstructions(input))
println("Part 2")
crt.forEach { println(it.replace('.',' ')) }
check(crt == expectedCrt)
}
println("Day - 10")
separator()
part1()
separator()
part2()
}
Today’s puzzle require the parsing of rules describing monkeys that have some of your items. Each monkey will toss the item to another monkey based on the worried factor perceived by the monkey.
The model represents all the rules as well as tracking the number of inspections performed by the monkey.
data class Monkey(
val number: Int,
val worriedLevel: Long,
val items: MutableList<Long>,
val boredTarget: Int,
val worriedTarget: Int,
val expression: (Long) -> Long,
) {
var inspected: Int = 0
private set
fun inspect(value: Int) {
inspected += value
}
fun findTarget(boredLevel: Long) =
if (boredLevel % worriedLevel != 0L) worriedTarget else boredTarget
override fun toString(): String {
return "Monkey(number=$number, " +
"inspected=$inspected, " +
"worriedLevel=$worriedLevel, " +
"boredTarget=$boredTarget, " +
"worriedTarget=$worriedTarget, "+
"items=$items)"
}
}
I decided to parse each monkey rules using a regular expression for each line.
val regexMonkey = listOf(
"Monkey (\\d+):$",
" Starting items:\\s*((\\S+(,\\s)*)*)\$",
" Operation: new =\\s(\\S+)\\s(\\+|\\*)\\s(\\S+)\$",
" Test: divisible by (\\d+)$",
" If true: throw to monkey (\\d+)$",
" If false: throw to monkey (\\d+)$"
).map { it.toRegex() }
fun parseMonkey(lines: List<String>): Monkey {
val result = regexMonkey.mapIndexed { index, regex ->
regex.find(lines[index]) ?: error("Regex error for ${lines[index]}")
}.toTypedArray()
val items = result[1].groupValues[1].split(",")
.map { it.trim().toLong() }
.toMutableList()
val words = result[2].groupValues.drop(1)
check(words[0] == "old")
val isAdd = words[1] == "+"
val constant = words[2].toLongOrNull()
val lambda: (Long) -> Long =
if (isAdd)
{ old -> Math.addExact(old, constant ?: old) }
else
{ old -> Math.multiplyExact(old, constant ?: old) } (1)
return Monkey(
result[0].groupValues[1].toInt(),
result[3].groupValues[1].toLong(),
items,
result[4].groupValues[1].toInt(),
result[5].groupValues[1].toInt(),
lambda
)
}
1 | Math.addExact and Math.multiplyExact will result in an exception if there is an overflow |
Each monkey must process their items in the sequence of the assigned number.
fun processItems(
monkeys: Map<Int, Monkey>,
rounds: Int,
divisor: Long = 3L
): Map<Int, Monkey> {
val divisors = monkeys.values
.map { monkey -> monkey.worriedLevel }
.reduce { acc, l -> acc * l * divisor } (1)
val sorted = monkeys.values.sortedBy { it.number }
repeat(rounds) {
sorted.forEach { monkey ->
monkey.items.forEach { item ->
val level = monkey.expression(item)
val bored = level / divisor
val targetNumber = monkey.findTarget(bored)
val targetMonkey = monkeys[targetNumber]
?: error("Cannot find target Monkey:$targetNumber")
targetMonkey.items.add(bored % divisors) (2)
}
monkey.inspect(monkey.items.size)
monkey.items.clear()
}
}
return monkeys
}
1 | multiplying all worriedLevel with each other and the required divisor provides a least common divisor. |
2 | mod with least common divisor to ensure the smallest valid value. |
Initially using Int
values caused an overflow and I changed the worriedLevel and items to Long.
fun calcShenanigans1(input: List<String>): Int {
val monkeys = input.chunked(7)
.map { parseMonkey(it) }
.associateBy { it.number }
val result = processItems(monkeys, 20)
return result.values
.map { it.inspected }
.sortedDescending()
.take(2)
.reduce { acc, i -> acc * i }
}
When we had to increase the number of rounds to 10000 the worriedLevel overflowed. Using BigInteger doesn’t solve the problem because the calculations for BigInteger take too long as the multiplication ofr BigInteger takes exponentially longer as the numbers increase.
Using a smallest common divisor all the modulus and / operations will ensure that the overflow is prevented until the number of Monkeys become very large.
|
fun calcShenanigans2(input: List<String>): Long {
val monkeys = input.chunked(7)
.map { parseMonkey(it) }
.associateBy { it.number }
val result = processItems(monkeys, 10000, 1)
return result.values
.map { it.inspected.toLong() }
.sortedDescending()
.take(2)
.reduce { acc, i -> acc * i }
}
import utils.readFile
import utils.separator
fun main() {
val test = readFile("day11_test")
val input = readFile("day11")
data class Monkey(
val number: Int,
val worriedLevel: Long,
val items: MutableList<Long>,
val boredTarget: Int,
val worriedTarget: Int,
val expression: (Long) -> Long,
) {
var inspected: Int = 0
private set
fun inspect(value: Int) {
inspected += value
}
fun findTarget(boredLevel: Long) = if (boredLevel % worriedLevel != 0L) worriedTarget else boredTarget
override fun toString(): String {
return "Monkey(number=$number, inspected=$inspected, worriedLevel=$worriedLevel, boredTarget=$boredTarget, worriedTarget=$worriedTarget, items=$items)"
}
}
val regexMonkey = listOf(
"Monkey (\\d+):$",
" Starting items:\\s*((\\S+(,\\s)*)*)\$",
" Operation: new =\\s(\\S+)\\s(\\+|\\*)\\s(\\S+)\$",
" Test: divisible by (\\d+)$",
" If true: throw to monkey (\\d+)$",
" If false: throw to monkey (\\d+)$"
).map { it.toRegex() }
fun parseMonkey(lines: List<String>): Monkey {
val result = regexMonkey.mapIndexed { index, regex ->
regex.find(lines[index]) ?: error("Regex error for ${lines[index]}")
}.toTypedArray()
val items = result[1].groupValues[1].split(",")
.map { it.trim().toLong() }
.toMutableList()
val words = result[2].groupValues.drop(1)
check(words[0] == "old")
val isAdd = words[1] == "+"
val constant = words[2].toLongOrNull()
val lambda: (Long) -> Long =
if (isAdd)
{ old -> old + (constant ?: old) }
else
{ old -> old * (constant ?: old) }
return Monkey(
result[0].groupValues[1].toInt(),
result[3].groupValues[1].toLong(),
items,
result[4].groupValues[1].toInt(),
result[5].groupValues[1].toInt(),
lambda
)
}
fun processItems(
monkeys: Map<Int, Monkey>,
rounds: Int,
divisor: Long = 3L
): Map<Int, Monkey> {
// The mod of the total of worriedLevels overcomes the Long overflow
// using all divisors ensure that it is the smallest value that will
// still satisfy all the requirements when using a large number of rounds
val divisors = monkeys.values
.map { monkey -> monkey.worriedLevel }
.reduce { acc, l -> acc * l * divisor }
val sorted = monkeys.values.sortedBy { it.number }
repeat(rounds) {
sorted.forEach { monkey ->
monkey.items.forEach { item ->
val level = monkey.expression(item)
val bored = level / divisor
val targetNumber = monkey.findTarget(bored)
val targetMonkey = monkeys[targetNumber]
?: error("Cannot find target Monkey:$targetNumber")
targetMonkey.items.add(bored % divisors) // mod to ensure smallest valid value
}
monkey.inspect(monkey.items.size)
monkey.items.clear()
}
}
return monkeys
}
fun calcShenanigans1(input: List<String>): Int {
val monkeys = input.chunked(7)
.map { parseMonkey(it) }
.associateBy { it.number }
println("Before: ====")
monkeys.values.forEach { println(it.toString()) }
val result = processItems(monkeys, 20)
println("After: ====")
result.values.forEach { println(it.toString()) }
return result.values
.map { it.inspected }
.sortedDescending()
.take(2)
.reduce { acc, i -> acc * i }
}
fun calcShenanigans2(input: List<String>): Long {
val monkeys = input.chunked(7)
.map { parseMonkey(it) }
.associateBy { it.number }
println("Before: ====")
monkeys.values.forEach { println(it.toString()) }
val result = processItems(monkeys, 10000, 1)
println("After: ====")
result.values.forEach { println(it.toString()) }
return result.values
.map { it.inspected.toLong() }
.sortedDescending()
.take(2)
.reduce { acc, i -> acc * i }
}
fun part1() {
val testResult = calcShenanigans1(test)
println("Part 1 Answer = $testResult")
check(testResult == 10605)
val result = calcShenanigans1(input)
println("Part 1 Answer = $result")
check(result == 151312)
}
fun part2() {
val testResult = calcShenanigans2(test)
println("Part 2 Answer = $testResult")
check(testResult == 2713310158L)
val result = calcShenanigans2(input)
println("Part 2 Answer = $result")
check(result == 51382025916L)
}
println("Day - 11")
separator()
part1()
separator()
part2()
}
Today’s puzzle required graph traversal. I decided to use utilities I created during the 2019 edition of Aoc.
The problem used a grid of letter to describe a map. Each letter describes an elevation starting at a
as the lowest and z
the highest.
The user may only navigate horizontally or vertically and only to one step higher, the same level, or lower by any amount. (You may jump off a cliff!)
Sabqponm
abcryxxl
accszExk
acctuvwj
abdefghi
The Graph requires a list of Edges. Each of the edges must contain two Comparable items that are linked with a distance. In our case the distance is always one and since you can go from low to only one step higher but jump off a cliff the graph will be directed and the distance between cells will always be 1.
The Graph uses the Dijkstra algorithm to find the shortest path between nodes.
data class Cell(
val c: Char,
val pos: Coord
) : Comparable<Cell> {
override fun compareTo(other: Cell): Int {
var result = c.compareTo(other.c)
if (result == 0) {
result = pos.compareTo(other.pos)
}
return result
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
other as Cell
if (c != other.c) return false
if (pos != other.pos) return false
return true
}
override fun hashCode(): Int {
return pos.hashCode() * c.hashCode()
}
fun actual(): Char = when (c) {
'S' -> 'a'
'E' -> 'z'
else -> c
}
override fun toString(): String {
return "Cell($c, $pos)"
}
}
The parsing of the grid and creating of Cells and Edges will be in one function.
fun createGrid(input: List<String>): List<Edge<Cell>> {
val edges = mutableListOf<Edge<Cell>>()
val cells = input.mapIndexed { y, line ->
line.mapIndexed { x, c ->
Cell(c, Coord(x, input.lastIndex - y))
}
}.flatMap { row -> row.map { it } }.associateBy { it.pos }
cells.values.forEach { cell ->
cell.pos.surrounds().forEach { coord ->
val neighbour = cells[coord]
if (neighbour != null) { (1)
val height = neighbour.actual() - cell.actual()
if (height <= 1) { (2)
edges.add(Edge(cell, neighbour))
}
}
}
}
return edges
}
1 | If there is no neighbour at the surrounding coordinates it will be skipped. This will happen for the cells on the edges of the grid. |
2 | We are interested in cells that are 1 step higher or lower. |
fun calculateSteps(
edges: List<Edge<Cell>>,
start: Cell,
end: Cell
): Int? {
val graph = Graph(edges, true)
val path = graph.findPath(start, end)
return if (path.isEmpty()) null else path.size - 1 (1)
}
1 | The path includes the start and end node and we will subtract 1 from the path length to obtain the number of steps. Null is returned for an empty path. |
In part 1 we had to find the shortest path between S
(which is the same as a
) and E
(which is the same elevation as z
).
fun calcSolution1(input: List<String>): Int {
val edges = createGrid(input)
val end = edges.map { edge -> edge.c2 }
.find { it.c == 'E' } ?: error("Cannot find E")
val start = edges.map { edge -> edge.c1 }
.find { it.c == 'S' } ?: error("Cannot find S")
return calculateSteps(edges, start, end)
?: error("Cannot find solution from $start to $end")
}
In part 2 we had to find the shortest paths for all cells labelled a
and report the shortest of those.
fun calcSolution2(input: List<String>): Int {
val edges = createGrid(input)
val end = edges.map { edge -> edge.c2 }
.find { it.c == 'E' } ?: error("Cannot find E")
return edges.map { edge -> edge.c1 }
.filter { it.actual() == 'a' }
.mapNotNull { start -> calculateSteps(edges, start, end) }
.min()
}
package day12
import main.utils.Edge
import main.utils.Graph
import main.utils.measureAndPrint
import utils.Coord
import utils.readFile
import utils.readLines
fun main() {
val test = readLines(
"""Sabqponm
abcryxxl
accszExk
acctuvwj
abdefghi"""
)
val input = readFile("day12")
data class Cell(
val c: Char,
val pos: Coord
) : Comparable<Cell> {
override fun compareTo(other: Cell): Int {
var result = c.compareTo(other.c)
if (result == 0) {
result = pos.compareTo(other.pos)
}
return result
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
other as Cell
if (c != other.c) return false
if (pos != other.pos) return false
return true
}
override fun hashCode(): Int {
return pos.hashCode() * c.hashCode()
}
fun actual(): Char = when (c) {
'S' -> 'a'
'E' -> 'z'
else -> c
}
override fun toString(): String {
return "Cell($c, $pos)"
}
}
fun createGrid(input: List<String>): List<Edge<Cell>> {
val edges = mutableListOf<Edge<Cell>>()
val cells = input.mapIndexed { y, line ->
line.mapIndexed { x, c ->
Cell(c, Coord(x, input.lastIndex - y))
}
}.flatMap { row -> row.map { it } }.associateBy { it.pos }
cells.values.forEach { cell ->
cell.pos.surrounds().forEach { coord ->
val neighbour = cells[coord]
if (neighbour != null) {
val height = neighbour.actual() - cell.actual()
if (height <= 1) {
edges.add(Edge(cell, neighbour))
}
}
}
}
return edges
}
fun calculateSteps(
edges: List<Edge<Cell>>,
start: Cell,
end: Cell
): Int? {
val graph = Graph(edges, true)
val path = graph.findPath(start, end)
return if (path.isEmpty()) null else path.size - 1
}
fun calcSolution1(input: List<String>): Int {
val edges = createGrid(input)
val end = edges.map { edge -> edge.c2 }
.find { it.c == 'E' } ?: error("Cannot find E")
val start = edges.map { edge -> edge.c1 }
.find { it.c == 'S' } ?: error("Cannot find S")
return calculateSteps(edges, start, end)
?: error("Cannot find solution from $start to $end")
}
fun calcSolution2(input: List<String>): Int {
val edges = createGrid(input)
val end = edges.map { edge -> edge.c2 }
.find { it.c == 'E' } ?: error("Cannot find E")
return edges.map { edge -> edge.c1 }
.filter { it.actual() == 'a' }
.mapNotNull { start -> calculateSteps(edges, start, end) }
.min()
}
fun part1() {
val testResult = measureAndPrint("Part 1 Test Time: ") {
calcSolution1(test)
}
println("Part 1 Answer = $testResult")
check(testResult == 31)
val result = measureAndPrint("Part 1 Time: ") {
calcSolution1(input)
}
println("Part 1 Answer = $result")
check(result == 339)
}
fun part2() {
val testResult = measureAndPrint("Part 2 Test Time: ") {
calcSolution2(test)
}
println("Part 2 Answer = $testResult")
check(testResult == 29)
val result = measureAndPrint("Part 2 Time: ") {
calcSolution2(input)
}
println("Part 2 Answer = $result")
check(result == 332)
}
println("Day - 12")
part1()
part2()
}
Today’s puzzle required parsing a data structure with nested elements. My brain fell apart initially.
I decided to try a List<Object> where the entry can be an Int or List<Object> Once this worked I refactored to use an abstract class to represent any packet item, packet value and packet list.
Implementing the determination of the order provided some problems until I understood that the packet order is determined once two items aren’t equal.
[1,1,3,1,1]
[1,1,5,1,1]
[[1],[2,3,4]]
[[1],4]
[9]
[[8,7,6]]
[[4,4],4,4]
[[4,4],4,4,4]
[7,7,7,7]
[7,7,7]
[]
[3]
[[[]]]
[[]]
[1,[2,[3,[4,[5,6,7]]]],8,9]
[1,[2,[3,[4,[5,6,0]]]],8,9]
The model for Item, Value and Packet below
abstract class Item
data class Value(val value: Int) : Item() {
fun compareTo(right: Value): Int {
return value.compareTo(right.value)
}
override fun toString(): String {
return value.toString()
}
}
data class Packet(val items: List<Item>) : Item(), List<Item> by items {
constructor(value: Item) : this(listOf(value)) {
}
}
fun parsePacket(input: String): Packet {
val value = StringBuilder()
var list: MutableList<Item> = mutableListOf()
fun checkAdd() {
if (value.isNotBlank()) {
list.add(Value(value.toString().toInt()))
}
value.clear()
}
val lists = Stack<List<Item>>()
var index = 1 // skip first [
while (index < input.lastIndex) {
when (val c = input[index]) {
',', ' ' -> checkAdd()
'[' -> {
lists.push(list)
list = mutableListOf()
}
']' -> {
checkAdd()
val v = list
list = lists.pop().toMutableList()
list.add(Packet(v))
}
'-' -> value.append('-')
'+' -> value.append('+')
else -> {
if (c.isDigit()) {
value.append(c)
} else {
error("Unexpected $c")
}
}
}
index += 1
}
checkAdd()
return Packet(list.toList())
}
We needed comparison functions for the Packet and one for generic Item.
The isOrdered
function couldn’t be part of Item because it references Packet and Value.
fun Packet.compareTo(right: Packet): Int {
var result = 0
for (index in indices) {
val item = this[index]
val rightItem = right.getOrNull(index)
result = when {
rightItem == null -> 1
item is Value && rightItem is Value -> item.compareTo(rightItem)
item is Packet && rightItem is Value -> item.compareTo(Packet(rightItem))
item is Packet && rightItem is Packet -> item.compareTo(rightItem)
item is Value && rightItem is Packet -> Packet(item).compareTo(rightItem)
else -> 1
}
if (result != 0) {
break
}
}
if (result == 0 && size < right.size) {
result = -1
}
return result
}
fun Item.isOrdered(right: Item?): Int {
return when {
right == null -> 1
this is Value && right is Value -> compareTo(right)
this is Packet && right is Packet -> this.compareTo(right)
this is Packet && right is Value -> this.compareTo(Packet(right))
this is Value && right is Packet -> Packet(this).compareTo(right)
else -> 1
}
}
fun isIndexOrdered(index: Int, input: List<String>): Boolean {
check(input.size == 2)
val (left, right) = input.map { parsePacket(it) }
var result = 0
for (index in left.indices) {
val item = left[index]
val rightItem = if (index <= right.lastIndex) right[index]
else null
result = item.isOrdered(rightItem)
if (result != 0) {
break
}
}
return result <= 0
}
In part 1 we had check every 2 pairs to determine if they are in the right order and then report the sum of the indexes of those pairs.
fun calcSolution1(input: List<List<String>>): Int {
return input.mapIndexedNotNull { index, inputs ->
if (isIndexOrdered(index, inputs)) index + 1 else null
}.sum()
}
In part 2 we had to treat all the packets as one list and add 2 more with the values:
"[[2]]"
"[[6]]"
Then we had to sort the packets using the ordering rules from part 1. We need to find the indexes of the 2 additional packets and print the product.
fun calcSolution2(input: List<String>): Int {
val extra1 = "[[2]]"
val extra2 = "[[6]]"
val packets = input.filter { it.isNotBlank() }.toMutableList()
packets.add(extra1)
packets.add(extra2)
val sorted = packets.map { it to parsePacket(it) }
.sortedWith { o1, o2 -> o1.second.compareTo(o2.second) }
.map { it.first }
val index1 = sorted.indexOf(extra1) + 1
val index2 = sorted.indexOf(extra2) + 1
return index1 * index2
}
package day13
import utils.groupLines
import utils.readFile
import utils.readLines
import utils.separator
import java.util.*
fun main() {
val test = readLines(
"""[1,1,3,1,1]
[1,1,5,1,1]
[[1],[2,3,4]]
[[1],4]
[9]
[[8,7,6]]
[[4,4],4,4]
[[4,4],4,4,4]
[7,7,7,7]
[7,7,7]
[]
[3]
[[[]]]
[[]]
[1,[2,[3,[4,[5,6,7]]]],8,9]
[1,[2,[3,[4,[5,6,0]]]],8,9]
""".trimIndent()
)
val input = readFile("day13")
abstract class Item
data class Value(val value: Int) : Item() {
fun compareTo(right: Value): Int {
return value.compareTo(right.value)
}
override fun toString(): String {
return value.toString()
}
}
data class Packet(val items: List<Item>) : Item(), List<Item> by items {
constructor(value: Item) : this(listOf(value)) {}
}
fun parsePacket(input: String): Packet {
val value = StringBuilder()
var list: MutableList<Item> = mutableListOf()
fun checkAdd() {
if (value.isNotBlank()) {
list.add(Value(value.toString().toInt()))
}
value.clear()
}
val lists = Stack<List<Item>>()
var index = 1 // skip first [
while (index < input.lastIndex) {
when (val c = input[index]) {
',', ' ' -> checkAdd()
'[' -> {
lists.push(list)
list = mutableListOf()
}
']' -> {
checkAdd()
val v = list
list = lists.pop().toMutableList()
list.add(Packet(v))
}
'-' -> value.append('-')
'+' -> value.append('+')
else -> {
if (c.isDigit()) {
value.append(c)
} else {
error("Unexpected $c")
}
}
}
index += 1
}
checkAdd()
return Packet(list.toList())
}
fun Packet.compareTo(right: Packet): Int {
var result = 0
for (index in indices) {
val item = this[index]
val rightItem = right.getOrNull(index)
result = when {
rightItem == null -> 1
item is Value && rightItem is Value -> item.compareTo(rightItem)
item is Packet && rightItem is Value -> item.compareTo(Packet(rightItem))
item is Packet && rightItem is Packet -> item.compareTo(rightItem)
item is Value && rightItem is Packet -> Packet(item).compareTo(rightItem)
else -> 1
}
if (result != 0) {
break
}
}
if (result == 0 && size < right.size) {
result = -1
}
return result
}
fun Item.isOrdered(right: Item?): Int {
return when {
right == null -> 1
this is Value && right is Value -> compareTo(right)
this is Packet && right is Packet -> this.compareTo(right)
this is Packet && right is Value -> this.compareTo(Packet(right))
this is Value && right is Packet -> Packet(this).compareTo(right)
else -> 1
}
}
fun isIndexOrdered(index: Int, input: List<String>): Boolean {
check(input.size == 2)
val (left, right) = input.map { parsePacket(it) }
var result = 0
for (index in left.indices) {
val item = left[index]
val rightItem = if (index <= right.lastIndex)
right[index] else null
result = item.isOrdered(rightItem)
if (result != 0) {
break
}
}
val ordered = result <= 0
if (result == 0) {
println("Pair $index: was the same")
} else if (result < 0) {
println("Pair $index: left was lower")
}
return ordered
}
fun calcSolution1(input: List<List<String>>): Int {
return input.mapIndexedNotNull { index, inputs ->
if (isIndexOrdered(index, inputs)) index + 1 else null
}.sum()
}
fun calcSolution2(input: List<String>): Int {
val extra1 = "[[2]]"
val extra2 = "[[6]]"
val packets = input.filter { it.isNotBlank() }.toMutableList()
packets.add(extra1)
packets.add(extra2)
val sorted = packets.map { it to parsePacket(it) }
.sortedWith { o1, o2 -> o1.second.compareTo(o2.second) }
.map { it.first }
val index1 = sorted.indexOf(extra1) + 1
val index2 = sorted.indexOf(extra2) + 1
return index1 * index2
}
fun part1() {
val testResult = calcSolution1(groupLines(test))
println("Part 1 Test Answer = $testResult")
check(testResult == 13)
separator()
val result = calcSolution1(groupLines(input))
println("Part 1 Answer = $result")
check(result == 4643)
}
fun part2() {
val testResult = calcSolution2(test)
println("Part 2 Test Answer = $testResult")
check(testResult == 140)
separator()
val result = calcSolution2(input)
println("Part 2 Answer = $result")
check(result == 21614)
}
println("Day - 13")
part1()
separator()
part2()
}
Today we had to use the input data to represent a cave system and model sand falling into the cave.
In the first part there is a point where the sand falls over the edge and it means all the sand falls down that abyss. So you needed to detect and out of boundary condition.
498,4 -> 498,6 -> 496,6
503,4 -> 502,4 -> 502,9 -> 494,9
The model of the cave is a grid with a map of coordinates and the substance found their, like rock, sand or air.
open class Grid(
val cells: MutableMap<Coord, Substance> = mutableMapOf()
): MutableMap<Coord, Substance> by cells {
open val maxX: Int get() = cells.keys.maxOf { it.x }
open val minX: Int get() = cells.keys.minOf { it.x }
open val maxY: Int get() = cells.keys.maxOf { it.y }
open val minY: Int = 0
fun countSand() = cells.values.count { it == Substance.SAND }
open fun isInGrid(pos: Coord): Boolean = pos.x in minX..maxX
&& pos.y in minY..maxY
fun getSubStance(pos: Coord) = getOrDefault(pos, Substance.AIR)
fun isAir(pos: Coord) = getSubStance(pos) == Substance.AIR
fun isTarget(pos: Coord) = isInGrid(pos) && isAir(pos)
fun canDrop(pos: Coord): Pair<Coord, Direction> {
if (!isInGrid(pos.left()) || !isInGrid(pos.right())) {
return Pair(pos, Direction.ABYSS)
}
if (isInGrid(pos.bottom())) {
if (isTarget(pos.bottom())) {
return canDrop(pos.bottom())
} else {
if (isTarget(pos.bottom().left())) {
return canDrop(pos.bottom().left())
} else if (isTarget(pos.bottom().right())) {
return canDrop(pos.bottom().right())
}
if (isTarget(pos)) {
return Pair(pos, Direction.DOWN)
}
}
}
return Pair(pos, Direction.STOP)
}
}
fun loadStructure(lines: List<String>): Grid {
val grid = mutableMapOf<Coord, Substance>()
lines.map {
it.scanInts()
.windowed(2, 2)
.map { line -> Coord(line[0], line[1]) }
}.forEach { scan ->
scan.windowed(2)
.forEach { pair ->
val prev = pair[0]
val curr = pair[1]
val diff = curr - prev
if (diff.y == 0) {
for (x in min(curr.x, prev.x)..max(curr.x, prev.x)) {
val pos = Coord(x, curr.y)
if (!grid.containsKey(pos)) {
grid[pos] = Substance.ROCK
}
}
} else {
for (y in min(curr.y, prev.y)..max(curr.y, prev.y)) {
val pos = Coord(curr.x, y)
if (!grid.containsKey(pos)) {
grid[pos] = Substance.ROCK
}
}
}
}
}
return Grid(grid)
}
fun isStop(direction: Direction) = direction == Direction.STOP
|| direction == Direction.NONE
|| direction == Direction.ABYSS
fun dropSand(grid: Grid, entry: Coord): Pair<Coord, Boolean> {
val drop = grid.canDrop(entry)
if (drop.second == Direction.ABYSS) {
return Pair(drop.first, false)
}
return Pair(drop.first, !isStop(drop.second))
}
fun simulateSand(grid: Grid, start: Coord, print: Boolean): Int {
do {
val result = dropSand(grid, start)
if (result.second) {
grid[result.first] = Substance.SAND
}
} while (result.second)
return grid.countSand()
}
Part 1 involved simulating the sand while determining the boundary.
fun calcSand(input: List<String>, print: Boolean): Int {
val grid = loadStructure(input)
val entry = Coord(500, 0)
simulateSand(grid, entry, print)
return grid.countSand()
}
Part 2 provided for an infinite floor and I overloaded the grid to provide for a different boundary condition and the floor.
class InfiniteGird(
cells: MutableMap<Coord, Substance>,
val level: Int
) : Grid(cells) {
override val maxY: Int get() = level
override fun get(key: Coord): Substance? {
if (key.y == level) {
return Substance.ROCK
}
return super.get(key)
}
override fun getOrDefault(key: Coord, defaultValue: Substance): Substance {
if (key.y == level) {
return Substance.ROCK
}
return super.getOrDefault(key, defaultValue)
}
override fun isInGrid(pos: Coord): Boolean = pos.y <= level
}
fun calcInfiniteSand(input: List<String>, print: Boolean): Int {
val grid = loadStructure(input)
val entry = Coord(500, 0)
val infiniteGrid = InfiniteGird(grid.toMutableMap(), grid.maxY + 2)
if (print) {
grid.print(entry)
}
infiniteGrid.stretch()
val sand = simulateSand(infiniteGrid, entry, print)
if (print) {
infiniteGrid.print(null)
}
return sand
}
package day14
import main.utils.measureAndPrint
import main.utils.scanInts
import utils.Coord
import utils.readFile
import utils.readLines
import utils.separator
import kotlin.math.max
import kotlin.math.min
enum class Substance(val c: Char) {
AIR('.'),
ROCK('#'),
SAND('o')
}
enum class Direction {
STOP,
NONE,
DOWN,
LEFT,
RIGHT,
ABYSS
}
object Depth {
private val depth = ThreadLocal.withInitial { 0 }
var maxDepth = 0
fun enter() {
depth.set(depth.get() + 1)
maxDepth = max(maxDepth, depth.get())
}
fun exit() {
depth.set(depth.get() - 1)
}
}
fun main() {
val test = readLines(
"""498,4 -> 498,6 -> 496,6
503,4 -> 502,4 -> 502,9 -> 494,9"""
)
val input = readFile("day14")
open class Grid(val cells: MutableMap<Coord, Substance> = mutableMapOf()) : MutableMap<Coord, Substance> by cells {
open val maxX: Int get() = cells.keys.maxOf { it.x }
open val minX: Int get() = cells.keys.minOf { it.x }
open val maxY: Int get() = cells.keys.maxOf { it.y }
open val minY: Int = 0
open fun stretch() {}
fun countSand() = cells.values.count { it == Substance.SAND }
open fun isInGrid(pos: Coord): Boolean = pos.x in minX..maxX
&& pos.y in minY..maxY
fun getSubStance(pos: Coord) = getOrDefault(pos, Substance.AIR)
fun isAir(pos: Coord) = getSubStance(pos) == Substance.AIR
fun isTarget(pos: Coord) = isInGrid(pos) && isAir(pos)
fun canDrop(pos: Coord): Pair<Coord, Direction> {
Depth.enter()
try {
if (!isInGrid(pos.left()) || !isInGrid(pos.right())) {
return Pair(pos, Direction.ABYSS)
}
if (isInGrid(pos.bottom())) {
if (isTarget(pos.bottom())) {
return canDrop(pos.bottom())
} else {
if (isTarget(pos.bottom().left())) {
return canDrop(pos.bottom().left())
} else if (isTarget(pos.bottom().right())) {
return canDrop(pos.bottom().right())
}
if (isTarget(pos)) {
return Pair(pos, Direction.DOWN)
}
}
}
return Pair(pos, Direction.STOP)
} finally {
Depth.exit()
}
}
fun print(entry: Coord?) {
val zX = maxX
val aX = minX
val zY = maxY
val aY = minY
for (y in aY..zY) {
for (x in aX..zX) {
val pos = Coord(x, y)
if (entry != null && pos == entry) {
print('+')
} else {
print(getOrDefault(pos, Substance.AIR).c)
}
}
println()
}
}
}
class InfiniteGird(cells: MutableMap<Coord, Substance>, val level: Int) : Grid(cells) {
override val maxY: Int get() = level
override fun get(key: Coord): Substance? {
if (key.y == level) {
return Substance.ROCK
}
return super.get(key)
}
override fun getOrDefault(key: Coord, defaultValue: Substance): Substance {
if (key.y == level) {
return Substance.ROCK
}
return super.getOrDefault(key, defaultValue)
}
override fun isInGrid(pos: Coord): Boolean = pos.y <= level
override fun stretch() {
val zX = (cells.keys.filter { it.y < level }.maxOfOrNull { it.x } ?: maxX) + 1
val aX = (cells.keys.filter { it.y < level }.minOfOrNull { it.x } ?: minX) - 1
for (x in aX..zX) {
val pos = Coord(x, level)
if (!containsKey(pos)) {
cells[pos] = Substance.ROCK
}
}
}
}
fun loadStructure(lines: List<String>): Grid {
val grid = mutableMapOf<Coord, Substance>()
val scans = lines.map {
it.scanInts()
.windowed(2, 2)
.map { line -> Coord(line[0], line[1]) }
}
scans.forEach { scan ->
scan.windowed(2)
.forEach { pair ->
check(pair.size == 2)
val prev = pair[0]
val curr = pair[1]
val diff = curr - prev
check((diff.x == 0 && diff.y != 0) || (diff.x != 0 && diff.y == 0)) { "Expected one 0 in $diff" }
if (diff.y == 0) {
for (x in min(curr.x, prev.x)..max(curr.x, prev.x)) {
val pos = Coord(x, curr.y)
if (!grid.containsKey(pos)) {
grid[pos] = Substance.ROCK
}
}
} else {
for (y in min(curr.y, prev.y)..max(curr.y, prev.y)) {
val pos = Coord(curr.x, y)
if (!grid.containsKey(pos)) {
grid[pos] = Substance.ROCK
}
}
}
}
}
return Grid(grid)
}
fun isStop(direction: Direction) = direction == Direction.STOP
|| direction == Direction.NONE
|| direction == Direction.ABYSS
fun dropSand(grid: Grid, entry: Coord): Pair<Coord, Boolean> {
val drop = grid.canDrop(entry)
if (drop.second == Direction.ABYSS) {
return Pair(drop.first, false)
}
return Pair(drop.first, !isStop(drop.second))
}
fun simulateSand(grid: Grid, start: Coord, print: Boolean): Int {
do {
val result = dropSand(grid, start)
if (result.second) {
grid[result.first] = Substance.SAND
}
} while (result.second)
return grid.countSand()
}
fun calcSand(input: List<String>, print: Boolean): Int {
val grid = loadStructure(input)
val entry = Coord(500, 0)
if(print) {
grid.print(entry)
}
simulateSand(grid, entry, print)
if(print) {
grid.print(entry)
}
return grid.countSand()
}
fun calcInfiniteSand(input: List<String>, print: Boolean): Int {
val grid = loadStructure(input)
val entry = Coord(500, 0)
val infiniteGrid = InfiniteGird(grid.toMutableMap(), grid.maxY + 2)
if (print) {
grid.print(entry)
}
infiniteGrid.stretch()
val sand = simulateSand(infiniteGrid, entry, print)
if (print) {
infiniteGrid.print(null)
}
return sand
}
fun part1() {
val testResult = calcSand(test, false)
println("Part 1 Test Answer = $testResult")
check(testResult == 24) { "Expected 24 not $testResult" }
val result = measureAndPrint("Test 1 Time: ") {
calcSand(input, false)
}
println("Part 1 Answer = $result")
}
fun part2() {
val testResult = calcInfiniteSand(test, false)
println("Part 2 Test Answer = $testResult")
check(testResult == 93) { "Expected 93 not $testResult" }
val result = measureAndPrint("Test 2 Time: ") {
calcInfiniteSand(input, false)
}
println("Part 2 Answer = $result")
check(result == 22499) { "Expected 22499 not $result" }
}
println("Day - 14")
part1()
part2()
}