Skip to content

Commit

Permalink
Add puzzle solution for 2023, day 15
Browse files Browse the repository at this point in the history
  • Loading branch information
curtislb committed Jan 8, 2024
1 parent 8306b3f commit f818c63
Show file tree
Hide file tree
Showing 18 changed files with 540 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ includes:
* [Advent of Code 2019][aoc-2019-link] (Days 1-25)
* [Advent of Code 2020][aoc-2020-link] (Days 1-25)
* [Advent of Code 2021][aoc-2021-link] (Days 1-25)
* [Advent of Code 2023][aoc-2023-link] (Days 1-14)
* [Advent of Code 2023][aoc-2023-link] (Days 1-15)

## Getting Started

Expand Down
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,7 @@ dependencies {
kover(project(":year2023:day14:dish"))
kover(project(":year2023:day14:part1"))
kover(project(":year2023:day14:part2"))
kover(project(":year2023:day15:hash"))
kover(project(":year2023:day15:part1"))
kover(project(":year2023:day15:part2"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ package com.curtislb.adventofcode.common.number

import java.math.BigInteger

/**
* Returns the result of multiplying this number and [multiplicand], modulo the given [modulus].
*
* @throws IllegalArgumentException If [modulus] is negative or zero.
*/
fun Int.modMultiply(multiplicand: Int, modulus: Int): Int {
require(modulus > 0) { "Modulus must be positive: $modulus" }
return (mod(modulus) * multiplicand.mod(modulus)).mod(modulus)
}

/**
* Returns the result of multiplying this number and [multiplicand], modulo the given [modulus].
*
* @throws IllegalArgumentException If [modulus] is negative or zero.
*/
fun Long.modMultiply(multiplicand: Long, modulus: Long): Long {
require(modulus > 0L) { "Modulus must be positive: $modulus" }
return (this.mod(modulus) * multiplicand.mod(modulus)).mod(modulus)
return (mod(modulus) * multiplicand.mod(modulus)).mod(modulus)
}

/**
Expand All @@ -19,5 +29,5 @@ fun Long.modMultiply(multiplicand: Long, modulus: Long): Long {
*/
fun BigInteger.modMultiply(multiplicand: BigInteger, modulus: BigInteger): BigInteger {
require(modulus > BigInteger.ZERO) { "Modulus must be positive: $modulus" }
return (this.mod(modulus) * multiplicand.mod(modulus)).mod(modulus)
return (mod(modulus) * multiplicand.mod(modulus)).mod(modulus)
}
5 changes: 5 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,8 @@ include("year2023:day13:part2")
include("year2023:day14:dish")
include("year2023:day14:part1")
include("year2023:day14:part2")

// Day 15: Lens Library
include("year2023:day15:hash")
include("year2023:day15:part1")
include("year2023:day15:part2")
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class DishPlatform(initialGrid: Grid<Rock>) {
*
* The total load is given by adding together the loads contributed by all [Rock.ROUND] rocks on
* the platform. The load for each rock is equal to the height of the grid minus the index of
* the row in which the rock is located (starting from the top row with an index of 0).
* the row in which the rock is located (starting from index 0 for the top row).
*/
fun findNorthBeamLoad(): Int = grid.shallowRows().withIndex().sumOf { (rowIndex, row) ->
(grid.height - rowIndex) * row.count { it == Rock.ROUND }
Expand Down
19 changes: 19 additions & 0 deletions year2023/day15/hash/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
val kotlinVersion: String by System.getProperties()
val koverVersion: String by System.getProperties()

kotlin("jvm") version kotlinVersion
id("org.jetbrains.kotlinx.kover") version koverVersion
}

dependencies {
val assertjVersion: String by properties
val junitJupiterVersion: String by properties
val junitPlatformVersion: String by properties

implementation(project(":common:number"))
testImplementation("org.assertj:assertj-core:$assertjVersion")
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:$junitPlatformVersion")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.curtislb.adventofcode.year2023.day15.hash

import com.curtislb.adventofcode.common.number.modMultiply

/**
* An implementation of the HASH algorithm, which converts any string to an integer in the range
* `0..<modulus`.
*
* @property modulus The modulus applied to each intermediate value of the HASH algorithm.
*
* @constructor Creates a new instance of [HashAlgorithm] with the given [modulus].
*/
class HashAlgorithm(private val modulus: Int) {
/**
* Returns the result of applying the HASH algorithm to the given [string].
*/
fun convert(string: String): Int = string.fold(0) { acc, char ->
(acc + char.code).modMultiply(FACTOR, modulus)
}

companion object {
/**
* A multiplicative factor applied to each intermediate value of the HASH algorithm.
*/
private const val FACTOR = 17
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.curtislb.adventofcode.year2023.day15.hash

/**
* A single step in the full HASHMAP process for arranging a sequence of labeled lenses.
*
* @property label The label of the lens on which the step operates.
* @property operation The operation that the step performs on the lens.
*/
data class HashmapStep(val label: String, val operation: Operation) {
companion object {
/**
* A regex used to parse the lens label and operation info from a HASHMAP step string.
*/
private val STEP_REGEX = Regex("""(\w+)([\-=])(\d*)""")

/**
* Returns a [HashmapStep] with a lens label and operation info read from the given string.
*
* The [string] must have one of the following formats:
*
* - `"$label-"`: Perform the [Operation.Dash] operation on the lens with the given `label`.
* - `"$label=$focalLength"`: Perform the [Operation.Equals] operation with the specified
* `focalLength` on the lens with the given `label`.
*
* @throws IllegalArgumentException If [string] is formatted incorrectly.
*/
fun fromString(string: String): HashmapStep {
val matchResult = STEP_REGEX.matchEntire(string)
require(matchResult != null) { "Malformed HASHMAP step string: $string" }

// Parse label and operation info from the string
val (label, operationString, operationArgument) = matchResult.destructured
val operation = when (operationString) {
"-" -> Operation.Dash
"=" -> Operation.Equals(operationArgument.toInt())
else -> error("Invalid operation string: $operationString")
}

return HashmapStep(label, operation)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.curtislb.adventofcode.year2023.day15.hash

/**
* An arrangement of lenses, with various labels and focal lengths, into an ordered sequence of
* boxes.
*
* @param boxCount The number of boxes into which lenses can be arranged.
*
* @constructor Creates a new instance of [LensConfiguration] with the given `boxCount`.
*/
class LensConfiguration(boxCount: Int) {
/**
* A sequence of boxes that makes up the current lens configuration.
*
* Each box is represented by an ordered [LinkedHashMap] from the label of each lens in that box
* to its focal length.
*/
private val boxes: Array<LinkedHashMap<String, Int>> = Array(boxCount) { linkedMapOf() }

/**
* The HASH algorithm used to convert each lens label to the index of its corresponding box.
*/
private val hashAlgorithm: HashAlgorithm = HashAlgorithm(boxCount)

/**
* Returns the focusing power of all the lenses in the current configuration.
*
* The focusing power is given by multiplying the index (starting from 1) of each box by the sum
* of the focal length of each lens in the box multiplied by its index (also from 1) within the
* box.
*/
fun findFocusingPower(): Int = boxes.withIndex().sumOf { (boxIndex, box) ->
val boxPower = box.values.withIndex().sumOf { (valueIndex, value) ->
(valueIndex + 1) * value
}
(boxIndex + 1) * boxPower
}

/**
* Updates the current lens arrangement by performing the specified HASHMAP process [step].
*/
fun performStep(step: HashmapStep) {
val boxIndex = hashAlgorithm.convert(step.label)
val box = boxes[boxIndex]
when (step.operation) {
Operation.Dash -> box.remove(step.label)
is Operation.Equals -> box[step.label] = step.operation.focalLength
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.curtislb.adventofcode.year2023.day15.hash

/**
* An operation that may be performed on a lens as part of the HASHMAP process.
*/
sealed interface Operation {
/**
* A HASHMAP operation that corresponds to removing a lens (of any focal length) from its
* assigned box.
*/
data object Dash : Operation

/**
* A HASHMAP operation that corresponds to placing a lens with the specified focal length in its
* assigned box, replacing any existing lens with the same label in the box.
*
* @property focalLength The focal length of the lens to place in its assigned box.
*
* @constructor Creates a new instance of the [Equals] operation with the given [focalLength].
*/
data class Equals(val focalLength: Int) : Operation
}
1 change: 1 addition & 0 deletions year2023/day15/input/input.txt

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions year2023/day15/input/test_input.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7
24 changes: 24 additions & 0 deletions year2023/day15/part1/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
val kotlinVersion: String by System.getProperties()
val koverVersion: String by System.getProperties()

application
kotlin("jvm") version kotlinVersion
id("org.jetbrains.kotlinx.kover") version koverVersion
}

application {
mainClass.set("com.curtislb.adventofcode.year2023.day15.part1.Year2023Day15Part1Kt")
}

dependencies {
val assertjVersion: String by properties
val junitJupiterVersion: String by properties
val junitPlatformVersion: String by properties

implementation(project(":year2023:day15:hash"))
testImplementation("org.assertj:assertj-core:$assertjVersion")
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:$junitPlatformVersion")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
--- Day 15: Lens Library ---
The newly-focused parabolic reflector dish is sending all of the collected light to a point on the
side of yet another mountain - the largest mountain on Lava Island. As you approach the mountain,
you find that the light is being collected by the wall of a large facility embedded in the
mountainside.
You find a door under a large sign that says "Lava Production Facility" and next to a smaller sign
that says "Danger - Personal Protective Equipment required beyond this point".
As you step inside, you are immediately greeted by a somewhat panicked reindeer wearing goggles and
a loose-fitting hard hat. The reindeer leads you to a shelf of goggles and hard hats (you quickly
find some that fit) and then further into the facility. At one point, you pass a button with a faint
snout mark and the label "PUSH FOR HELP". No wonder you were loaded into that trebuchet so quickly!
You pass through a final set of doors surrounded with even more warning signs and into what must be
the room that collects all of the light from outside. As you admire the large assortment of lenses
available to further focus the light, the reindeer brings you a book titled "Initialization Manual".
"Hello!", the book cheerfully begins, apparently unaware of the concerned reindeer reading over your
shoulder. "This procedure will let you bring the Lava Production Facility online - all without
burning or melting anything unintended!"
"Before you begin, please be prepared to use the Holiday ASCII String Helper algorithm (appendix
1A)." You turn to appendix 1A. The reindeer leans closer with interest.
The HASH algorithm is a way to turn any string of characters into a single number in the range 0 to
255. To run the HASH algorithm on a string, start with a current value of 0. Then, for each
character in the string starting from the beginning:
- Determine the ASCII code for the current character of the string.
- Increase the current value by the ASCII code you just determined.
- Set the current value to itself multiplied by 17.
- Set the current value to the remainder of dividing itself by 256.
After following these steps for each character in the string in order, the current value is the
output of the HASH algorithm.
So, to find the result of running the HASH algorithm on the string HASH:
- The current value starts at 0.
- The first character is H; its ASCII code is 72.
- The current value increases to 72.
- The current value is multiplied by 17 to become 1224.
- The current value becomes 200 (the remainder of 1224 divided by 256).
- The next character is A; its ASCII code is 65.
- The current value increases to 265.
- The current value is multiplied by 17 to become 4505.
- The current value becomes 153 (the remainder of 4505 divided by 256).
- The next character is S; its ASCII code is 83.
- The current value increases to 236.
- The current value is multiplied by 17 to become 4012.
- The current value becomes 172 (the remainder of 4012 divided by 256).
- The next character is H; its ASCII code is 72.
- The current value increases to 244.
- The current value is multiplied by 17 to become 4148.
- The current value becomes 52 (the remainder of 4148 divided by 256).
So, the result of running the HASH algorithm on the string HASH is 52.
The initialization sequence (your puzzle input) is a comma-separated list of steps to start the Lava
Production Facility. Ignore newline characters when parsing the initialization sequence. To verify
that your HASH algorithm is working, the book offers the sum of the result of running the HASH
algorithm on each step in the initialization sequence.
For example:
```
rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7
```
This initialization sequence specifies 11 individual steps; the result of running the HASH algorithm
on each of the steps is as follows:
- rn=1 becomes 30.
- cm- becomes 253.
- qp=3 becomes 97.
- cm=2 becomes 47.
- qp- becomes 14.
- pc=4 becomes 180.
- ot=9 becomes 9.
- ab=5 becomes 197.
- pc- becomes 48.
- pc=6 becomes 214.
- ot=7 becomes 231.
In this example, the sum of these results is 1320. Unfortunately, the reindeer has stolen the page
containing the expected verification number and is currently running around the facility with it
excitedly.
Run the HASH algorithm on each step in the initialization sequence. What is the sum of the results?
(The initialization sequence is one long line; be careful when copy-pasting it.)
*/

package com.curtislb.adventofcode.year2023.day15.part1

import com.curtislb.adventofcode.year2023.day15.hash.HashAlgorithm
import java.nio.file.Path
import java.nio.file.Paths

/**
* Returns the solution to the puzzle for 2023, day 15, part 1.
*
* @param inputPath The path to the input file for this puzzle.
* @param modulus The modulus applied to each intermediate value of the HASH algorithm.
*/
fun solve(inputPath: Path = Paths.get("..", "input", "input.txt"), modulus: Int = 256): Int {
val stepStrings = inputPath.toFile().readText().trim().split(",")
val hashAlgorithm = HashAlgorithm(modulus)
return stepStrings.sumOf { hashAlgorithm.convert(it) }
}

fun main() {
println(solve())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.curtislb.adventofcode.year2023.day15.part1

import java.nio.file.Paths
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

/**
* Tests the solution to the puzzle for 2023, day 15, part 1.
*/
class Year2023Day15Part1Test {
@Test
fun solve_withRealInput() {
val solution = solve()
assertThat(solution).isEqualTo(506437)
}

@Test
fun solve_withTestInput() {
val solution = solve(inputPath = Paths.get("..", "input", "test_input.txt"))
assertThat(solution).isEqualTo(1320)
}
}
Loading

0 comments on commit f818c63

Please sign in to comment.